dosemate / auth.py
DoseMate
Deploy DoseMate to Hugging Face Spaces
a0c836f
Raw History Blame Contribute Delete
2.71 kB
"""Authentication utilities for the medication-adherence layer.
Self-contained and additive: the existing chat endpoints never import this,
so they stay fully public and unchanged.
"""
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
import bcrypt
from sqlalchemy.orm import Session
import config
from database import get_db, User
# tokenUrl is only used by Swagger's "Authorize" button.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login", auto_error=False)
def _to_bytes(password: str) -> bytes:
# bcrypt only considers the first 72 bytes; truncate explicitly to avoid errors.
return password.encode("utf-8")[:72]
def hash_password(password: str) -> str:
return bcrypt.hashpw(_to_bytes(password), bcrypt.gensalt()).decode("utf-8")
def verify_password(plain: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(_to_bytes(plain), hashed.encode("utf-8"))
except Exception:
return False
def create_access_token(user_id: int) -> str:
expire = datetime.utcnow() + timedelta(minutes=config.JWT_EXPIRE_MIN)
payload = {"sub": str(user_id), "exp": expire}
return jwt.encode(payload, config.JWT_SECRET, algorithm=config.JWT_ALG)
def decode_token(token: str) -> Optional[int]:
try:
payload = jwt.decode(token, config.JWT_SECRET, algorithms=[config.JWT_ALG])
sub = payload.get("sub")
return int(sub) if sub is not None else None
except (JWTError, ValueError):
return None
_credentials_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
def get_current_user(
token: Optional[str] = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> User:
"""FastAPI dependency: resolves the Bearer token to a User or 401s."""
if not token:
raise _credentials_exc
user_id = decode_token(token)
if user_id is None:
raise _credentials_exc
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise _credentials_exc
return user
def get_optional_user(
token: Optional[str] = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> Optional[User]:
"""Like get_current_user but returns None instead of raising.
Used by the /v2 conversation endpoints so chat works logged-in or out.
"""
if not token:
return None
user_id = decode_token(token)
if user_id is None:
return None
return db.query(User).filter(User.id == user_id).first()