feat(auth): add remember me option with persistent DB sessions

This commit is contained in:
2026-07-05 21:28:58 +08:00
parent 2bd914532e
commit 9692ff8ade
4 changed files with 60 additions and 21 deletions

View File

@@ -1,14 +1,15 @@
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from uuid import uuid4
import bcrypt
from fastapi import Cookie, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session as DbSession
from app.config import SECRET_KEY, SESSION_COOKIE_NAME
from app.database import get_db
from app.models import ApiKey, User
from app.models import ApiKey, User, UserSession
# ---------- Password ----------
@@ -22,34 +23,49 @@ def verify_password(password: str, password_hash: str) -> bool:
# ---------- Session ----------
# In-memory session store: token -> user_id
_sessions: dict[str, int] = {}
def create_session(user_id: int) -> str:
def create_session(user_id: int, db: DbSession, remember_me: bool = False) -> str:
token = uuid4().hex
_sessions[token] = user_id
expires_at = (datetime.now(timezone.utc) + timedelta(days=365)) if remember_me else None
db.add(UserSession(token=token, user_id=user_id, expires_at=expires_at))
db.commit()
return token
def destroy_session(token: str) -> None:
_sessions.pop(token, None)
def destroy_session(token: str, db: DbSession) -> None:
db.query(UserSession).filter(UserSession.token == token).delete()
db.commit()
def get_session_user_id(token: str | None) -> int | None:
def get_session_user_id(token: str | None, db: DbSession) -> int | None:
if token is None:
return None
return _sessions.get(token)
s = db.query(UserSession).filter(UserSession.token == token).first()
if s is None:
return None
if s.expires_at and s.expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
db.delete(s)
db.commit()
return None
return s.user_id
def cleanup_expired_sessions(db: DbSession) -> None:
now = datetime.now(timezone.utc)
db.query(UserSession).filter(
UserSession.expires_at.isnot(None),
UserSession.expires_at < now,
).delete()
db.commit()
# ---------- Web Dependencies ----------
def get_current_user_web(
request: Request,
db: Session = Depends(get_db),
db: DbSession = Depends(get_db),
) -> User:
token = request.cookies.get(SESSION_COOKIE_NAME)
uid = get_session_user_id(token)
uid = get_session_user_id(token, db)
if uid is None:
raise HTTPException(status_code=303, headers={"Location": "/login"})
user = db.query(User).filter(User.id == uid).first()
@@ -58,10 +74,10 @@ def get_current_user_web(
return user
def redirect_if_not_logged_in(request: Request, db: Session) -> User | None:
def redirect_if_not_logged_in(request: Request, db: DbSession) -> User | None:
"""Return user if logged in, else None — caller decides redirect."""
token = request.cookies.get(SESSION_COOKIE_NAME)
uid = get_session_user_id(token)
uid = get_session_user_id(token, db)
if uid is None:
return None
return db.query(User).filter(User.id == uid).first()
@@ -71,7 +87,7 @@ def redirect_if_not_logged_in(request: Request, db: Session) -> User | None:
def get_current_user_api(
request: Request,
db: Session = Depends(get_db),
db: DbSession = Depends(get_db),
) -> User:
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):