Files
pablan/docs/diagrams/auth-sequence.svg
T
ProfessorNovaandClaude Opus 5 784b76baf7 Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 09:21:37 +02:00

159 lines
11 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1290" width="1000" height="1290" role="img" aria-label="Login and every page request">
<title>Login and every page request</title>
<style>
:root {
--bg: #fbfaf9;
--surface: #ffffff;
--sunken: #f2f0ed;
--ink: #1a1815;
--muted: #63605a;
--line: #c5bfb6;
--line-soft: #e5e1db;
--accent: #b8770a;
--secondary: #b04a00;
--note: #faf3e6;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0e0d;
--surface: #1a1817;
--sunken: #232120;
--ink: #efece8;
--muted: #a49d94;
--line: #4a443d;
--line-soft: #332f2b;
--accent: #e6b422;
--secondary: #e08030;
--note: #241f16;
}
}
text { font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; fill: var(--ink); }
.t-title { font-size: 15px; font-weight: 650; }
.t-sub { font-size: 11.5px; fill: var(--muted); }
.t-node { font-size: 12.5px; font-weight: 600; }
.t-body { font-size: 11.5px; }
.t-muted { font-size: 11px; fill: var(--muted); }
.t-edge { font-size: 10.5px; fill: var(--muted); }
.t-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
.box { fill: var(--surface); stroke: var(--line); stroke-width: 1.2; }
.box-sunken { fill: var(--sunken); stroke: var(--line-soft); stroke-width: 1; }
.box-accent { fill: var(--surface); stroke: var(--accent); stroke-width: 1.6; }
.note { fill: var(--note); stroke: var(--line-soft); stroke-width: 1; }
.group { fill: none; stroke: var(--line-soft); stroke-width: 1.2;
stroke-dasharray: 5 4; }
.lifeline { stroke: var(--line); stroke-width: 1; stroke-dasharray: 4 4; }
.edge { stroke: var(--line); stroke-width: 1.3; fill: none; }
.edge-accent { stroke: var(--secondary); stroke-width: 1.5; fill: none; }
.edge-soft { stroke: var(--line); stroke-width: 1.1; fill: none;
stroke-dasharray: 5 4; }
.edge-soft-accent { stroke: var(--secondary); stroke-width: 1.3; fill: none;
stroke-dasharray: 5 4; }
.planned { stroke-dasharray: 6 4; opacity: 0.62; }
</style>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 1 L 9 5 L 0 9 z" fill="var(--line)"/>
</marker>
<marker id="arrow-accent" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 1 L 9 5 L 0 9 z" fill="var(--secondary)"/>
</marker>
<marker id="arrow-open" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 1 L 9 5 L 0 9" fill="none" stroke="var(--line)"
stroke-width="1.3"/>
</marker>
</defs>
<rect width="1000" height="1290" fill="var(--bg)"/>
<text x="28" y="30" class="t-title" text-anchor="start">Login and every page request</text>
<text x="28" y="50" class="t-sub" text-anchor="start">Server-side sessions: the cookie token IS the auth_sessions row id.</text>
<rect x="98" y="74" width="96" height="30" rx="8" class="box" />
<text x="146" y="93" class="t-node" text-anchor="middle">Browser</text>
<line x1="146" y1="104" x2="146" y2="1272" class="lifeline"/>
<rect x="316.2" y="74" width="131.6" height="30" rx="8" class="box" />
<text x="382" y="93" class="t-node" text-anchor="middle">SvelteKit server</text>
<line x1="382" y1="104" x2="382" y2="1272" class="lifeline"/>
<rect x="570" y="74" width="96" height="30" rx="8" class="box" />
<text x="618" y="93" class="t-node" text-anchor="middle">FastAPI</text>
<line x1="618" y1="104" x2="618" y2="1272" class="lifeline"/>
<rect x="806" y="74" width="96" height="30" rx="8" class="box" />
<text x="854" y="93" class="t-node" text-anchor="middle">Postgres</text>
<line x1="854" y1="104" x2="854" y2="1272" class="lifeline"/>
<line x1="28" y1="138" x2="972" y2="138" class="edge-soft"/>
<rect x="336.325" y="127" width="327.35" height="22" rx="11" class="box-sunken" />
<text x="500" y="142" class="t-muted" text-anchor="middle">Login: the browser talks to FastAPI directly (D8)</text>
<text x="382" y="170" class="t-edge" text-anchor="middle">POST /api/auth/login {email, password}</text>
<line x1="146" y1="187" x2="618" y2="187" class="edge" marker-end="url(#arrow)"/>
<text x="736" y="209" class="t-edge" text-anchor="middle">SELECT user by email</text>
<line x1="618" y1="226" x2="854" y2="226" class="edge" marker-end="url(#arrow)"/>
<rect x="644" y="268" width="321.2" height="40" rx="6" class="note" />
<text x="657" y="285" class="t-body" text-anchor="start">Unknown email still burns a dummy argon2 verify,</text>
<text x="657" y="298" class="t-body" text-anchor="start">so timing reveals nothing about who exists.</text>
<text x="382" y="326" class="t-edge" text-anchor="middle">401 {detail, code: invalid_credentials}</text>
<line x1="618" y1="343" x2="146" y2="343" class="edge-soft" marker-end="url(#arrow-open)"/>
<text x="736" y="407" class="t-edge" text-anchor="middle">INSERT auth_sessions</text>
<text x="736" y="420" class="t-edge" text-anchor="middle">(id = cookie token, expires_at = now + 14d)</text>
<line x1="618" y1="437" x2="854" y2="437" class="edge" marker-end="url(#arrow)"/>
<text x="382" y="459" class="t-edge" text-anchor="middle">200 user + Set-Cookie pablan_session</text>
<text x="382" y="472" class="t-edge" text-anchor="middle">(httpOnly, SameSite=Lax, Secure per PABLAN_COOKIE_SECURE)</text>
<line x1="618" y1="489" x2="146" y2="489" class="edge-soft-accent" marker-end="url(#arrow-accent)"/>
<line x1="28" y1="533" x2="972" y2="533" class="edge-soft"/>
<rect x="336.325" y="522" width="327.35" height="22" rx="11" class="box-sunken" />
<text x="500" y="537" class="t-muted" text-anchor="middle">Every page request: handle = sequence(auth, i18n)</text>
<text x="264" y="565" class="t-edge" text-anchor="middle">GET /some-page (Cookie: pablan_session, Accept-Language)</text>
<line x1="146" y1="582" x2="382" y2="582" class="edge" marker-end="url(#arrow)"/>
<text x="500" y="604" class="t-edge" text-anchor="middle">GET /api/auth/me (absolute URL, cookie forwarded)</text>
<line x1="382" y1="621" x2="618" y2="621" class="edge" marker-end="url(#arrow)"/>
<rect x="408" y="637" width="351.95" height="40" rx="6" class="note" />
<text x="421" y="654" class="t-body" text-anchor="start">The base URL must be absolute: a relative server-side</text>
<text x="421" y="667" class="t-body" text-anchor="start">fetch never reaches the proxy.</text>
<text x="736" y="695" class="t-edge" text-anchor="middle">SELECT auth_sessions + user, check expires_at</text>
<line x1="618" y1="712" x2="854" y2="712" class="edge" marker-end="url(#arrow)"/>
<text x="500" y="760" class="t-edge" text-anchor="middle">200 user (incl. locale)</text>
<line x1="618" y1="777" x2="382" y2="777" class="edge-soft" marker-end="url(#arrow-open)"/>
<rect x="408" y="793" width="444.2" height="53" rx="6" class="note" />
<text x="421" y="810" class="t-body" text-anchor="start">Auth runs FIRST and stashes user.locale in a WeakMap keyed by the</text>
<text x="421" y="823" class="t-body" text-anchor="start">Request: the locale strategy only receives the request, and a second</text>
<text x="421" y="836" class="t-body" text-anchor="start">/me would ask a question we already asked.</text>
<rect x="408" y="858" width="450.35" height="40" rx="6" class="note" />
<text x="421" y="875" class="t-body" text-anchor="start">i18n: paraglideMiddleware resolves userPreference &gt; cookie &gt;</text>
<text x="421" y="888" class="t-body" text-anchor="start">Accept-Language &gt; base, then stamps %lang% / %dir% into the document.</text>
<text x="264" y="916" class="t-edge" text-anchor="middle">render with locals.user, in the resolved locale</text>
<line x1="382" y1="933" x2="146" y2="933" class="edge-soft" marker-end="url(#arrow-open)"/>
<text x="500" y="997" class="t-edge" text-anchor="middle">401 {code: not_authenticated}</text>
<line x1="618" y1="1014" x2="382" y2="1014" class="edge-soft" marker-end="url(#arrow-open)"/>
<rect x="408" y="1030" width="438.05" height="27" rx="6" class="note" />
<text x="421" y="1047" class="t-body" text-anchor="start">No user, so the locale falls back to the cookie or Accept-Language.</text>
<text x="264" y="1075" class="t-edge" text-anchor="middle">303 redirect to /login</text>
<line x1="382" y1="1092" x2="146" y2="1092" class="edge-soft" marker-end="url(#arrow-open)"/>
<line x1="28" y1="1136" x2="972" y2="1136" class="edge-soft"/>
<rect x="437.8" y="1125" width="124.4" height="22" rx="11" class="box-sunken" />
<text x="500" y="1140" class="t-muted" text-anchor="middle">Ending a session</text>
<rect x="290.2" y="1162" width="419.6" height="92" rx="6" class="note" />
<text x="303.2" y="1179" class="t-body" text-anchor="start">Logout deletes the session row: revoked everywhere, instantly.</text>
<text x="303.2" y="1192" class="t-body" text-anchor="start"></text>
<text x="303.2" y="1205" class="t-body" text-anchor="start">Password change (POST /api/account/password): verify the current</text>
<text x="303.2" y="1218" class="t-body" text-anchor="start">hash, store the new one, then DELETE auth_sessions WHERE</text>
<text x="303.2" y="1231" class="t-body" text-anchor="start">user_id = me AND id &lt;&gt; my session. Other devices are logged out,</text>
<text x="303.2" y="1244" class="t-body" text-anchor="start">this one keeps its cookie.</text>
<rect x="28" y="242" width="944" height="125" rx="8" class="group" />
<rect x="28" y="242" width="38.45" height="20" rx="6" class="box-sunken" />
<text x="47.225" y="256" class="t-muted" text-anchor="middle">alt</text>
<text x="78.45" y="256" class="t-muted" text-anchor="start">unknown email or wrong password</text>
<rect x="28" y="375" width="944" height="138" rx="8" class="group" />
<rect x="28" y="375" width="44.6" height="20" rx="6" class="box-sunken" />
<text x="50.3" y="389" class="t-muted" text-anchor="middle">else</text>
<text x="84.6" y="389" class="t-muted" text-anchor="start">credentials valid</text>
<rect x="28" y="728" width="944" height="229" rx="8" class="group" />
<rect x="28" y="728" width="38.45" height="20" rx="6" class="box-sunken" />
<text x="47.225" y="742" class="t-muted" text-anchor="middle">alt</text>
<text x="78.45" y="742" class="t-muted" text-anchor="start">session valid</text>
<rect x="28" y="965" width="944" height="151" rx="8" class="group" />
<rect x="28" y="965" width="44.6" height="20" rx="6" class="box-sunken" />
<text x="50.3" y="979" class="t-muted" text-anchor="middle">else</text>
<text x="84.6" y="979" class="t-muted" text-anchor="start">missing, expired or invalid</text>
</svg>