"""Auth routes: register, login, and current user profile.""" from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth import create_access_token, get_current_user, hash_password, verify_password from app.dependencies import get_db from app.models.saas_models import Organization, OrganizationMember, StatusPage, User router = APIRouter(tags=["auth"]) # ── Request / Response schemas ────────────────────────────────────────────── class RegisterRequest(BaseModel): email: EmailStr password: str class LoginRequest(BaseModel): email: EmailStr password: str class AuthResponse(BaseModel): access_token: str token_type: str = "bearer" class UserProfile(BaseModel): id: str email: str display_name: str | None = None is_email_verified: bool created_at: str | None = None # ── Routes ─────────────────────────────────────────────────────────────────── @router.post("/auth/register", status_code=status.HTTP_201_CREATED, response_model=AuthResponse) async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)): """Register a new user, create a default Organization + StatusPage, return JWT.""" # Check for existing user result = await db.execute(select(User).where(User.email == body.email)) if result.scalar_one_or_none() is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="A user with this email already exists", ) # Create user user = User( email=body.email, password_hash=hash_password(body.password), ) db.add(user) await db.flush() # assign user.id # Create default organization org_slug = body.email.split("@")[0].lower() org = Organization( name=org_slug, slug=org_slug, ) db.add(org) await db.flush() # assign org.id # Create organization membership (owner) membership = OrganizationMember( organization_id=org.id, user_id=user.id, role="owner", ) db.add(membership) # Create default status page status_page = StatusPage( organization_id=org.id, slug="main", title="Status Page", ) db.add(status_page) # Commit all together (the get_db dependency also commits, but flush ensures # relationships are consistent before we return) await db.flush() # Generate JWT token = create_access_token(user.id) return AuthResponse(access_token=token) @router.post("/auth/login", response_model=AuthResponse) async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)): """Authenticate user with email + password, return JWT.""" result = await db.execute(select(User).where(User.email == body.email)) user = result.scalar_one_or_none() if user is None or not verify_password(body.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password", ) token = create_access_token(user.id) return AuthResponse(access_token=token) @router.get("/auth/me", response_model=UserProfile) async def me(current_user: User = Depends(get_current_user)): """Return the current authenticated user's profile.""" return UserProfile( id=current_user.id, email=current_user.email, display_name=current_user.display_name, is_email_verified=current_user.is_email_verified, created_at=current_user.created_at.isoformat() if current_user.created_at else None, )