Files
TieMeasureFlow/src/backend/api/routers/auth.py
T

105 lines
3.4 KiB
Python

"""Authentication router - login, logout, profile."""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.backend.database import get_db
from src.backend.api.middleware.api_key import get_current_user
from src.backend.models.orm.user import User
from src.backend.models.api.user import (
LoginRequest,
LoginResponse,
PinLoginRequest,
PinUserItem,
UserProfileUpdate,
UserResponse,
)
from src.backend.services.auth_service import (
authenticate_pin,
authenticate_user,
login_user,
logout_user,
)
router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.post("/login", response_model=LoginResponse)
async def login(data: LoginRequest, db: AsyncSession = Depends(get_db)):
"""Login with username and password, receive API key."""
user = await authenticate_user(db, data.username, data.password)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
)
api_key = await login_user(db, user)
return LoginResponse(
user=UserResponse.model_validate(user),
api_key=api_key,
)
@router.get("/pin-users", response_model=list[PinUserItem])
async def pin_users(db: AsyncSession = Depends(get_db)):
"""Utenti che possono entrare con PIN (#59): attivi, non admin, con PIN.
Pubblico per costruzione (serve prima del login): espone solo id e nome.
"""
result = await db.execute(
select(User).where(
User.active == True, # noqa: E712
User.is_admin == False, # noqa: E712
User.pin_hash.is_not(None),
)
)
items = [
PinUserItem(id=u.id, display_name=u.display_name or u.username)
for u in result.scalars().all()
]
items.sort(key=lambda i: i.display_name.casefold())
return items
@router.post("/login-pin", response_model=LoginResponse)
async def login_pin(data: PinLoginRequest, db: AsyncSession = Depends(get_db)):
"""Login con user_id e PIN a 4 cifre (#59); stessa risposta del login."""
user = await authenticate_pin(db, data.user_id, data.pin)
api_key = await login_user(db, user)
return LoginResponse(user=UserResponse.model_validate(user), api_key=api_key)
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
request: Request,
all_sessions: bool = False,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Logout. Deletes this session's API key only; ?all_sessions=true
deletes every key of the user."""
await logout_user(
db, user, key=request.headers.get("X-API-Key"), all_sessions=all_sessions
)
@router.get("/me", response_model=UserResponse)
async def get_profile(user: User = Depends(get_current_user)):
"""Get current user profile."""
return UserResponse.model_validate(user)
@router.put("/me", response_model=UserResponse)
async def update_profile(
data: UserProfileUpdate,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update own profile (display_name, language, theme)."""
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
await db.flush()
await db.refresh(user)
return UserResponse.model_validate(user)