feat(auth): add remember me option with persistent DB sessions
This commit is contained in:
52
app/auth.py
52
app/auth.py
@@ -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 "):
|
||||
|
||||
Reference in New Issue
Block a user