feat: recurring tasks, habits, attachments, import/export, audit and security

This commit is contained in:
2026-09-05 13:35:17 +08:00
parent 57145c198b
commit 067d27a477
22 changed files with 1231 additions and 33 deletions
+23 -3
View File
@@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError
from fastapi import Cookie, Depends, HTTPException, Response, status
from fastapi import Cookie, Depends, HTTPException, Request, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,6 +14,7 @@ from .models import Session, User
password_hasher = PasswordHasher()
COOKIE_NAME = "dodo_session"
CSRF_COOKIE_NAME = "dodo_csrf"
def hash_token(token: str) -> str:
@@ -31,15 +32,24 @@ def verify_password(password_hash: str, password: str) -> bool:
return False
async def issue_session(db: AsyncSession, response: Response, user: User) -> None:
async def issue_session(db: AsyncSession, response: Response, user: User, request: Request | None = None) -> None:
token = secrets.token_urlsafe(32)
csrf = secrets.token_urlsafe(24)
expires = datetime.now(UTC) + timedelta(days=get_settings().session_days)
db.add(Session(token_hash=hash_token(token), user_id=user.id, expires_at=expires))
db.add(Session(
token_hash=hash_token(token), user_id=user.id, expires_at=expires,
ip_address=request.client.host if request and request.client else None,
user_agent=request.headers.get("user-agent", "")[:500] if request else None,
))
await db.commit()
response.set_cookie(
COOKIE_NAME, token, max_age=get_settings().session_days * 86400,
httponly=True, secure=get_settings().cookie_secure, samesite="lax", path="/",
)
response.set_cookie(
CSRF_COOKIE_NAME, csrf, max_age=get_settings().session_days * 86400,
httponly=False, secure=get_settings().cookie_secure, samesite="lax", path="/",
)
async def session_token(
@@ -63,3 +73,13 @@ async def current_user(
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="会话已失效,请重新登录")
return user
async def current_session(
token: str = Depends(session_token),
db: AsyncSession = Depends(get_db),
) -> Session:
row = await db.scalar(select(Session).where(Session.token_hash == hash_token(token), Session.expires_at > datetime.now(UTC)))
if row is None:
raise HTTPException(status_code=401, detail="会话已失效,请重新登录")
return row