rojects/workspace/outline/auth-restructure.html

366 lines
14 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>rokojori-auth Restructure — Roject</title>
<link rel="stylesheet" href="../_assets_/styles.css">
<link rel="stylesheet" href="../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<p class="date">Plan — In Progress</p>
<h1>rokojori-auth</h1>
<p class="subtitle">
Extracting the user system from Roject into a standalone centralized auth service
at <code>account.rokojori.com</code>, shared across all rokojori projects.
</p>
</header>
<section>
<h2>Why</h2>
<div class="card">
<p>
Roject currently has a self-contained user system (registration, login, sessions
via <code>express-session</code>, bcrypt password hashing, JSON file storage).
Any future rokojori project — a personal website, desktop tools, other services —
would need to duplicate this, resulting in multiple disconnected user systems with
no shared identity.
</p>
<p style="margin-top:0.75rem">
The goal is one user record per person across all rokojori projects. Roles,
permissions, purchased products, and global settings all live in one place.
New projects plug in by verifying a JWT — no auth code to write, no user table
to maintain.
</p>
<p style="margin-top:0.75rem">
Payments will be handled by <strong>Polar</strong> (merchant of record — handles
VAT/tax automatically). Polar is not used as a user store; it only tells us who
paid for what. That purchase record is then written to the user in
<code>rokojori-auth</code> via a webhook and stored there permanently.
</p>
</div>
</section>
<section>
<h2>Two Repos, Clear Responsibilities</h2>
<div class="card">
<h3>rokojori-auth — new repo</h3>
<p>
A standalone Express service, identical stack to Roject (Node.js, ts-node,
JSON file storage). Owns everything identity-related:
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li>User registration and login</li>
<li>JWT issuance (access token + refresh token)</li>
<li>Password reset via email</li>
<li>User profile management</li>
<li>Roles, permissions, products, global settings</li>
<li>Login and register HTML pages (shared across all apps)</li>
</ul>
<div class="tags">
<span class="tag">c:\rokojori\projects\web-projects\rokojori-auth</span>
<span class="tag">account.rokojori.com</span>
</div>
</div>
<div class="card">
<h3>roject — existing repo, becomes a client</h3>
<p>
Roject drops all user management and becomes a JWT-validating client.
Its own data (groups, projects, files, layouts) continues to be stored
locally — it just references <code>userId</code> from the JWT instead
of a local user table.
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li>Remove <code>source/server/routes/auth.ts</code></li>
<li>Remove <code>source/server/middleware/auth.ts</code></li>
<li>Remove user parts of <code>source/server/db.ts</code></li>
<li>Remove <code>bcryptjs</code>, <code>express-session</code></li>
<li>Remove <code>source/pages/login.html</code>, <code>register.html</code></li>
<li>Remove <code>source/server/email/</code> (moves to rokojori-auth)</li>
<li>Add JWT verification middleware (reads cookie, verifies with shared secret)</li>
<li>Login / logout nav links point to <code>account.rokojori.com</code></li>
</ul>
</div>
</section>
<section>
<h2>Two Login Modes</h2>
<div class="card">
<h3>Browser — redirect flow</h3>
<p>
Used by web apps (Roject, website). App links to
<code>account.rokojori.com/login?redirect=...</code>, user logs in on the
HTML page, cookie is set on <code>.rokojori.com</code>, browser is redirected
back. No token handling needed in the app.
</p>
</div>
<div class="card">
<h3>Non-browser — direct API call</h3>
<p>
Used by Electron, mobile apps, Godot, CLI tools, or any client that manages
its own storage. POST credentials directly to the login endpoint and receive
tokens in the response body — no browser, no redirect, no cookie.
</p>
<pre style="margin-top:0.75rem"><code>POST https://account.rokojori.com/api/auth/login
{ "email": "...", "password": "..." }
→ { "accessToken": "...", "refreshToken": "..." }</code></pre>
<p style="margin-top:0.75rem">
The client stores the tokens locally (file, memory, secure storage) and sends
the access token on every request as a header:
</p>
<pre style="margin-top:0.5rem"><code>Authorization: Bearer &lt;accessToken&gt;</code></pre>
<p style="margin-top:0.75rem">
When the access token expires, the client calls
<code>POST /api/auth/refresh</code> with the refresh token to get a new one —
no re-login needed.
</p>
</div>
<div class="card">
<h3>Same endpoint, both modes</h3>
<p>
<code>POST /api/auth/login</code> always returns JSON with the tokens.
The HTML login page additionally sets the cookie and performs the redirect
client-side after reading the response. Non-browser clients ignore the cookie
and just use the response body. No separate endpoints needed.
</p>
</div>
</section>
<section>
<h2>Token Strategy</h2>
<div class="card">
<h3>Access token — short-lived JWT</h3>
<p>
Signed with a shared <code>JWT_SECRET</code> (HS256). Contains everything
an app needs to know about the user — no database call required at runtime:
</p>
<pre style="margin-top:0.75rem"><code>{
"userId": "uuid",
"email": "user@example.com",
"roles": ["user"],
"products": ["roject-pro"],
"settings": { "theme": "dark", "language": "en" }
}</code></pre>
<p style="margin-top:0.75rem">
Lifetime: ~1 hour. Apps verify the signature locally — no round-trip to
<code>rokojori-auth</code> on every request.
</p>
</div>
<div class="card">
<h3>Refresh token — long-lived, server-side</h3>
<p>
A random UUID stored in <code>refreshTokens.json</code> on
<code>rokojori-auth</code>. Used only to request a new access token when
the current one expires. Lifetime: 30 days. Invalidated on logout.
</p>
</div>
<div class="card">
<h3>Cookie on <code>.rokojori.com</code></h3>
<p>
On login, <code>rokojori-auth</code> sets the access token as a cookie
on the <code>.rokojori.com</code> domain. This makes it automatically
available to all subdomains (<code>roject.rokojori.com</code>,
<code>account.rokojori.com</code>, future services) without any extra
token-passing logic.
</p>
<p style="margin-top:0.75rem">
Web apps read the cookie directly. Desktop/CLI apps (e.g. Electron later)
store the token locally and send it in the <code>Authorization</code> header.
</p>
</div>
</section>
<section>
<h2>Login Flow</h2>
<div class="card">
<ol style="line-height:2">
<li>User visits <code>roject.rokojori.com</code>, not logged in.</li>
<li>Clicks Login → browser goes to
<code>account.rokojori.com/login?redirect=https://roject.rokojori.com</code>
</li>
<li>User submits credentials on <code>account.rokojori.com/login.html</code>.</li>
<li><code>rokojori-auth</code> verifies password, issues JWT, sets cookie on
<code>.rokojori.com</code>.</li>
<li>Redirects to the <code>redirect</code> URL — cookie is already valid there.</li>
<li>Roject reads and verifies the cookie — user is logged in.</li>
</ol>
<p style="margin-top:0.75rem">
Logout works the same way: link to
<code>account.rokojori.com/logout?redirect=...</code>, which clears the cookie
and redirects back.
</p>
</div>
</section>
<section>
<h2>User Data Model</h2>
<div class="card">
<h3>users.json — one record per user</h3>
<pre><code>{
"id": "uuid",
"email": "user@example.com",
"passwordHash": "...",
"roles": ["user"],
"products": [
{ "id": "roject-pro", "acquiredAt": "2026-07-12", "source": "polar" }
],
"settings": {
"theme": "dark",
"language": "en"
},
"createdAt": "2026-07-12T00:00:00Z"
}</code></pre>
</div>
<div class="card">
<h3>Roles and permissions</h3>
<p>
Role types are defined in code (a TypeScript object or JSON file) mapping
role names to permission sets. The user record stores only the role name —
the permissions are derived at runtime.
</p>
<pre style="margin-top:0.75rem"><code>{
"admin": ["manage-users", "manage-products", "access-all"],
"user": ["access-own"]
}</code></pre>
<p style="margin-top:0.75rem">
App-specific roles (e.g. "editor of project X" in Roject) are never stored
in <code>rokojori-auth</code> — they stay in the respective app, keyed by
<code>userId</code>.
</p>
</div>
<div class="card">
<h3>Products</h3>
<p>
When Polar fires a purchase webhook, a record is appended to the user's
<code>products</code> array. Apps check product IDs from the JWT at runtime —
no Polar dependency needed outside of the webhook handler.
</p>
</div>
<div class="card">
<h3>Settings</h3>
<p>
A free JSON object for global preferences (theme, language, timezone, etc.)
that should be consistent across all apps. App-specific preferences (Roject
panel layout, editor config) stay in the respective app.
</p>
</div>
<div class="card">
<h3>refreshTokens.json</h3>
<pre><code>{ "token": "uuid", "userId": "uuid", "expiresAt": "2026-08-12T00:00:00Z" }</code></pre>
</div>
<div class="card">
<h3>resetTokens.json</h3>
<pre><code>{ "token": "uuid", "userId": "uuid", "expiresAt": "2026-07-12T01:00:00Z" }</code></pre>
</div>
</section>
<section>
<h2>API Endpoints</h2>
<div class="card">
<pre><code>POST /api/auth/register — create user, issue tokens
POST /api/auth/login — verify password, issue tokens
POST /api/auth/logout — invalidate refresh token, clear cookie
POST /api/auth/refresh — swap refresh token for new access token
POST /api/auth/forgot-password — send password reset email
POST /api/auth/reset-password — consume reset token, set new password
GET /api/auth/me — return user data from access token
PATCH /api/auth/me/settings — merge-update global settings</code></pre>
</div>
</section>
<section>
<h2>File Structure — rokojori-auth</h2>
<div class="card">
<pre><code>rokojori-auth/
source/
server/
routes/
auth.ts — all auth endpoints
middleware/
requireAuth.ts — JWT verification middleware
email/
EmailSender.ts — interface
SMTPEmailSender.ts — Nodemailer implementation
EmailService.ts — static facade
roles.ts — role → permissions map
db.ts — user, refresh token, reset token storage
index.ts — Express app entry point
pages/
login.html — accepts ?redirect= param
register.html — accepts ?redirect= param
profile.html — view/edit email, change password
scripts/
copy-pages.js
package.json
tsconfig.json
tsconfig.client.json
tsconfig.ts-node.json
.gitignore
.env — never committed</code></pre>
</div>
</section>
<section>
<h2>Environment Variables</h2>
<div class="card">
<h3>Shared across all services</h3>
<pre><code>JWT_SECRET=... — same value on all services that verify tokens</code></pre>
<h3 style="margin-top:1rem">rokojori-auth only</h3>
<pre><code>SMTP_HOST=smtp.ionos.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=noreply@rokojori.com
SMTP_PASS=...
SMTP_FROM=noreply@rokojori.com
PORT=3001 — or whichever port nginx proxies to</code></pre>
</div>
</section>
<section>
<h2>Adding a New App Later</h2>
<div class="card">
<ol style="line-height:2">
<li>Add <code>JWT_SECRET</code> env var to the new app.</li>
<li>Add JWT verification middleware (copy from Roject).</li>
<li>Point login/logout links at <code>account.rokojori.com</code>.</li>
<li>Done — no auth code to write, no user table to create.</li>
</ol>
</div>
</section>
<footer>
Roject &mdash; rokojori-auth restructure plan
</footer>
</div>
<script>var NAV_ROOT = '../';</script>
<script src="../_assets_/nav-data.js"></script>
<script src="../_assets_/nav.js"></script>
</body>
</html>