122 lines
No EOL
3.9 KiB
Python
122 lines
No EOL
3.9 KiB
Python
"""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,
|
|
) |