Track B — Auth & User System (complete): - User registration with bcrypt + Turnstile verification - JWT access/refresh token flow with httpOnly cookie rotation - Redis refresh token blocklist for logout - User profile + settings update endpoints (username, email) - API key generation with bcrypt hashing (ff_key_ prefix) - BYOK key management with AES-256-GCM encryption at rest - Free tier rate limiting (5 shaders/month) - Tier-gated endpoints (Pro/Studio for BYOK, API keys, bounty posting) Track C — Shader Submission & Renderer (complete): - GLSL validator: entry point check, banned extensions, infinite loop detection, brace balancing, loop bound warnings, code length limits - Puppeteer/headless Chromium renderer with Shadertoy-compatible uniform injection (iTime, iResolution, iMouse), WebGL2 with SwiftShader fallback - Shader compilation error detection via page title signaling - Thumbnail capture at t=1s, preview frame at t=duration - Renderer client service for API→renderer HTTP communication - Shader submission pipeline: validate GLSL → create record → enqueue render job - Desire fulfillment linking on shader submit - Re-validation and re-render on shader code update - Fork endpoint copies code, tags, metadata, enqueues new render Track D — Frontend Shell (complete): - React 18 + Vite + TypeScript + Tailwind CSS + TanStack Query + Zustand - Dark theme with custom fracta color palette and surface tones - Responsive layout with sticky navbar, gradient branding - Auth: Login + Register pages with JWT token management - API client with automatic 401 refresh interceptor - ShaderCanvas: Full WebGL2 renderer component with Shadertoy uniforms, mouse tracking, ResizeObserver, debounced recompilation, error callbacks - GLSL Editor: Split pane (code textarea + live preview), 400ms debounced preview, metadata panel (description, tags, type), GLSL validation errors, shader publish flow, fork-from-existing support - Feed: Infinite scroll with IntersectionObserver sentinel, dwell time tracking, skeleton loading states, empty state with CTA - Explore: Search + tag filter + sort tabs (trending/new/top), grid layout - ShaderDetail: Full-screen preview, vote controls, view source toggle, fork button - Bounties: Desire queue list sorted by heat score, status badges, tip display - BountyDetail: Single desire view with style hints, fulfill CTA - Profile: User header with avatar initial, shader grid - Settings: Account info, API key management (create/revoke/copy), subscription tiers - Generate: AI generation UI stub with prompt input, style controls, example prompts 76 files, ~5,700 lines of application code.
114 lines
4 KiB
Python
114 lines
4 KiB
Python
"""Users & Settings router."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from app.database import get_db
|
|
from app.models import User
|
|
from app.schemas import UserPublic, UserMe, UserUpdate, ByokKeysUpdate
|
|
from app.middleware.auth import get_current_user, require_tier
|
|
from app.services.byok import encrypt_key, get_stored_providers
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/users/{username}", response_model=UserPublic)
|
|
async def get_user_profile(username: str, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(User).where(User.username == username))
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return user
|
|
|
|
|
|
@router.get("/me", response_model=UserMe)
|
|
async def get_me(user: User = Depends(get_current_user)):
|
|
return user
|
|
|
|
|
|
@router.put("/me", response_model=UserMe)
|
|
async def update_me(
|
|
body: UserUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
updates = body.model_dump(exclude_unset=True)
|
|
if not updates:
|
|
return user
|
|
|
|
# Check uniqueness for username/email changes
|
|
if "username" in updates and updates["username"] != user.username:
|
|
existing = await db.execute(
|
|
select(User).where(User.username == updates["username"])
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=409, detail="Username already taken")
|
|
|
|
if "email" in updates and updates["email"] != user.email:
|
|
existing = await db.execute(
|
|
select(User).where(User.email == updates["email"])
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=409, detail="Email already taken")
|
|
|
|
for field, value in updates.items():
|
|
setattr(user, field, value)
|
|
|
|
return user
|
|
|
|
|
|
@router.put("/me/ai-keys")
|
|
async def update_byok_keys(
|
|
body: ByokKeysUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_tier("pro", "studio")),
|
|
):
|
|
"""Store encrypted BYOK API keys for AI providers."""
|
|
from app.services.byok import save_user_keys
|
|
|
|
await save_user_keys(db, user, body)
|
|
providers = await get_stored_providers(db, user)
|
|
return {"status": "ok", "configured_providers": providers}
|
|
|
|
|
|
@router.get("/me/ai-keys")
|
|
async def get_byok_keys(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_tier("pro", "studio")),
|
|
):
|
|
"""List which providers have BYOK keys configured (never returns actual keys)."""
|
|
providers = await get_stored_providers(db, user)
|
|
return {"configured_providers": providers}
|
|
|
|
|
|
# ── Creator Economy Stubs (501) ─────────────────────────────
|
|
|
|
@router.get("/dashboard")
|
|
async def creator_dashboard(user: User = Depends(get_current_user)):
|
|
raise HTTPException(status_code=501, detail="Creator dashboard coming in future release")
|
|
|
|
|
|
@router.get("/shaders/{shader_id}/unlock-status")
|
|
async def unlock_status(shader_id: str, user: User = Depends(get_current_user)):
|
|
raise HTTPException(status_code=501, detail="Source unlock coming in future release")
|
|
|
|
|
|
@router.post("/shaders/{shader_id}/unlock")
|
|
async def unlock_source(shader_id: str, user: User = Depends(get_current_user)):
|
|
raise HTTPException(status_code=501, detail="Source unlock coming in future release")
|
|
|
|
|
|
@router.post("/shaders/{shader_id}/commercial")
|
|
async def purchase_commercial(shader_id: str, user: User = Depends(get_current_user)):
|
|
raise HTTPException(status_code=501, detail="Commercial licensing coming in future release")
|
|
|
|
|
|
@router.post("/me/creator/apply")
|
|
async def apply_verified(user: User = Depends(get_current_user)):
|
|
raise HTTPException(status_code=501, detail="Verified creator program coming in future release")
|
|
|
|
|
|
@router.get("/me/creator/earnings")
|
|
async def creator_earnings(user: User = Depends(get_current_user)):
|
|
raise HTTPException(status_code=501, detail="Creator earnings coming in future release")
|