Phase O Task 46 needs internal validation of the full self-serve flow against the prod backend before flipping SELF_SERVE_ENABLED public. This adds the per-email allowlist that bypasses the global flag for specific authenticated users. - INTERNAL_TESTER_EMAILS: comma-separated list, parsed by a Pydantic field_validator into a normalized lowercase list. Settings.is_internal_tester and Settings.is_self_serve_active_for centralize the allowlist + global-flag check; both endpoints below call the latter. - New get_current_user_optional dep — best-effort auth that returns None on missing/invalid token instead of 401. Used by /config/public so the same endpoint serves anonymous public callers and authenticated allowlist members. - /config/public now accepts optional auth and returns self_serve_enabled=True for authenticated allowlist members even when the global flag is off. Anonymous callers always see the global flag. - /auth/register replaces the SELF_SERVE_ENABLED check with the helper so a registering email on the allowlist can join without an invite code. Non-allowlist emails still 400 when self-serve is off. - docker-compose.dev.yml passes SELF_SERVE_ENABLED + INTERNAL_TESTER_EMAILS through; backend/.env.example documents both. Tests cover: allowlisted authenticated user sees true, non-allowlisted authenticated user sees the global flag, anonymous calls ignore the allowlist, allowlisted email registers without invite code, non-allowlisted email still blocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
1.8 KiB
Python
51 lines
1.8 KiB
Python
"""Public runtime configuration endpoint.
|
|
|
|
GET /api/v1/config/public
|
|
Returns the small set of runtime flags the frontend needs at app load
|
|
to decide whether to render the self-serve signup flow and which OAuth
|
|
buttons to show. No authentication required.
|
|
|
|
The response model lives in `app.schemas.config` so it can be reused by
|
|
frontend codegen and other call sites if needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated, Optional
|
|
|
|
from fastapi import APIRouter, Depends
|
|
|
|
from app.api.deps import get_current_user_optional
|
|
from app.core.config import settings
|
|
from app.models.user import User
|
|
from app.schemas.config import PublicConfigResponse
|
|
|
|
router = APIRouter(prefix="/config", tags=["config"])
|
|
|
|
|
|
@router.get("/public", response_model=PublicConfigResponse)
|
|
async def get_public_config(
|
|
current_user: Annotated[Optional[User], Depends(get_current_user_optional)],
|
|
) -> PublicConfigResponse:
|
|
"""Return public-safe runtime config.
|
|
|
|
`oauth_providers` reflects which OAuth client IDs are configured server
|
|
side; the frontend uses it to render only buttons that will actually
|
|
succeed. `self_serve_enabled` is the master switch for the new public
|
|
self-serve signup flow; an authenticated caller whose email is on the
|
|
INTERNAL_TESTER_EMAILS allowlist sees `True` even when the global flag
|
|
is off, so internal validation in prod test mode can exercise the full
|
|
surface before the public flip.
|
|
"""
|
|
providers: list[str] = []
|
|
if settings.GOOGLE_CLIENT_ID:
|
|
providers.append("google")
|
|
if settings.MS_CLIENT_ID:
|
|
providers.append("microsoft")
|
|
|
|
user_email = current_user.email if current_user else None
|
|
return PublicConfigResponse(
|
|
self_serve_enabled=settings.is_self_serve_active_for(user_email),
|
|
oauth_providers=providers,
|
|
)
|