"""Authentication endpoints.""" from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.core.database import get_db from app.core.security import verify_password, get_password_hash, create_access_token, create_refresh_token, decode_token from app.models.user import User from app.models.organization import Organization, OrganizationMember, MemberRole from app.schemas.user import UserCreate, UserRead, Token from app.services.audit import AuditService import re router = APIRouter() def slugify(text: str) -> str: """Convert text to URL-friendly slug.""" text = text.lower() text = re.sub(r'[^\w\s-]', '', text) text = re.sub(r'[-\s]+', '-', text) return text.strip('-') @router.post("/register", response_model=Token) async def register( user_in: UserCreate, request: Request, db: AsyncSession = Depends(get_db) ): """Register a new user.""" # Check if email exists result = await db.execute(select(User).where(User.email == user_in.email)) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) # Create user user = User( email=user_in.email, hashed_password=get_password_hash(user_in.password), full_name=user_in.full_name ) db.add(user) await db.flush() # Create default organization for user org_name = user_in.full_name or user_in.email.split('@')[0] org_slug = slugify(org_name) + f"-{user.id}" organization = Organization( name=f"{org_name}'s Organization", slug=org_slug ) db.add(organization) await db.flush() # Add user as organization owner membership = OrganizationMember( organization_id=organization.id, user_id=user.id, role=MemberRole.OWNER ) db.add(membership) await db.flush() # Audit log await AuditService.log( db, action="user.register", user_id=user.id, resource_type="user", resource_id=user.id, ip_address=request.client.host if request.client else None ) # Return tokens access_token = create_access_token({"sub": str(user.id), "email": user.email}) refresh_token = create_refresh_token({"sub": str(user.id), "email": user.email}) return Token( access_token=access_token, refresh_token=refresh_token, token_type="bearer" ) @router.post("/login", response_model=Token) async def login( email: str, password: str, request: Request, db: AsyncSession = Depends(get_db) ): """Login and get access token.""" result = await db.execute(select(User).where(User.email == email)) user = result.scalar_one_or_none() if not user or not verify_password(password, user.hashed_password): 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="User is inactive" ) # Update last login user.last_login = datetime.utcnow() # Audit log await AuditService.log( db, action="user.login", user_id=user.id, resource_type="user", resource_id=user.id, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent") ) # Create tokens token_data = {"user_id": user.id, "email": user.email} access_token = create_access_token(token_data) refresh_token = create_refresh_token(token_data) return Token(access_token=access_token, refresh_token=refresh_token) @router.post("/refresh", response_model=Token) async def refresh_token( refresh_token: str, db: AsyncSession = Depends(get_db) ): """Refresh access token.""" payload = decode_token(refresh_token) if not payload or payload.get("type") != "refresh": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" ) user_id = payload.get("user_id") result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive" ) token_data = {"user_id": user.id, "email": user.email} new_access_token = create_access_token(token_data) new_refresh_token = create_refresh_token(token_data) return Token(access_token=new_access_token, refresh_token=new_refresh_token)