fixes and refactor

This commit is contained in:
HotSwapp
2025-08-14 19:16:28 -05:00
parent 5111079149
commit bfc04a6909
61 changed files with 5689 additions and 767 deletions

View File

@@ -3,6 +3,7 @@ Authentication schemas
"""
from typing import Optional
from pydantic import BaseModel, EmailStr
from pydantic.config import ConfigDict
class UserBase(BaseModel):
@@ -32,8 +33,7 @@ class UserResponse(UserBase):
is_admin: bool
theme_preference: Optional[str] = "light"
class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
class ThemePreferenceUpdate(BaseModel):
@@ -45,7 +45,7 @@ class Token(BaseModel):
"""Token response schema"""
access_token: str
token_type: str
refresh_token: str | None = None
refresh_token: Optional[str] = None
class TokenData(BaseModel):

View File

@@ -1,7 +1,7 @@
"""
Authentication and security utilities
"""
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Optional, Union, Tuple
from uuid import uuid4
from jose import JWTError, jwt
@@ -54,12 +54,12 @@ def _decode_with_rotation(token: str) -> dict:
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token"""
to_encode = data.copy()
expire = datetime.utcnow() + (
expire = datetime.now(timezone.utc) + (
expires_delta if expires_delta else timedelta(minutes=settings.access_token_expire_minutes)
)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"iat": datetime.now(timezone.utc),
"type": "access",
})
return _encode_with_rotation(to_encode)
@@ -68,14 +68,14 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
def create_refresh_token(user: User, user_agent: Optional[str], ip_address: Optional[str], db: Session) -> str:
"""Create refresh token, store its JTI in DB for revocation."""
jti = uuid4().hex
expire = datetime.utcnow() + timedelta(minutes=settings.refresh_token_expire_minutes)
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.refresh_token_expire_minutes)
payload = {
"sub": user.username,
"uid": user.id,
"jti": jti,
"type": "refresh",
"exp": expire,
"iat": datetime.utcnow(),
"iat": datetime.now(timezone.utc),
}
token = _encode_with_rotation(payload)
@@ -84,7 +84,7 @@ def create_refresh_token(user: User, user_agent: Optional[str], ip_address: Opti
jti=jti,
user_agent=user_agent,
ip_address=ip_address,
issued_at=datetime.utcnow(),
issued_at=datetime.now(timezone.utc),
expires_at=expire,
revoked=False,
)
@@ -93,6 +93,15 @@ def create_refresh_token(user: User, user_agent: Optional[str], ip_address: Opti
return token
def _to_utc_aware(dt: Optional[datetime]) -> Optional[datetime]:
"""Convert a datetime to UTC-aware. If naive, assume it's already UTC and attach tzinfo."""
if dt is None:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def verify_token(token: str) -> Optional[str]:
"""Verify JWT token and return username"""
try:
@@ -122,14 +131,20 @@ def decode_refresh_token(token: str) -> Optional[dict]:
def is_refresh_token_revoked(jti: str, db: Session) -> bool:
token_row = db.query(RefreshToken).filter(RefreshToken.jti == jti).first()
return not token_row or token_row.revoked or token_row.expires_at <= datetime.utcnow()
if not token_row:
return True
if token_row.revoked:
return True
expires_at_utc = _to_utc_aware(token_row.expires_at)
now_utc = datetime.now(timezone.utc)
return expires_at_utc is not None and expires_at_utc <= now_utc
def revoke_refresh_token(jti: str, db: Session) -> None:
token_row = db.query(RefreshToken).filter(RefreshToken.jti == jti).first()
if token_row and not token_row.revoked:
token_row.revoked = True
token_row.revoked_at = datetime.utcnow()
token_row.revoked_at = datetime.now(timezone.utc)
db.commit()