feat: add initial frontend setup with styles, types, and testing framework
- Created a global CSS file for styling the frontend with responsive design. - Introduced TypeScript types for various entities including Research, Alpha, and Account. - Implemented Playwright tests for account management and data synchronization workflows. - Configured TypeScript with strict settings and included necessary libraries. - Set up Vite as the build tool with React plugin and API proxy configuration. - Added a Python script to initialize environment variables securely.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""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, "系统登录已过期,请重新登录")
|
||||
Reference in New Issue
Block a user