"""Authentication endpoints.""" from datetime import datetime, timedelta from typing import Annotated from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status from jose import JWTError, jwt from sqlalchemy.orm import Session from app.core.config import settings from app.core.deps import get_db from app.core.security import create_access_token, get_password_hash, verify_password from app.models.user import User from app.schemas.auth import EmailVerificationRequest, LoginRequest, UserRegister from app.schemas.user import Token from app.services.email_service import send_email router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/login", response_model=Token) def login( login_data: LoginRequest, db: Annotated[Session, Depends(get_db)], ) -> Token: """ Login with email and password. Returns JWT token for authenticated requests. """ user = db.query(User).filter(User.email == login_data.email).first() if not user or not verify_password(login_data.password, str(user.hashed_password)): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled", ) access_token = create_access_token(subject=int(user.id)) return Token(access_token=access_token, token_type="bearer") @router.post("/register", status_code=201) async def register( data: UserRegister, background_tasks: BackgroundTasks, db: Annotated[Session, Depends(get_db)], ) -> dict: """ Register new user with email verification. Creates an inactive user account and sends verification email. """ # Check if email already exists existing = db.query(User).filter(User.email == data.email).first() if existing: raise HTTPException(status_code=400, detail="Email already registered") # Create user (inactive until verified) user = User( email=data.email, hashed_password=get_password_hash(data.password), full_name=data.full_name, organization=data.organization, role="user", # Default role is_active=False, # Inactive until email verified ) db.add(user) db.commit() db.refresh(user) # Generate verification token (JWT, expires in 24h) verification_token = jwt.encode( { "sub": str(user.id), "type": "email_verification", "exp": datetime.utcnow() + timedelta(hours=24), }, settings.secret_key, algorithm="HS256", ) # Send verification email (background task) verification_link = f"{settings.frontend_url}/verify?token={verification_token}" subject = "Verifică contul tău - Space Booking" body = f"""Bună ziua {user.full_name}, Bine ai venit pe platforma Space Booking! Pentru a-ți activa contul, te rugăm să accesezi link-ul de mai jos: {verification_link} Link-ul va expira în 24 de ore. Dacă nu ai creat acest cont, te rugăm să ignori acest email. Cu stimă, Echipa Space Booking """ background_tasks.add_task(send_email, user.email, subject, body) return { "message": "Registration successful. Please check your email to verify your account.", "email": user.email, } @router.post("/verify") def verify_email( data: EmailVerificationRequest, db: Annotated[Session, Depends(get_db)], ) -> dict: """ Verify email address with token. Activates the user account if token is valid. """ try: # Decode token payload = jwt.decode( data.token, settings.secret_key, algorithms=["HS256"], ) # Check token type if payload.get("type") != "email_verification": raise HTTPException(status_code=400, detail="Invalid verification token") user_id = int(payload.get("sub")) # Get user user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") # Check if already verified if user.is_active: return {"message": "Email already verified"} # Activate user user.is_active = True db.commit() return {"message": "Email verified successfully. You can now log in."} except jwt.ExpiredSignatureError: raise HTTPException( status_code=400, detail="Verification link expired. Please request a new one.", ) except JWTError: raise HTTPException(status_code=400, detail="Invalid verification token") @router.post("/resend-verification") async def resend_verification( email: str, background_tasks: BackgroundTasks, db: Annotated[Session, Depends(get_db)], ) -> dict: """ Resend verification email. For security, always returns success even if email doesn't exist. """ user = db.query(User).filter(User.email == email).first() if not user: # Don't reveal if email exists return {"message": "If the email exists, a verification link has been sent."} if user.is_active: raise HTTPException(status_code=400, detail="Account already verified") # Generate new token verification_token = jwt.encode( { "sub": str(user.id), "type": "email_verification", "exp": datetime.utcnow() + timedelta(hours=24), }, settings.secret_key, algorithm="HS256", ) # Send email verification_link = f"{settings.frontend_url}/verify?token={verification_token}" subject = "Verifică contul tău - Space Booking" body = f"""Bună ziua {user.full_name}, Ai solicitat un nou link de verificare pentru contul tău pe Space Booking. Pentru a-ți activa contul, te rugăm să accesezi link-ul de mai jos: {verification_link} Link-ul va expira în 24 de ore. Cu stimă, Echipa Space Booking """ background_tasks.add_task(send_email, user.email, subject, body) return {"message": "If the email exists, a verification link has been sent."}