Files
worldquant-alpha-system/backend/app/security.py
T

66 lines
2.1 KiB
Python
Raw Normal View History

"""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, "系统登录已过期,请重新登录")