from __future__ import annotations import hashlib import hmac from fastapi import Depends, Header, HTTPException, Request, status from fastapi.security import HTTPBasic, HTTPBasicCredentials from .config import ( ACCOUNTS_AUTH_PASSWORD, ACCOUNTS_SESSION_COOKIE, ADMIN_AUTH_PASSWORD, ADMIN_SESSION_COOKIE, BASIC_AUTH_PASSWORD, INGEST_AUTH_TOKEN, RECORDS_SESSION_COOKIE, ) security = HTTPBasic(auto_error=False) def configured_password(label: str, password: str) -> str: if not password: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"服务未配置{label}访问密码", ) return password def session_token(password: str, purpose: bytes) -> str: return hmac.new( password.encode("utf-8"), purpose, hashlib.sha256, ).hexdigest() def has_valid_session(request: Request, cookie_name: str, password: str, purpose: bytes) -> bool: token = request.cookies.get(cookie_name, "") return bool(password and token) and hmac.compare_digest(token, session_token(password, purpose)) def has_valid_basic_auth(credentials: HTTPBasicCredentials | None, password: str) -> bool: if credentials is None: return False return bool(password) and hmac.compare_digest(credentials.password, password) def is_records_authenticated( request: Request, credentials: HTTPBasicCredentials | None = None, ) -> bool: return has_valid_session( request, RECORDS_SESSION_COOKIE, BASIC_AUTH_PASSWORD, b"xsk-education-management-records-session-v1", ) or has_valid_basic_auth(credentials, BASIC_AUTH_PASSWORD) def is_admin_authenticated( request: Request, credentials: HTTPBasicCredentials | None = None, ) -> bool: return has_valid_session( request, ADMIN_SESSION_COOKIE, ADMIN_AUTH_PASSWORD, b"xsk-admin-web-session-v1", ) or has_valid_session( request, ACCOUNTS_SESSION_COOKIE, ADMIN_AUTH_PASSWORD, b"xsk-accounts-web-session-v1", ) or has_valid_basic_auth(credentials, ADMIN_AUTH_PASSWORD) def is_accounts_authenticated( request: Request, credentials: HTTPBasicCredentials | None = None, ) -> bool: return is_admin_authenticated(request, credentials) def verify_records_auth( request: Request, credentials: HTTPBasicCredentials | None = Depends(security), ) -> str: configured_password("课程记录", BASIC_AUTH_PASSWORD) if not is_records_authenticated(request, credentials): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="请先登录", ) return "records" def verify_admin_auth( request: Request, credentials: HTTPBasicCredentials | None = Depends(security), ) -> str: configured_password("管理后台", ADMIN_AUTH_PASSWORD) if not is_admin_authenticated(request, credentials): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="请先登录管理后台", ) return "admin" def verify_accounts_auth( request: Request, credentials: HTTPBasicCredentials | None = Depends(security), ) -> str: return verify_admin_auth(request, credentials) def verify_any_auth( request: Request, credentials: HTTPBasicCredentials | None = Depends(security), ) -> str: if is_records_authenticated(request, credentials) or is_admin_authenticated(request, credentials): return "authenticated" raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="请先登录", ) def verify_ingest_token(x_ingest_token: str = Header(default="")) -> str: configured_password("课程小结推送", INGEST_AUTH_TOKEN) if not hmac.compare_digest(x_ingest_token, INGEST_AUTH_TOKEN): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="课程小结推送 token 不正确", ) return "ingest"