Files
delphi-database/app/api/auth.py
2025-08-10 19:06:21 -05:00

178 lines
5.2 KiB
Python

"""
Authentication API endpoints
"""
from datetime import datetime, timedelta
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.database.base import get_db
from app.models.user import User
from app.auth.security import (
authenticate_user,
create_access_token,
get_password_hash,
get_current_user,
get_admin_user
)
from app.auth.schemas import (
Token,
UserCreate,
UserResponse,
LoginRequest,
ThemePreferenceUpdate
)
from app.config import settings
from app.core.logging import get_logger, log_auth_attempt
router = APIRouter()
logger = get_logger("auth")
@router.post("/login", response_model=Token)
async def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_db)):
"""Login endpoint"""
client_ip = request.client.host if request.client else "unknown"
user_agent = request.headers.get("user-agent", "")
logger.info(
"Login attempt started",
username=login_data.username,
client_ip=client_ip,
user_agent=user_agent
)
user = authenticate_user(db, login_data.username, login_data.password)
if not user:
log_auth_attempt(
username=login_data.username,
success=False,
ip_address=client_ip,
user_agent=user_agent,
error="Invalid credentials"
)
logger.warning(
"Login failed - invalid credentials",
username=login_data.username,
client_ip=client_ip
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Update last login
user.last_login = datetime.utcnow()
db.commit()
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
log_auth_attempt(
username=login_data.username,
success=True,
ip_address=client_ip,
user_agent=user_agent
)
logger.info(
"Login successful",
username=login_data.username,
user_id=user.id,
client_ip=client_ip
)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/register", response_model=UserResponse)
async def register(
user_data: UserCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user) # Only admins can create users
):
"""Register new user (admin only)"""
# Check if username or email already exists
existing_user = db.query(User).filter(
(User.username == user_data.username) | (User.email == user_data.email)
).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username or email already registered"
)
# Create new user
hashed_password = get_password_hash(user_data.password)
new_user = User(
username=user_data.username,
email=user_data.email,
full_name=user_data.full_name,
hashed_password=hashed_password
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
@router.get("/me", response_model=UserResponse)
async def read_users_me(current_user: User = Depends(get_current_user)):
"""Get current user info"""
logger.debug("User info requested", username=current_user.username, user_id=current_user.id)
return current_user
@router.post("/refresh", response_model=Token)
async def refresh_token(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Refresh access token for current user"""
# Update last login timestamp
current_user.last_login = datetime.utcnow()
db.commit()
# Create new token with full expiration time
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
data={"sub": current_user.username},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/users", response_model=List[UserResponse])
async def list_users(
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""List all users (admin only)"""
users = db.query(User).all()
return users
@router.post("/theme-preference")
async def update_theme_preference(
theme_data: ThemePreferenceUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Update user's theme preference"""
if theme_data.theme_preference not in ['light', 'dark']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Theme preference must be 'light' or 'dark'"
)
current_user.theme_preference = theme_data.theme_preference
db.commit()
return {"message": "Theme preference updated successfully", "theme": theme_data.theme_preference}