Spaces:
Sleeping
Sleeping
| """ | |
| Auth endpoints — signup, login, Google SSO, me. | |
| Mirrored from NourishBot/routes/auth.py; adapted to CodyBuddy conventions. | |
| """ | |
| import os | |
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from pydantic import BaseModel, EmailStr | |
| from sqlalchemy.orm import Session | |
| from auth.dependencies import get_current_user | |
| from auth.roles import SUPER_ADMIN_EMAILS, ensure_role as _ensure_role, resolve_role as _resolve_role | |
| from auth.utils import create_access_token, hash_password, verify_password | |
| from db.database import get_db | |
| from db.models import User | |
| router = APIRouter(prefix="/auth", tags=["auth"]) | |
| # Role logic (allowlist) now lives in auth.roles so get_current_user can apply it | |
| # on every authenticated request — re-exported here for the auth endpoints. | |
| __all__ = ["router", "SUPER_ADMIN_EMAILS"] | |
| class SignupRequest(BaseModel): | |
| email: EmailStr | |
| password: str | |
| name: str | None = None | |
| class LoginRequest(BaseModel): | |
| email: EmailStr | |
| password: str | |
| class GoogleAuthRequest(BaseModel): | |
| id_token: str | |
| class GitHubStatus(BaseModel): | |
| connected: bool = False | |
| login: str | None = None | |
| def _github_status(user: User) -> GitHubStatus: | |
| """Derive GitHub connection status defensively. NEVER returns the token. | |
| ponytail: guard against MagicMock/non-str attrs (test fixtures) — only a real, | |
| non-empty encrypted string counts as connected. | |
| """ | |
| enc = getattr(user, "github_token_enc", None) | |
| login = getattr(user, "github_login", None) | |
| connected = isinstance(enc, str) and bool(enc) | |
| return GitHubStatus(connected=connected, login=login if (connected and isinstance(login, str)) else None) | |
| class UserResponse(BaseModel): | |
| id: str | |
| email: str | |
| name: str | None | |
| role: str = "user" | |
| github: GitHubStatus = GitHubStatus() | |
| model_config = {"from_attributes": True} | |
| class AuthResponse(BaseModel): | |
| token: str | |
| user: UserResponse | |
| def signup(payload: SignupRequest, db: Session = Depends(get_db)) -> AuthResponse: | |
| if len(payload.password) < 8: | |
| raise HTTPException(status_code=400, detail="Password must be at least 8 characters") | |
| if db.query(User).filter(User.email == payload.email).first(): | |
| raise HTTPException(status_code=409, detail="Email already registered") | |
| user = User( | |
| email=payload.email, | |
| name=payload.name or payload.email.split("@")[0], | |
| hashed_password=hash_password(payload.password), | |
| ) | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| role = _ensure_role(user, db) | |
| token = create_access_token(str(user.id), user.email) | |
| return AuthResponse(token=token, user=UserResponse(id=str(user.id), email=user.email, name=user.name, role=role, github=_github_status(user))) | |
| def login(payload: LoginRequest, db: Session = Depends(get_db)) -> AuthResponse: | |
| user = db.query(User).filter(User.email == payload.email).first() | |
| if not user or not user.hashed_password: | |
| raise HTTPException(status_code=401, detail="Invalid credentials") | |
| if not verify_password(payload.password, user.hashed_password): | |
| raise HTTPException(status_code=401, detail="Invalid credentials") | |
| role = _ensure_role(user, db) | |
| token = create_access_token(str(user.id), user.email) | |
| return AuthResponse(token=token, user=UserResponse(id=str(user.id), email=user.email, name=user.name, role=role, github=_github_status(user))) | |
| def google_auth(payload: GoogleAuthRequest, db: Session = Depends(get_db)) -> AuthResponse: | |
| google_client_id = os.environ.get("GOOGLE_CLIENT_ID", "") | |
| if not google_client_id: | |
| raise HTTPException(status_code=503, detail="Google sign-in not configured") | |
| from google.oauth2 import id_token as google_id_token | |
| from google.auth.transport import requests as google_requests | |
| try: | |
| idinfo = google_id_token.verify_oauth2_token( | |
| payload.id_token, | |
| google_requests.Request(), | |
| google_client_id, | |
| ) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=401, detail=f"Invalid Google token: {exc}") | |
| google_id = idinfo["sub"] | |
| email = idinfo["email"] | |
| name = idinfo.get("name", email.split("@")[0]) | |
| # ponytail: upsert by google_id first, then email, then create | |
| user = db.query(User).filter(User.google_id == google_id).first() | |
| if not user: | |
| user = db.query(User).filter(User.email == email).first() | |
| if user: | |
| user.google_id = google_id | |
| else: | |
| user = User(email=email, name=name, google_id=google_id) | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| role = _ensure_role(user, db) | |
| token = create_access_token(str(user.id), user.email) | |
| return AuthResponse(token=token, user=UserResponse(id=str(user.id), email=user.email, name=user.name, role=role, github=_github_status(user))) | |
| def me(current_user: User = Depends(get_current_user)) -> UserResponse: | |
| return UserResponse( | |
| id=str(current_user.id), email=current_user.email, | |
| name=current_user.name, role=_resolve_role(current_user.email), | |
| github=_github_status(current_user), | |
| ) | |