"""Authentication endpoints: register, login, refresh, me.""" from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.api.deps import get_current_user from app.core.security import ( create_access_token, create_refresh_token, verify_password, verify_token, get_password_hash, ) from app.db.session import get_async_session from app.models import Account, User, Role from app.schemas.auth import ( RegisterRequest, LoginRequest, RefreshRequest, TokenResponse, UserInfo, UserResponse, ) router = APIRouter(prefix="/auth", tags=["auth"]) def _build_user_info(user: User, role: Role | None = None) -> UserInfo: """Helper to extract UserInfo from a User model instance.""" # Fallback to user.role if loaded, else use the provided role role_obj = role or user.role return UserInfo( id=user.id, email=user.email, full_name=user.full_name, account_id=user.account_id, role_id=user.role_id, role_name=role_obj.name if role_obj else None, permissions=role_obj.permissions if role_obj else [], ) @router.post( "/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED ) async def register( body: RegisterRequest, session: AsyncSession = Depends(get_async_session), ): """Register a new account with an admin user.""" # Check if email already exists result = await session.execute(select(User).where(User.email == body.email)) if result.scalars().first(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="A user with this email already exists", ) # Check if account name already exists result = await session.execute( select(Account).where(Account.name == body.account_name) ) if result.scalars().first(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="An account with this name already exists", ) # Create account account = Account(name=body.account_name) session.add(account) await session.flush() # Create admin role admin_permissions = [ "projects:read", "projects:write", "projects:delete", "equipment:read", "equipment:write", "equipment:delete", "crew:read", "crew:write", "crew:delete", "vehicles:read", "vehicles:write", "vehicles:delete", "contacts:read", "contacts:write", "contacts:delete", "users:read", "users:write", "users:delete", "roles:read", "roles:write", ] admin_role = Role( account_id=account.id, name="Admin", description="Full access to all features", permissions=admin_permissions, ) session.add(admin_role) await session.flush() # Create admin user user = User( account_id=account.id, email=body.email, full_name=body.full_name, password_hash=get_password_hash(body.password), role_id=admin_role.id, ) session.add(user) await session.commit() await session.refresh(user) # Generate tokens access_token = create_access_token(subject=user.id) refresh_token = create_refresh_token(subject=user.id) return TokenResponse( access_token=access_token, refresh_token=refresh_token, user=_build_user_info(user), ) @router.post("/login", response_model=TokenResponse) async def login( body: LoginRequest, session: AsyncSession = Depends(get_async_session), ): """Authenticate user with email and password.""" result = await session.execute( select(User).options(selectinload(User.role)).where(User.email == body.email) ) user = result.scalars().first() if not user or not verify_password(body.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account is deactivated", ) access_token = create_access_token(subject=user.id) refresh_token = create_refresh_token(subject=user.id) return TokenResponse( access_token=access_token, refresh_token=refresh_token, user=_build_user_info(user), ) @router.post("/refresh", response_model=TokenResponse) async def refresh( body: RefreshRequest, session: AsyncSession = Depends(get_async_session), ): """Refresh an access token using a valid refresh token.""" try: payload = verify_token(body.refresh_token) if payload.get("type") != "refresh": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type", ) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload", ) except Exception: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired refresh token", ) # Verify user exists and is active result = await session.execute(select(User).where(User.id == user_id)) user = result.scalars().first() if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive", ) access_token = create_access_token(subject=user.id) new_refresh_token = create_refresh_token(subject=user.id) return TokenResponse( access_token=access_token, refresh_token=new_refresh_token, user=_build_user_info(user), ) @router.get("/me", response_model=UserResponse) async def get_current_user_info( current_user: User = Depends(get_current_user), ): """Get the current authenticated user's info.""" return UserResponse.model_validate(current_user)