"""Server-side authentication and encryption utilities.""" import hashlib import secrets from datetime import timedelta from argon2 import PasswordHasher from argon2.exceptions import VerificationError from cryptography.fernet import Fernet from fastapi import Depends, HTTPException, Request from fastapi.security import APIKeyCookie from sqlalchemy import delete from .models import Account, Admin, LoginSession, now password_hasher = PasswordHasher() session_cookie = APIKeyCookie(name="wq_session", auto_error=False) def token_hash(token: str) -> str: return hashlib.sha256(token.encode()).hexdigest() def cipher(settings) -> Fernet: return Fernet(settings.encryption_key.get_secret_value().encode()) async def bootstrap(db, settings): """Only initialize missing singleton records; deployments never reset existing passwords.""" if not await db.get(Admin, 1): db.add( Admin( id=1, username=settings.admin_username, password_hash=password_hasher.hash(settings.admin_password.get_secret_value()), ) ) if not await db.get(Account, 1): db.add(Account(id=1)) await db.execute(delete(LoginSession).where(LoginSession.expires_at < now())) await db.commit() def valid_password(encoded: str, value: str) -> bool: try: return password_hasher.verify(encoded, value) except VerificationError: return False async def issue_session(db, hours: int) -> str: token = secrets.token_urlsafe(32) db.add(LoginSession(token_hash=token_hash(token), expires_at=now() + timedelta(hours=hours))) await db.commit() return token async def require_auth(request: Request, token: str | None = Depends(session_cookie)): if not token: raise HTTPException(401, "请先登录系统") async with request.app.state.sessions() as db: row = await db.get(LoginSession, token_hash(token)) # SQLite fixtures lose tzinfo; production PostgreSQL preserves it. if row is None or row.expires_at.replace(tzinfo=now().tzinfo) <= now(): raise HTTPException(401, "系统登录已过期,请重新登录")