diff --git a/backend/alembic/versions/025_add_auto_lock_settings.py b/backend/alembic/versions/025_add_auto_lock_settings.py new file mode 100644 index 0000000..a50992c --- /dev/null +++ b/backend/alembic/versions/025_add_auto_lock_settings.py @@ -0,0 +1,30 @@ +"""Add auto-lock settings columns to settings table. + +Revision ID: 025 +Revises: 024 +Create Date: 2026-02-25 +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers +revision = "025" +down_revision = "024" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "settings", + sa.Column("auto_lock_enabled", sa.Boolean(), server_default="false", nullable=False), + ) + op.add_column( + "settings", + sa.Column("auto_lock_minutes", sa.Integer(), server_default="5", nullable=False), + ) + + +def downgrade() -> None: + op.drop_column("settings", "auto_lock_minutes") + op.drop_column("settings", "auto_lock_enabled") diff --git a/backend/app/models/settings.py b/backend/app/models/settings.py index 6552d7c..cf5ea87 100644 --- a/backend/app/models/settings.py +++ b/backend/app/models/settings.py @@ -42,6 +42,10 @@ class Settings(Base): ntfy_todo_lead_days: Mapped[int] = mapped_column(Integer, default=1, server_default="1") ntfy_project_lead_days: Mapped[int] = mapped_column(Integer, default=2, server_default="2") + # Auto-lock settings + auto_lock_enabled: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") + auto_lock_minutes: Mapped[int] = mapped_column(Integer, default=5, server_default="5") + @property def ntfy_has_token(self) -> bool: """Derived field for SettingsResponse — True when an auth token is stored.""" diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 854e5bf..8515fa6 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -28,7 +28,7 @@ from app.database import get_db from app.models.user import User from app.models.session import UserSession from app.models.settings import Settings -from app.schemas.auth import SetupRequest, LoginRequest, ChangePasswordRequest +from app.schemas.auth import SetupRequest, LoginRequest, ChangePasswordRequest, VerifyPasswordRequest from app.services.auth import ( hash_password, verify_password_with_upgrade, @@ -373,6 +373,29 @@ async def auth_status( return {"authenticated": authenticated, "setup_required": setup_required} +@router.post("/verify-password") +async def verify_password( + data: VerifyPasswordRequest, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """ + Verify the current user's password without changing anything. + Used by the frontend lock screen to re-authenticate without a full login. + Also handles transparent bcrypt→Argon2id upgrade. + """ + valid, new_hash = verify_password_with_upgrade(data.password, current_user.password_hash) + if not valid: + raise HTTPException(status_code=401, detail="Invalid password") + + # Persist upgraded hash if migration happened + if new_hash: + current_user.password_hash = new_hash + await db.commit() + + return {"verified": True} + + @router.post("/change-password") async def change_password( data: ChangePasswordRequest, diff --git a/backend/app/routers/settings.py b/backend/app/routers/settings.py index 50d0959..6ac79a3 100644 --- a/backend/app/routers/settings.py +++ b/backend/app/routers/settings.py @@ -36,6 +36,8 @@ def _to_settings_response(s: Settings) -> SettingsResponse: ntfy_todo_lead_days=s.ntfy_todo_lead_days, ntfy_project_lead_days=s.ntfy_project_lead_days, ntfy_has_token=bool(s.ntfy_auth_token), # derived — never expose the token value + auto_lock_enabled=s.auto_lock_enabled, + auto_lock_minutes=s.auto_lock_minutes, created_at=s.created_at, updated_at=s.updated_at, ) diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index eb09b0d..198eb89 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -60,3 +60,7 @@ class ChangePasswordRequest(BaseModel): @classmethod def validate_new_password(cls, v: str) -> str: return _validate_password_strength(v) + + +class VerifyPasswordRequest(BaseModel): + password: str diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index ab32bee..8591e62 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -31,6 +31,17 @@ class SettingsUpdate(BaseModel): ntfy_todo_lead_days: Optional[int] = None ntfy_project_lead_days: Optional[int] = None + # Auto-lock settings + auto_lock_enabled: Optional[bool] = None + auto_lock_minutes: Optional[int] = None + + @field_validator('auto_lock_minutes') + @classmethod + def validate_auto_lock_minutes(cls, v: Optional[int]) -> Optional[int]: + if v is not None and not (1 <= v <= 60): + raise ValueError("auto_lock_minutes must be between 1 and 60") + return v + @field_validator('first_day_of_week') @classmethod def validate_first_day(cls, v: int | None) -> int | None: @@ -134,6 +145,10 @@ class SettingsResponse(BaseModel): # Derived field: computed via Settings.ntfy_has_token property (from_attributes reads it) ntfy_has_token: bool = False + # Auto-lock settings + auto_lock_enabled: bool = False + auto_lock_minutes: int = 5 + created_at: datetime updated_at: datetime diff --git a/frontend/src/components/auth/AmbientBackground.tsx b/frontend/src/components/auth/AmbientBackground.tsx new file mode 100644 index 0000000..d169c80 --- /dev/null +++ b/frontend/src/components/auth/AmbientBackground.tsx @@ -0,0 +1,45 @@ +/** + * Shared animated ambient background for the login screen and lock overlay. + * Renders floating gradient orbs and a subtle grid overlay. + */ +export default function AmbientBackground() { + return ( +