mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-13 08:30:07 +00:00
- Add new routers: admin, appointments, campaigns - Add storage service and logging config - Add migrations directory and test suite with pytest config - Add supabase_migration_features.sql - Update models, dependencies, config, and existing routers - Remove whatsapp_service (deleted) - Update pyproject.toml and uv.lock dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
261 lines
9.0 KiB
Python
261 lines
9.0 KiB
Python
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)
|
|
|
|
|
|
@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 flag
|
|
try:
|
|
profile = supabase.table("user_profiles").select("is_admin").eq("user_id", user.id).execute()
|
|
is_admin = profile.data[0].get("is_admin", False) if profile.data else False
|
|
except Exception:
|
|
is_admin = False
|
|
|
|
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,
|
|
),
|
|
)
|
|
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})
|
|
|
|
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"
|
|
|
|
return UserResponse(id=user.id, email=user.email, company_name=company_name, plan=plan)
|
|
|
|
|
|
@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").eq("user_id", user.id).execute()
|
|
is_admin = profile.data[0].get("is_admin", False) if profile.data else False
|
|
except Exception:
|
|
is_admin = False
|
|
|
|
return UserResponse(
|
|
id=user.id,
|
|
email=user.email,
|
|
company_name=company_name,
|
|
plan=plan,
|
|
is_admin=is_admin,
|
|
)
|