import asyncpg from supabase import create_client, Client from app.config import get_settings from app.exceptions import AppError, UnauthorizedError from app.models.auth import RegisterRequest, LoginRequest, RefreshRequest def _client() -> Client: s = get_settings() return create_client(s.SUPABASE_URL, s.SUPABASE_ANON_KEY) def _admin_client() -> Client: s = get_settings() return create_client(s.SUPABASE_URL, s.SUPABASE_SERVICE_ROLE_KEY) async def register(req: RegisterRequest, db: asyncpg.Connection) -> dict: try: result = _client().auth.sign_up({"email": req.email, "password": req.password}) except Exception as e: raise AppError("REGISTRATION_FAILED", str(e), 400) if not result.user: raise AppError("REGISTRATION_FAILED", "Could not create account", 400) user_id = str(result.user.id) await db.execute( """ INSERT INTO profiles (id, email, full_name, phone, role) VALUES ($1, $2, $3, $4, 'client') ON CONFLICT (id) DO NOTHING """, user_id, req.email, req.resolved_name(), req.phone, ) if result.session: return { "access_token": result.session.access_token, "refresh_token": result.session.refresh_token, "token_type": "bearer", "expires_in": result.session.expires_in, } return {"message": "Account created successfully."} async def login(req: LoginRequest) -> dict: try: result = _client().auth.sign_in_with_password({"email": req.email, "password": req.password}) except Exception: raise UnauthorizedError("Invalid email or password") if not result.session: raise UnauthorizedError("Invalid email or password") return { "access_token": result.session.access_token, "refresh_token": result.session.refresh_token, "token_type": "bearer", "expires_in": result.session.expires_in, } async def refresh(req: RefreshRequest) -> dict: try: result = _client().auth.refresh_session(req.refresh_token) except Exception: raise UnauthorizedError("Invalid or expired refresh token") return { "access_token": result.session.access_token, "refresh_token": result.session.refresh_token, "token_type": "bearer", "expires_in": result.session.expires_in, } async def forgot_password(email: str): try: _client().auth.reset_password_email(email) except Exception: pass # Always return success to avoid email enumeration async def update_profile(user_id: str, db: asyncpg.Connection, full_name: str | None, phone: str | None) -> dict: updates = {} if full_name is not None: updates["full_name"] = full_name if phone is not None: updates["phone"] = phone if not updates: row = await db.fetchrow("SELECT * FROM profiles WHERE id = $1", user_id) return dict(row) set_clauses = [f"{k} = ${i + 2}" for i, k in enumerate(updates)] row = await db.fetchrow( f"UPDATE profiles SET {', '.join(set_clauses)}, updated_at = now() WHERE id = $1 RETURNING *", user_id, *updates.values(), ) return dict(row) async def create_admin_user(email: str, password: str, full_name: str, db: asyncpg.Connection) -> dict: try: result = _admin_client().auth.admin.create_user({ "email": email, "password": password, "email_confirm": True, }) except Exception as e: raise AppError("USER_CREATE_FAILED", str(e), 400) user_id = str(result.user.id) await db.execute( """ INSERT INTO profiles (id, email, full_name, role) VALUES ($1, $2, $3, 'admin') ON CONFLICT (id) DO UPDATE SET role = 'admin' """, user_id, email, full_name, ) row = await db.fetchrow("SELECT * FROM profiles WHERE id = $1", user_id) return dict(row)