Files
interactive-story/backend/app/routers/story_cards.py
T
parththakkar106andClaude Fable 5 de4db373f2 Phase 8: optional accounts, per-user data, shared demo key
Guest-first multi-user mode behind AIDND_MULTI_USER (local installs
unchanged): signed-cookie guest sessions bootstrapped by /api/auth/me,
register upgrades the guest in place, login/logout, per-IP rate limits.
Every router scoped by user_id; Settings become per-user with the API
key Fernet-encrypted at rest and write-only through the API. Users
without a key get a server-funded demo key (OpenRouter free models,
20 turns/day, memory bank disabled on demo turns). Public read-only
demo scenarios (seed_demo.py); debug log restricted to local mode.
Frontend: auth modal + guest nudge, 401 re-establish/retry, demo
banner and key management in Settings.

Migrations 13-23 adopt existing data under a local user and encrypt
stored keys. Verified: migration on a copy of real data.db, two-session
isolation + register/login via curl and Chrome, demo cap 429, live
OpenRouter turn through the encrypted-key path, vite build + oxlint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFsGHju9szibJJa2YJcdbg
2026-07-06 23:04:03 +05:30

82 lines
3.1 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from .. import auth, models, schemas
from ..database import get_db
router = APIRouter(prefix="/api/story-cards", tags=["story-cards"])
def _card_editable_or_404(card: models.StoryCard | None, user: models.User) -> models.StoryCard:
"""Cards inherit their scope from the owning scenario/adventure. Public
(demo) scenarios are visible to everyone but editable by no one."""
if card is not None:
owner = card.scenario if card.scenario_id is not None else card.adventure
if owner is not None and owner.user_id == user.id:
return card
raise HTTPException(404, "Story card not found")
@router.get("", response_model=list[schemas.StoryCardOut])
def list_story_cards(
scenario_id: int | None = None,
adventure_id: int | None = None,
db: Session = Depends(get_db),
user: models.User = Depends(auth.get_current_user),
):
if (scenario_id is None) == (adventure_id is None):
raise HTTPException(422, "Provide exactly one of scenario_id or adventure_id")
if scenario_id is not None:
scenario = db.get(models.Scenario, scenario_id)
if scenario is None or (scenario.user_id != user.id and not scenario.is_public):
raise HTTPException(404, "Owner not found")
return sorted(scenario.story_cards, key=lambda c: c.id)
adventure = db.get(models.Adventure, adventure_id)
if adventure is None or adventure.user_id != user.id:
raise HTTPException(404, "Owner not found")
return sorted(adventure.story_cards, key=lambda c: c.id)
@router.post("", response_model=schemas.StoryCardOut, status_code=201)
def create_story_card(
payload: schemas.StoryCardCreate,
db: Session = Depends(get_db),
user: models.User = Depends(auth.get_current_user),
):
if (payload.scenario_id is None) == (payload.adventure_id is None):
raise HTTPException(422, "Provide exactly one of scenario_id or adventure_id")
owner_model = models.Scenario if payload.scenario_id else models.Adventure
owner_id = payload.scenario_id or payload.adventure_id
owner = db.get(owner_model, owner_id)
if owner is None or owner.user_id != user.id:
raise HTTPException(404, "Owner not found")
card = models.StoryCard(**payload.model_dump())
db.add(card)
db.commit()
return card
@router.patch("/{card_id}", response_model=schemas.StoryCardOut)
def update_story_card(
card_id: int,
payload: schemas.StoryCardUpdate,
db: Session = Depends(get_db),
user: models.User = Depends(auth.get_current_user),
):
card = _card_editable_or_404(db.get(models.StoryCard, card_id), user)
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(card, field, value)
db.commit()
return card
@router.delete("/{card_id}", status_code=204)
def delete_story_card(
card_id: int,
db: Session = Depends(get_db),
user: models.User = Depends(auth.get_current_user),
):
card = _card_editable_or_404(db.get(models.StoryCard, card_id), user)
db.delete(card)
db.commit()