diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 8515fa6..2b2c0b6 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -376,6 +376,7 @@ async def auth_status( @router.post("/verify-password") async def verify_password( data: VerifyPasswordRequest, + request: Request, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): @@ -383,11 +384,20 @@ async def verify_password( 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. + Shares the same rate-limit and lockout guards as /login. """ + client_ip = request.client.host if request.client else "unknown" + _check_ip_rate_limit(client_ip) + await _check_account_lockout(current_user) + valid, new_hash = verify_password_with_upgrade(data.password, current_user.password_hash) if not valid: + _record_ip_failure(client_ip) + await _record_failed_login(db, current_user) raise HTTPException(status_code=401, detail="Invalid password") + _failed_attempts.pop(client_ip, None) + # Persist upgraded hash if migration happened if new_hash: current_user.password_hash = new_hash diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index fee7b6d..b4e5fa8 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,3 +1,4 @@ +from app.schemas.auth import SetupRequest, LoginRequest, ChangePasswordRequest, VerifyPasswordRequest from app.schemas.settings import SettingsUpdate, SettingsResponse from app.schemas.todo import TodoCreate, TodoUpdate, TodoResponse from app.schemas.calendar_event import CalendarEventCreate, CalendarEventUpdate, CalendarEventResponse @@ -8,6 +9,10 @@ from app.schemas.person import PersonCreate, PersonUpdate, PersonResponse from app.schemas.location import LocationCreate, LocationUpdate, LocationResponse __all__ = [ + "SetupRequest", + "LoginRequest", + "ChangePasswordRequest", + "VerifyPasswordRequest", "SettingsUpdate", "SettingsResponse", "TodoCreate", diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 198eb89..806835d 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -64,3 +64,10 @@ class ChangePasswordRequest(BaseModel): class VerifyPasswordRequest(BaseModel): password: str + + @field_validator("password") + @classmethod + def validate_length(cls, v: str) -> str: + if len(v) > 128: + raise ValueError("Password must be 128 characters or fewer") + return v diff --git a/frontend/src/components/layout/LockOverlay.tsx b/frontend/src/components/layout/LockOverlay.tsx index 281faf7..3108ded 100644 --- a/frontend/src/components/layout/LockOverlay.tsx +++ b/frontend/src/components/layout/LockOverlay.tsx @@ -80,6 +80,7 @@ export default function LockOverlay() { setPassword(e.target.value)} placeholder="Enter password to unlock" diff --git a/frontend/src/hooks/useLock.tsx b/frontend/src/hooks/useLock.tsx index 4dc01f4..fed762d 100644 --- a/frontend/src/hooks/useLock.tsx +++ b/frontend/src/hooks/useLock.tsx @@ -29,6 +29,8 @@ export function LockProvider({ children }: { children: ReactNode }) { const timerRef = useRef | null>(null); const lastActivityRef = useRef(Date.now()); + const activeMutationsRef = useRef(activeMutations); + activeMutationsRef.current = activeMutations; const lock = useCallback(() => { setIsLocked(true); @@ -60,7 +62,7 @@ export function LockProvider({ children }: { children: ReactNode }) { const resetTimer = () => { // Don't lock while TanStack mutations are in flight - if (activeMutations > 0) return; + if (activeMutationsRef.current > 0) return; if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { @@ -92,7 +94,7 @@ export function LockProvider({ children }: { children: ReactNode }) { timerRef.current = null; } }; - }, [settings?.auto_lock_enabled, settings?.auto_lock_minutes, isLocked, lock, activeMutations]); + }, [settings?.auto_lock_enabled, settings?.auto_lock_minutes, isLocked, lock]); return ( diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b47ec34..510aacd 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,5 +1,6 @@ export interface Settings { id: number; + user_id: number; accent_color: string; upcoming_days: number; preferred_name?: string | null;