366 lines
14 KiB
HTML
366 lines
14 KiB
HTML
<!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 <accessToken></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 — 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>
|