This commit is contained in:
Garret Patti
2026-06-29 10:41:29 -04:00
parent d48c1e973e
commit 2b0b19eb91
23 changed files with 969 additions and 44 deletions

81
backend/app/auth.py Normal file
View File

@@ -0,0 +1,81 @@
from datetime import datetime, timedelta, timezone
from fastapi import Depends, HTTPException, Query
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
import bcrypt
from jose import JWTError, jwt
from app.config import settings
from app.database import get_db
from app.models import User
security = HTTPBearer()
_optional_security = HTTPBearer(auto_error=False)
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(plain: str, hashed: str) -> bool:
return bcrypt.checkpw(plain.encode(), hashed.encode())
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(days=settings.access_token_expire_days)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.secret_key, algorithm="HS256")
def decode_access_token(token: str) -> dict:
try:
return jwt.decode(token, settings.secret_key, algorithms=["HS256"])
except JWTError:
raise HTTPException(401, "Invalid or expired token")
async def _resolve_user(token_str: str, db: AsyncSession) -> User:
payload = decode_access_token(token_str)
user_id_str: str = payload.get("sub")
if user_id_str is None:
raise HTTPException(401, "Invalid token payload")
try:
user_id = int(user_id_str)
except (ValueError, TypeError):
raise HTTPException(401, "Invalid token payload")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalars().first()
if not user:
raise HTTPException(401, "User not found")
return user
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db),
) -> User:
return await _resolve_user(credentials.credentials, db)
async def get_current_user_from_query_or_header(
token: str | None = Query(default=None),
credentials: HTTPAuthorizationCredentials | None = Depends(_optional_security),
db: AsyncSession = Depends(get_db),
) -> User:
if credentials:
token_str = credentials.credentials
elif token:
token_str = token
else:
raise HTTPException(401, "Not authenticated")
return await _resolve_user(token_str, db)
async def get_current_admin_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_admin:
raise HTTPException(403, "Admin privileges required")
return current_user