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
This commit is contained in:
parththakkar106
2026-07-06 23:04:03 +05:30
co-authored by Claude Fable 5
parent 253b533d3b
commit de4db373f2
26 changed files with 1247 additions and 189 deletions
+14 -4
View File
@@ -2,9 +2,14 @@
Run from the backend folder: .venv\\Scripts\\python.exe seed_demo.py
Safe to rerun: it deletes any previous rows titled "[Demo] ..." first.
Phase 8: the scenario is seeded as PUBLIC (user_id NULL + is_public), so in
multi-user mode every guest sees it as read-only starter content. The sample
adventure and script-library copies belong to the local user (only relevant
on single-user installs).
"""
from app import models, migrations
from app import auth, models, migrations
from app.database import SessionLocal, engine
# create_all + user_version stamp; plain create_all would leave a fresh DB at
@@ -205,6 +210,8 @@ STORY_CARDS = [
db = SessionLocal()
try:
owner = auth.local_user(db)
# Remove earlier demo rows so reruns stay clean.
for adv in db.query(models.Adventure).filter(models.Adventure.title.like(f"{DEMO_PREFIX}%")):
db.delete(adv)
@@ -214,12 +221,14 @@ try:
db.delete(s)
db.commit()
# Script library
# Scripts attached to the public scenario are unowned (user_id NULL) so
# they ship with it everywhere; they're copied into each adventure at
# creation, so they never need to appear in anyone's script library.
scripts = [models.Script(**s) for s in SCRIPTS]
db.add_all(scripts)
# Scenario with cards and scripts attached
scenario = models.Scenario(**SCENARIO)
# Scenario with cards and scripts attached — public starter content.
scenario = models.Scenario(**SCENARIO, is_public=True)
scenario.scripts = scripts
db.add(scenario)
db.flush()
@@ -228,6 +237,7 @@ try:
# Adventure created from the scenario, mirroring POST /api/adventures
adventure = models.Adventure(
user_id=owner.id,
scenario_id=scenario.id,
title=scenario.title,
memory=scenario.memory,