Full-stack app that turns HP customer briefs (master asset + regional supporting docs) into a set of branded Word deliverables via a RAG + agent pipeline. Stack - FastAPI + SQLAlchemy + pgvector + RQ (backend, Python 3.12) - React + Vite + TypeScript + Tailwind + TanStack Query (frontend) - Claude Opus 4.7 (generation) + Haiku 4.5 (translation/OCR) - Voyage voyage-3 or OpenAI text-embedding-3-small (embeddings) - python-docx (branded Word output, Montserrat + HP blue) - Docker Compose (5 services) Features - 6 built-in deliverable types (leadership themes, regional enrichment, LinkedIn posts, webinar spec, infographic specs, ABM enablement) - Data-driven deliverable types: admins add new types at runtime via prompt + JSON schema + template_json — no code, no deploy - Generic schema-driven review form + generic Word template renderer - Document ingestion pipeline with translation, chunking, pgvector RAG - Pluggable auth provider (password now, Entra SSO later); admin/user roles - Re-roll / retry on every deliverable; cascading delete; brief editing; inline document upload; progress hints; router-level ErrorBoundary - Admin panel with test-render preview for new deliverable types - Help page at /help with architecture overview and usage guide 82 backend tests passing, 18 skipped (gated live-API tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
2.4 KiB
Python
88 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
from pydantic import BaseModel, EmailStr
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.auth import get_auth_provider
|
|
from app.core.deps import get_current_user
|
|
from app.core.security import create_access_token
|
|
from app.db.models import User
|
|
from app.db.session import get_db
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request / response schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
class UserOut(BaseModel):
|
|
id: str
|
|
email: str
|
|
name: str
|
|
role: str
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
user: UserOut
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Routes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("/login", response_model=LoginResponse)
|
|
def login(
|
|
body: LoginRequest,
|
|
response: Response,
|
|
db: Session = Depends(get_db),
|
|
) -> Any:
|
|
provider = get_auth_provider()
|
|
user: User | None = provider.authenticate(body.email, body.password, db)
|
|
if user is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Incorrect email or password",
|
|
)
|
|
token = create_access_token(subject=str(user.id))
|
|
|
|
# httpOnly cookie — consumed by SSR / same-origin frontend
|
|
response.set_cookie(
|
|
key="access_token",
|
|
value=token,
|
|
httponly=True,
|
|
samesite="lax",
|
|
secure=False, # set True behind HTTPS in production
|
|
max_age=60 * 60 * 8,
|
|
)
|
|
return LoginResponse(
|
|
access_token=token,
|
|
user=UserOut(id=str(user.id), email=user.email, name=user.name, role=user.role),
|
|
)
|
|
|
|
|
|
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
|
def logout(response: Response) -> None:
|
|
response.delete_cookie("access_token")
|
|
|
|
|
|
@router.get("/me", response_model=UserOut)
|
|
def me(current_user: User = Depends(get_current_user)) -> Any:
|
|
return UserOut(
|
|
id=str(current_user.id),
|
|
email=current_user.email,
|
|
name=current_user.name,
|
|
role=current_user.role,
|
|
)
|