from fastapi import APIRouter, HTTPException, status, Depends from app.models import UserSignup, UserLogin, UserResponse, TokenResponse from app.database import get_supabase from app.dependencies import get_current_user from app.config import settings from pydantic import BaseModel, EmailStr, Field from typing import Optional import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/auth", tags=["Authentication"]) class ForgotPasswordRequest(BaseModel): email: EmailStr class ResetPasswordRequest(BaseModel): access_token: str new_password: str = Field(min_length=8) class ProfileUpdate(BaseModel): company_name: Optional[str] = None current_password: Optional[str] = None new_password: Optional[str] = Field(default=None, min_length=8) language: Optional[str] = None @router.post("/signup", response_model=TokenResponse) async def signup(data: UserSignup): supabase = get_supabase() user_id = None try: # Create auth user auth_resp = supabase.auth.sign_up( {"email": data.email, "password": data.password} ) if not auth_resp.user: raise HTTPException(status_code=400, detail="Failed to create account") user = auth_resp.user user_id = user.id # Create company record supabase.table("companies").insert( { "owner_id": user.id, "name": data.company_name, } ).execute() # Create free subscription supabase.table("subscriptions").insert( { "user_id": user.id, "plan": "free", "status": "active", } ).execute() # Safety-net: ensure user_profiles row exists (trigger should handle it, but just in case) try: supabase.table("user_profiles").insert({"user_id": user.id}).execute() except Exception: pass # Row may already exist from trigger token = auth_resp.session.access_token if auth_resp.session else "" return TokenResponse( access_token=token, user=UserResponse( id=user.id, email=user.email, company_name=data.company_name, plan="free", is_admin=False, ), ) except HTTPException: raise except Exception as e: logger.error(f"Signup error: {e}") # Rollback auth user if company/subscription creation failed if user_id and "already registered" not in str(e).lower(): try: supabase.auth.admin.delete_user(user_id) except Exception as rb_err: logger.error(f"Signup rollback failed: {rb_err}") if "already registered" in str(e).lower() or "already exists" in str(e).lower(): raise HTTPException(status_code=400, detail="Email already registered") raise HTTPException(status_code=400, detail=str(e)) @router.post("/login", response_model=TokenResponse) async def login(data: UserLogin): supabase = get_supabase() try: auth_resp = supabase.auth.sign_in_with_password( {"email": data.email, "password": data.password} ) if not auth_resp.user or not auth_resp.session: raise HTTPException(status_code=401, detail="Invalid credentials") user = auth_resp.user # Get company info company = supabase.table("companies").select("name").eq("owner_id", user.id).execute() company_name = company.data[0]["name"] if company.data else "" # Get subscription sub = ( supabase.table("subscriptions") .select("plan") .eq("user_id", user.id) .eq("status", "active") .execute() ) plan = sub.data[0]["plan"] if sub.data else "free" # Get is_admin and language from profile try: profile = supabase.table("user_profiles").select("is_admin, language").eq("user_id", user.id).execute() is_admin = profile.data[0].get("is_admin", False) if profile.data else False language = profile.data[0].get("language", "fr") if profile.data else "fr" except Exception: is_admin = False language = "fr" return TokenResponse( access_token=auth_resp.session.access_token, user=UserResponse( id=user.id, email=user.email, company_name=company_name, plan=plan, is_admin=is_admin, language=language, ), ) except HTTPException: raise except Exception as e: logger.error(f"Login error: {e}") raise HTTPException(status_code=401, detail="Invalid credentials") @router.post("/forgot-password") async def forgot_password(data: ForgotPasswordRequest): """Send password reset email via Supabase. Always returns success to prevent email enumeration.""" supabase = get_supabase() try: supabase.auth.reset_password_for_email( data.email, options={"redirect_to": f"{settings.app_url}/reset-password"}, ) except Exception as e: logger.warning(f"Forgot password request error (suppressed): {e}") return {"message": "If that email is registered, a password reset link has been sent."} @router.post("/reset-password") async def reset_password(data: ResetPasswordRequest): """Reset user password using the recovery token from the reset email.""" supabase = get_supabase() try: user_response = supabase.auth.get_user(data.access_token) if not user_response.user: raise HTTPException(status_code=400, detail="Invalid or expired reset token") supabase.auth.admin.update_user_by_id( user_response.user.id, {"password": data.new_password}, ) return {"message": "Password updated successfully"} except HTTPException: raise except Exception as e: logger.error(f"Password reset error: {e}") raise HTTPException(status_code=400, detail="Invalid or expired reset token") @router.patch("/profile", response_model=UserResponse) async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)): """Update company name and/or password.""" supabase = get_supabase() if data.company_name: supabase.table("companies").update({"name": data.company_name}).eq("owner_id", user.id).execute() if data.new_password: if not data.current_password: raise HTTPException(status_code=400, detail="Current password required to change password") try: supabase.auth.sign_in_with_password({"email": user.email, "password": data.current_password}) except Exception: raise HTTPException(status_code=400, detail="Current password is incorrect") supabase.auth.admin.update_user_by_id(user.id, {"password": data.new_password}) if data.language: supabase.table("user_profiles").update({"language": data.language}).eq("user_id", user.id).execute() company = supabase.table("companies").select("name").eq("owner_id", user.id).execute() company_name = company.data[0]["name"] if company.data else "" sub = supabase.table("subscriptions").select("plan").eq("user_id", user.id).eq("status", "active").execute() plan = sub.data[0]["plan"] if sub.data else "free" try: profile = supabase.table("user_profiles").select("is_admin, language").eq("user_id", user.id).execute() is_admin = profile.data[0].get("is_admin", False) if profile.data else False language = profile.data[0].get("language", "fr") if profile.data else "fr" except Exception: is_admin = False language = data.language or "fr" return UserResponse(id=user.id, email=user.email, company_name=company_name, plan=plan, is_admin=is_admin, language=language) @router.delete("/account") async def delete_account(user=Depends(get_current_user)): """Permanently delete account, company, chatbots, and all data.""" supabase = get_supabase() company = supabase.table("companies").select("id").eq("owner_id", user.id).execute() if company.data: supabase.table("companies").delete().eq("id", company.data[0]["id"]).execute() supabase.table("subscriptions").delete().eq("user_id", user.id).execute() try: supabase.auth.admin.delete_user(user.id) except Exception as e: logger.error(f"Failed to delete auth user {user.id}: {e}") raise HTTPException(status_code=500, detail="Failed to delete account") return {"message": "Account deleted successfully"} @router.post("/logout") async def logout(user=Depends(get_current_user)): supabase = get_supabase() try: supabase.auth.sign_out() except Exception: pass return {"message": "Logged out successfully"} @router.get("/me", response_model=UserResponse) async def get_me(user=Depends(get_current_user)): supabase = get_supabase() company = supabase.table("companies").select("name").eq("owner_id", user.id).execute() company_name = company.data[0]["name"] if company.data else "" sub = ( supabase.table("subscriptions") .select("plan") .eq("user_id", user.id) .eq("status", "active") .execute() ) plan = sub.data[0]["plan"] if sub.data else "free" try: profile = supabase.table("user_profiles").select("is_admin, language").eq("user_id", user.id).execute() is_admin = profile.data[0].get("is_admin", False) if profile.data else False language = profile.data[0].get("language", "fr") if profile.data else "fr" except Exception: is_admin = False language = "fr" return UserResponse( id=user.id, email=user.email, company_name=company_name, plan=plan, is_admin=is_admin, language=language, )