from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from app.auth import ( authenticate_user, create_access_token, get_current_user, get_password_hash, get_user_by_email, get_user_by_username, ) from app.database import get_db from app.models.user import User, UserRole from app.models.verification_token import TokenType from app.schemas import ( ActivateAccountRequest, ForgotPasswordRequest, MessageResponse, RegisterResponse, ResetPasswordRequest, Token, UserCreate, UserResponse, ) from app.services.email_service import send_activation_email, send_password_reset_email from app.services.verification import create_verification_token, verify_code router = APIRouter(prefix="/api/auth", tags=["auth"]) @router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED) def register(user_data: UserCreate, db: Annotated[Session, Depends(get_db)]): if user_data.password != user_data.confirm_password: raise HTTPException(status_code=400, detail="Passwords do not match") if get_user_by_email(db, user_data.email): raise HTTPException(status_code=400, detail="Email already registered") if get_user_by_username(db, user_data.username): raise HTTPException(status_code=400, detail="Username already taken") user = User( email=user_data.email, username=user_data.username, hashed_password=get_password_hash(user_data.password), role=UserRole.user, is_active=False, ) db.add(user) db.flush() token = create_verification_token(db, user, TokenType.activation) db.commit() send_activation_email(user.email, user.username, token.code) return RegisterResponse( message="Registration successful. Check your email for the activation code.", email=user.email, ) @router.post("/activate", response_model=MessageResponse) def activate_account( data: ActivateAccountRequest, db: Annotated[Session, Depends(get_db)], ): user = verify_code(db, data.email, data.code, TokenType.activation) if not user: raise HTTPException(status_code=400, detail="Invalid or expired activation code") user.is_active = True db.commit() return MessageResponse(message="Account activated. You can now log in.") @router.post("/resend-activation", response_model=MessageResponse) def resend_activation( data: ForgotPasswordRequest, db: Annotated[Session, Depends(get_db)], ): user = get_user_by_email(db, data.email) if not user: return MessageResponse( message="If the email exists, a new activation code has been sent." ) if user.is_active: raise HTTPException(status_code=400, detail="Account is already activated") token = create_verification_token(db, user, TokenType.activation) db.commit() send_activation_email(user.email, user.username, token.code) return MessageResponse( message="If the email exists, a new activation code has been sent." ) @router.post("/forgot-password", response_model=MessageResponse) def forgot_password( data: ForgotPasswordRequest, db: Annotated[Session, Depends(get_db)], ): user = get_user_by_email(db, data.email) if user and user.is_active: token = create_verification_token(db, user, TokenType.password_reset) db.commit() send_password_reset_email(user.email, user.username, token.code) return MessageResponse( message="If the email exists, a password reset code has been sent." ) @router.post("/reset-password", response_model=MessageResponse) def reset_password( data: ResetPasswordRequest, db: Annotated[Session, Depends(get_db)], ): if data.password != data.confirm_password: raise HTTPException(status_code=400, detail="Passwords do not match") user = verify_code(db, data.email, data.code, TokenType.password_reset) if not user: raise HTTPException(status_code=400, detail="Invalid or expired reset code") user.hashed_password = get_password_hash(data.password) db.commit() return MessageResponse(message="Password updated. You can now log in.") @router.post("/login", response_model=Token) def login( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Annotated[Session, Depends(get_db)], ): user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) if not user.is_active: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account not activated. Check your email for the activation code.", ) token = create_access_token(data={"sub": user.username}) return Token(access_token=token) @router.get("/me", response_model=UserResponse) def get_me(current_user: Annotated[User, Depends(get_current_user)]): return current_user