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
+46 -1
View File
@@ -1,6 +1,32 @@
import { useEffect, useState } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
import { api } from './api'
import { AuthModal } from './components'
export default function App() {
// null until /auth/me resolves; in local mode multi_user=false hides all auth UI.
const [me, setMe] = useState(null)
const [authMode, setAuthMode] = useState(null) // 'register' | 'login' | null
useEffect(() => {
api.getMe().then(setMe).catch(() => {})
}, [])
const onAuthed = (newMe, mode) => {
setAuthMode(null)
if (mode === 'login') {
// Different user now — reload so every page refetches its scoped data.
window.location.reload()
} else {
setMe(newMe) // register upgrades the same user in place; data unchanged
}
}
const logout = async () => {
try { await api.logout() } catch { /* already logged out */ }
window.location.reload()
}
return (
<>
<nav className="topnav">
@@ -17,8 +43,27 @@ export default function App() {
<NavLink to="/settings" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
Settings
</NavLink>
{me?.multi_user && (
<div className="nav-account">
{me.is_guest ? (
<>
<span className="guest-nudge">Playing as guest — sign up to keep your adventures</span>
<button onClick={() => setAuthMode('login')}>Log in</button>
<button className="primary" onClick={() => setAuthMode('register')}>Sign up</button>
</>
) : (
<>
<span className="account-email" title={me.email}>{me.email}</span>
<button onClick={logout}>Log out</button>
</>
)}
</div>
)}
</nav>
<Outlet />
<Outlet context={{ me, setMe }} />
{authMode && (
<AuthModal mode={authMode} onClose={() => setAuthMode(null)} onAuthed={onAuthed} />
)}
</>
)
}