-
Notifications
You must be signed in to change notification settings - Fork 2
feat(auth): first-run local administrator onboarding #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
1012839419a-alt
wants to merge
3
commits into
2233admin:main
Choose a base branch
from
1012839419a-alt:feat/local-admin-onboarding
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+917
−102
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| --- | ||
| schema: design-pipeline.motion-foundation.v0.1 | ||
| name: OpenCLI operator motion language | ||
| posture: minimal | ||
| primitiveRegistry: design-pipeline.motion-primitives.v1 | ||
| --- | ||
|
|
||
| ## Motion Thesis | ||
|
|
||
| Motion confirms a completed operator action or a changed system state. It never delays access to credentials, recovery, or operational data. | ||
|
|
||
| ## Motion Principles | ||
|
|
||
| - Keep authentication transitions short, interruptible, and secondary to the active form state. | ||
| - Never move focused controls or change their order while the user is typing. | ||
| - Prefer opacity and color feedback over layout movement for repeated operational use. | ||
|
|
||
| ## Motion Vocabulary | ||
|
|
||
| - primitive: reveal.trim-line | ||
| - Use only for a non-blocking transition between login states. | ||
|
|
||
| ## Procedural Motion | ||
|
|
||
| No procedural motion is used for authentication or recovery surfaces. | ||
|
|
||
| ## Runtime Policy | ||
|
|
||
| CSS transitions are the default adapter for small state changes. The existing Motion React adapter may preserve the selected primitive where it is already loaded; no new animation runtime is introduced. | ||
|
|
||
| ## Reduced Motion | ||
|
|
||
| When `prefers-reduced-motion` is enabled, state changes use immediate opacity changes and do not animate position, scale, or background effects. | ||
|
|
||
| Fallback: every animated confirmation has an immediate static state change with the same text and focus result. | ||
|
|
||
| ## Source Decisions | ||
|
|
||
| - Adopted: the existing login surface's short, non-blocking confirmation transitions; this keeps the new authentication states consistent with repeated console use. | ||
| - Rejected: decorative background and position animation for password and recovery states; these make an access-critical form less legible and are not required for the operator workflow. | ||
| - Authored for `openspec/changes/local-admin-onboarding`; no external motion implementation or visual reference is adopted. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| """First-run local administrator setup and password login.""" | ||
|
|
||
| from hmac import compare_digest | ||
| from typing import Annotated | ||
|
|
||
| from fastapi import APIRouter, Depends, HTTPException, Request, status | ||
| from pydantic import BaseModel, Field | ||
| from sqlalchemy import select | ||
| from sqlalchemy.exc import IntegrityError | ||
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from backend.config import get_settings | ||
| from backend.database import get_db | ||
| from backend.models.identity import LocalAdmin | ||
| from backend.schemas.common import ApiResponse | ||
| from backend.security.local_auth import ( | ||
| hash_password, | ||
| issue_session, | ||
| login_attempt_limiter, | ||
| verify_password, | ||
| ) | ||
|
|
||
| router = APIRouter(prefix="/auth/local", tags=["auth"]) | ||
|
|
||
|
|
||
| class PasswordInput(BaseModel): | ||
| password: str = Field(min_length=12, max_length=256) | ||
|
|
||
|
|
||
| class SetupInput(PasswordInput): | ||
| bootstrap_token: str = Field(min_length=1, max_length=1024) | ||
|
|
||
|
|
||
| async def _admin(db: AsyncSession) -> LocalAdmin | None: | ||
| return (await db.execute(select(LocalAdmin))).scalar_one_or_none() | ||
|
|
||
|
|
||
| @router.get("/status", response_model=ApiResponse[dict]) | ||
| async def local_status(db: Annotated[AsyncSession, Depends(get_db)]) -> ApiResponse: | ||
| return ApiResponse.ok({"configured": await _admin(db) is not None}) | ||
|
|
||
|
|
||
| @router.post("/setup", response_model=ApiResponse[dict]) | ||
| async def setup_local_admin( | ||
| body: SetupInput, | ||
| request: Request, | ||
| db: Annotated[AsyncSession, Depends(get_db)], | ||
| ) -> ApiResponse: | ||
| if await _admin(db) is not None: | ||
| raise HTTPException(status.HTTP_409_CONFLICT, "Local administrator is already configured") | ||
| expected = get_settings().bootstrap_admin_token | ||
| client_id = request.client.host if request.client else "unknown" | ||
| login_attempt_limiter.check(client_id) | ||
| if not expected or not compare_digest(body.bootstrap_token, expected): | ||
| login_attempt_limiter.record_failure(client_id) | ||
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid recovery credential") | ||
| db.add(LocalAdmin(id="local-admin", password_hash=hash_password(body.password))) | ||
| try: | ||
| await db.flush() | ||
| except IntegrityError as exc: | ||
| raise HTTPException( | ||
| status.HTTP_409_CONFLICT, "Local administrator is already configured" | ||
| ) from exc | ||
| login_attempt_limiter.reset(client_id) | ||
| return ApiResponse.ok({"access_token": issue_session()}) | ||
|
|
||
|
|
||
| @router.post("/login", response_model=ApiResponse[dict]) | ||
| async def local_login( | ||
| body: PasswordInput, | ||
| request: Request, | ||
| db: Annotated[AsyncSession, Depends(get_db)], | ||
| ) -> ApiResponse: | ||
| client_id = request.client.host if request.client else "unknown" | ||
| login_attempt_limiter.check(client_id) | ||
| admin = await _admin(db) | ||
| if admin is None or not verify_password(body.password, admin.password_hash): | ||
| login_attempt_limiter.record_failure(client_id) | ||
| raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid administrator password") | ||
| login_attempt_limiter.reset(client_id) | ||
| return ApiResponse.ok({"access_token": issue_session()}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
backend/migrations/versions/z7a8b9c0d1e2_add_local_admin.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """add local administrator credential | ||
|
|
||
| Revision ID: z7a8b9c0d1e2 | ||
| Revises: k8l9m0n1o2p3 | ||
| """ | ||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
| revision = "z7a8b9c0d1e2" | ||
| down_revision = "k8l9m0n1o2p3" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.create_table( | ||
| "local_admin_credentials", | ||
| sa.Column("id", sa.String(length=36), nullable=False), | ||
| sa.Column("password_hash", sa.String(length=255), nullable=False), | ||
| sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), | ||
| sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), | ||
| sa.PrimaryKeyConstraint("id"), | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.drop_table("local_admin_credentials") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Make the reduced-motion contract consistent.
MOTION.mdrequires an immediate state change for reduced motion and says that authentication uses no procedural motion. The feature specification permits a Motion React adapter but does not require an immediate static reduced-motion state. This can produce an animated reduced-motion path.MOTION.md#L23-L35: Define whetherreveal.trim-lineis permitted on authentication surfaces and state that reduced motion has no animation.openspec/changes/local-admin-onboarding/motion.md#L5-L8: Require the immediate static text and focus result when reduced motion is enabled.📍 Affects 2 files
MOTION.md#L23-L35(this comment)openspec/changes/local-admin-onboarding/motion.md#L5-L8🤖 Prompt for AI Agents