rokojori-auth/workspace/index.html

330 lines
13 KiB
HTML
Raw Normal View History

2026-07-13 11:42:50 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>rokojori-auth — Developer Documentation</title>
<link rel="stylesheet" href="_assets_/styles.css">
<link rel="stylesheet" href="_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<p class="date">Project Documentation</p>
<h1>rokojori-auth</h1>
<p class="subtitle">
Centralized auth service for all rokojori projects, deployed at
<code>account.rokojori.com</code>. Developer reference for human and agent contributors.
</p>
</header>
<section>
<h2>What it is</h2>
<div class="card">
<p>
rokojori-auth is a standalone Express service that owns all identity for the
rokojori ecosystem — registration, login, JWT issuance, password reset, roles,
products, and global user settings. Every other rokojori project (Roject, future
apps, desktop tools) plugs in by verifying the shared JWT. No auth code to write,
no user table to maintain in the client app.
</p>
<p style="margin-top:0.75rem">
The service issues a short-lived access token (1 hour) and a long-lived refresh
token (30 days). For browser clients both are set as <code>HttpOnly</code> cookies
on <code>.rokojori.com</code>, making them automatically available to all subdomains.
Non-browser clients (Electron, CLI, Godot) receive the tokens in the response body
and manage them locally.
</p>
</div>
</section>
<section>
<h2>Features</h2>
<div class="card">
<h3>Authentication</h3>
<p>
Email and password registration and login. Passwords are hashed with bcrypt (cost 10).
On login or register, the server issues an access token cookie and a refresh token
cookie, both scoped to <code>.rokojori.com</code>. The refresh token is also returned
in the response body for non-browser clients.
</p>
<div class="tags">
<span class="tag">POST /api/auth/register</span>
<span class="tag">POST /api/auth/login</span>
<span class="tag">POST /api/auth/logout</span>
</div>
</div>
<div class="card">
<h3>Token refresh</h3>
<p>
Browser clients whose access token has expired are redirected to
<code>GET /api/auth/refresh-session?redirect=...</code>. The server reads the
refresh token cookie, rotates both tokens, and redirects back. Non-browser clients
call <code>POST /api/auth/refresh</code> with the refresh token in the request body.
</p>
<div class="tags">
<span class="tag">GET /api/auth/refresh-session</span>
<span class="tag">POST /api/auth/refresh</span>
</div>
</div>
<div class="card">
<h3>Password reset</h3>
<p>
Forgot-password sends a time-limited email link (1 hour). The endpoint is
rate-limited per IP with an escalating response delay (5 s → 15 s → 30 s,
hard block after 20 attempts in 20 minutes) to prevent email spam.
The response is always <code>{ ok: true }</code> regardless of whether the
email exists, to avoid leaking account information.
</p>
<div class="tags">
<span class="tag">POST /api/auth/forgot-password</span>
<span class="tag">POST /api/auth/reset-password</span>
</div>
</div>
<div class="card">
<h3>Profile and account management</h3>
<p>
Authenticated users can read their profile, update global settings, change their
password (requires current password), and permanently delete their account.
Deletion removes the user record and all associated tokens.
</p>
<div class="tags">
<span class="tag">GET /api/auth/me</span>
<span class="tag">PATCH /api/auth/me/settings</span>
<span class="tag">POST /api/auth/me/password</span>
<span class="tag">DELETE /api/auth/me</span>
</div>
</div>
<div class="card">
<h3>Roles</h3>
<p>
Three built-in roles: <code>user</code> (default on registration),
<code>admin</code> (enhanced profile, user list), and <code>superadmin</code>
(can manage roles of other users). Roles are included in the JWT payload so
client apps can gate features without a round-trip. App-specific roles (e.g.
editor of a specific Roject project) stay in the respective app, keyed by
<code>userId</code>.
</p>
<p style="margin-top:0.75rem">
Bootstrap: set <code>INITIAL_SUPERADMIN_EMAIL</code> in <code>.env</code>.
The first registration with that email is automatically promoted to
<code>superadmin</code>, provided no superadmin exists yet.
</p>
</div>
<div class="card">
<h3>Products</h3>
<p>
Each user record has a <code>products</code> array. When a purchase is confirmed
(planned: via a Polar webhook), a product entry is appended. Apps read the product
list from the JWT at runtime — no Polar dependency needed outside the webhook handler.
</p>
</div>
<div class="card">
<h3>Rate limiting</h3>
<p>
In-memory per-IP rate limits on all sensitive endpoints. Windows and thresholds:
</p>
<ul style="margin-top:0.75rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li><strong style="color:var(--text)">Login</strong> — 10 attempts / 15 min; delay of 3 s added after attempt 5</li>
<li><strong style="color:var(--text)">Register</strong> — 5 attempts / hour, hard block</li>
<li><strong style="color:var(--text)">Forgot-password</strong> — 20 attempts / 20 min; escalating delay 5 s → 15 s → 30 s</li>
</ul>
</div>
<div class="card">
<h3>HTML pages</h3>
<p>
Five self-contained pages served at <code>account.rokojori.com</code>,
shared across all rokojori apps. All pages are plain HTML with inline
CSS and JavaScript — no framework, no build step.
</p>
<ul style="margin-top:0.75rem;line-height:1.9;font-size:0.9rem;color:var(--muted)">
<li><code>login.html</code> — accepts <code>?redirect=</code> query param</li>
<li><code>register.html</code> — accepts <code>?redirect=</code> query param</li>
<li><code>profile.html</code> — change password, delete account; admin/superadmin see user list</li>
<li><code>forgot-password.html</code></li>
<li><code>reset-password.html</code> — reads <code>?token=</code> from URL; includes hidden email field for browser password-manager integration</li>
</ul>
</div>
</section>
<section>
<h2>Technical Implementation</h2>
<div class="card">
<h3>Stack</h3>
<p>
Node.js + Express, TypeScript compiled on the fly with <code>ts-node</code>.
No database — all data lives as JSON files in <code>build/data/</code>
(auto-created on first run). All entity IDs are UUIDs via
<code>crypto.randomUUID()</code>. Email via Nodemailer (<code>SMTPEmailSender</code>),
configured entirely through environment variables.
</p>
<div class="tags">
<span class="tag">Node.js</span>
<span class="tag">Express</span>
<span class="tag">ts-node</span>
<span class="tag">jsonwebtoken</span>
<span class="tag">bcryptjs</span>
<span class="tag">nodemailer</span>
<span class="tag">cookie-parser</span>
</div>
</div>
<div class="card">
<h3>File structure</h3>
<pre><code>rokojori-auth/
source/
server/
routes/
auth.ts — all auth + profile endpoints
admin.ts — user list + role management (admin/superadmin)
middleware/
requireAuth.ts — JWT verification (cookie or Authorization header)
requireAdmin.ts — role guards (requireAdmin, requireSuperAdmin)
email/
EmailSender.ts — interface
SMTPEmailSender.ts
EmailService.ts — static facade
db.ts — users, refreshTokens, resetTokens (JSON file storage)
roles.ts — role → permissions map, helper functions
rateLimiter.ts — per-IP in-memory rate limiters
index.ts — Express entry point
pages/ — HTML pages (copied to build/app/ by build script)
scripts/
copy-pages.js
workspace/ — this documentation</code></pre>
</div>
<div class="card">
<h3>Data model</h3>
<pre><code>// users.json
{
"id": "uuid",
"email": "user@example.com",
"passwordHash": "...",
"roles": ["user"],
"products": [
{ "id": "roject-pro", "acquiredAt": "2026-07-13", "source": "polar" }
],
"settings": { "theme": "dark", "language": "en" },
"createdAt": "2026-07-13T00:00:00Z"
}
// refreshTokens.json
{ "token": "uuid", "userId": "uuid", "expiresAt": "..." }
// resetTokens.json
{ "token": "uuid", "userId": "uuid", "expiresAt": "..." }</code></pre>
</div>
<div class="card">
<h3>JWT payload</h3>
<p>
Access tokens are signed HS256 with <code>JWT_SECRET</code> (shared across all
services). They carry everything a client app needs to know — no round-trip to
rokojori-auth required at runtime.
</p>
<pre><code>{
"userId": "uuid",
"email": "user@example.com",
"roles": ["user"],
"products": ["roject-pro"],
"settings": { "theme": "dark" }
}</code></pre>
</div>
<div class="card">
<h3>Deployment</h3>
<p>
Running on Server A at <code>account.rokojori.com</code>. nginx handles TLS
termination (Let's Encrypt) and reverse-proxies to the Node.js process on port
3001, managed by a systemd service (<code>rokojori-auth.service</code>).
<code>trust proxy</code> is enabled so the rate limiter sees real client IPs.
</p>
<div class="tags">
<span class="tag">account.rokojori.com</span>
<span class="tag">nginx + Let's Encrypt</span>
<span class="tag">systemd</span>
<span class="tag">port 3001</span>
</div>
</div>
</section>
<section>
<h2>Planned</h2>
<div class="card">
<h3>Polar payment webhook</h3>
<p>
When a user purchases a product through Polar (merchant of record — handles
VAT/tax automatically), Polar fires a webhook to rokojori-auth. The handler
verifies the Polar signature, finds the user by email, and appends a product
entry to their record. The updated product list is included in the next JWT
issued for that user. No Polar dependency is needed in any other service.
</p>
<div class="tags">
<span class="tag">POST /api/webhooks/polar</span>
<span class="tag">signature verification</span>
<span class="tag">products array</span>
</div>
</div>
<div class="card">
<h3>Security hardening pass</h3>
<p>
A dedicated review pass covering: helmet.js headers, CSRF considerations for the
cookie flow, refresh token rotation audit, token expiry edge cases, and a review
of the rate limiter behaviour under proxy chains.
</p>
</div>
</section>
<section>
<h2>Related — Roject</h2>
<div class="card">
<p>
<strong style="color:var(--text)">Roject</strong> is the first client app of
rokojori-auth. It is a self-hosted, agent-based IDE deployed at
<code>roject.rokojori.com</code>. After the auth integration is complete, Roject
will drop its own user system entirely and become a JWT-validating client: it
redirects to <code>account.rokojori.com/login</code> for login, reads the shared
cookie, and verifies the JWT locally on every request.
</p>
<p style="margin-top:0.75rem">
For deeper context on the overall ecosystem, coding conventions, and how to work
with the codebase, consult the <strong style="color:var(--text)">Roject workspace
documentation</strong>. It contains guides (writing TypeScript code, backend routes,
editor panels), repeatable actions (update history, update outline), and reference
docs (Editor Singleton, client/server split). Ask for the Roject workspace docs
for those details.
</p>
<div class="tags">
<span class="tag">roject.rokojori.com</span>
<span class="tag">JWT client</span>
<span class="tag">shared .rokojori.com cookie</span>
</div>
</div>
</section>
<footer>
rokojori-auth &mdash; developer documentation
</footer>
</div>
<script>var NAV_ROOT = './';</script>
<script src="_assets_/nav-data.js"></script>
<script src="_assets_/nav.js"></script>
</body>
</html>