178 lines
6.8 KiB
HTML
178 lines
6.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sunday, 13 July 2026 — 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">Sunday, 13 July 2026</p>
|
|
<h1>rokojori-auth — built and deployed</h1>
|
|
<p class="subtitle">
|
|
Full centralized auth service created from scratch, deployed to
|
|
<code>account.rokojori.com</code>, and documented.
|
|
</p>
|
|
</header>
|
|
|
|
<section>
|
|
<h2>What we built</h2>
|
|
|
|
<div class="card">
|
|
<h3>rokojori-auth — new standalone service</h3>
|
|
<p>
|
|
A complete auth service at <code>c:\rokojori\projects\web-projects\rokojori-auth</code>,
|
|
same stack as Roject (Node.js, Express, ts-node, JSON file storage). Built entirely
|
|
from scratch this session. Deployed to <code>account.rokojori.com</code> via nginx +
|
|
systemd + Let's Encrypt on Server A, port 3001.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">account.rokojori.com</span>
|
|
<span class="tag">nginx + systemd</span>
|
|
<span class="tag">port 3001</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Auth endpoints</h3>
|
|
<p>
|
|
Register, login, logout, refresh (body), refresh-session (cookie + redirect),
|
|
forgot-password, reset-password, GET /me, PATCH /me/settings, POST /me/password,
|
|
DELETE /me. Access token is a 1-hour HS256 JWT containing userId, email, roles,
|
|
products, settings. Refresh token is a 30-day UUID stored server-side.
|
|
Both are set as <code>HttpOnly</code> cookies on <code>.rokojori.com</code>
|
|
and also returned in the response body for non-browser clients.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">jsonwebtoken</span>
|
|
<span class="tag">bcryptjs</span>
|
|
<span class="tag">cookie-parser</span>
|
|
<span class="tag">HttpOnly cookie</span>
|
|
<span class="tag">.rokojori.com domain</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Roles: user, admin, superadmin</h3>
|
|
<p>
|
|
Three built-in roles. <code>superadmin</code> is bootstrapped via
|
|
<code>INITIAL_SUPERADMIN_EMAIL</code> in <code>.env</code> — fires once on
|
|
first registration if no superadmin exists. Superadmin can grant/revoke admin
|
|
on other users; cannot change their own roles. Admin and superadmin get an
|
|
enhanced profile page with a full user list. Role management controls are
|
|
superadmin-only.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Rate limiting</h3>
|
|
<p>
|
|
In-memory per-IP rate limiters on all sensitive endpoints. Login: 10 attempts /
|
|
15 min, 3 s delay after attempt 5. Register: 5 / hour. Forgot-password: 20 / 20 min
|
|
with escalating delay (5 s → 15 s → 30 s). <code>trust proxy</code> enabled so
|
|
real client IPs are seen behind nginx.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>HTML pages</h3>
|
|
<p>
|
|
Five self-contained pages: login, register, profile, forgot-password, reset-password.
|
|
Plain HTML with inline CSS and JS. Login and register accept a <code>?redirect=</code>
|
|
query param. Reset-password fetches the email for the token via
|
|
<code>GET /api/auth/reset-token-email</code> and populates a hidden email field
|
|
so browsers offer to update the saved password. Profile page includes
|
|
change-password (with current password verification) and a two-step delete account
|
|
confirmation. Welcome email sent on registration (non-blocking).
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Admin routes</h3>
|
|
<p>
|
|
<code>GET /api/admin/users</code> (admin + superadmin) lists all users without
|
|
password hashes. <code>PATCH /api/admin/users/:id/roles</code> (superadmin only)
|
|
sets roles on another user. Guards: cannot change own roles; <code>user</code>
|
|
base role is always preserved.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Workspace documentation</h3>
|
|
<p>
|
|
Created <code>workspace/</code> in the rokojori-auth repo with the same asset
|
|
system as the Roject workspace (styles, nav, breadcrumb). A single
|
|
<code>workspace/index.html</code> covers features, technical implementation,
|
|
data model, JWT payload, deployment, planned work, and a reference to the
|
|
Roject workspace for guides and actions.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Key decisions</h2>
|
|
|
|
<div class="decision">
|
|
<strong>Refresh token also set as HttpOnly cookie</strong>
|
|
<p>
|
|
Originally the refresh token was only returned in the response body. Added a
|
|
refresh token cookie so browser clients can silently refresh via
|
|
<code>GET /api/auth/refresh-session?redirect=...</code> without re-login.
|
|
Non-browser clients continue to use the body token and
|
|
<code>POST /api/auth/refresh</code>.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>Superadmin cannot change their own roles</strong>
|
|
<p>
|
|
The PATCH /api/admin/users/:id/roles endpoint blocks changes to the calling
|
|
user's own record to prevent accidental self-demotion or lockout.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>Welcome email is non-blocking</strong>
|
|
<p>
|
|
SMTP failures on the welcome email are caught and silently ignored so that a
|
|
misconfigured mail server cannot break user registration.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>Forgot-password always returns OK</strong>
|
|
<p>
|
|
The response is always <code>{ ok: true }</code> regardless of whether the email
|
|
exists, preventing account enumeration via timing or response differences.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>What's next</h2>
|
|
|
|
<div class="card">
|
|
<p>
|
|
Integrate Roject with rokojori-auth: remove the local user system from Roject,
|
|
add JWT verification middleware, redirect login/logout to
|
|
<code>account.rokojori.com</code>, and update all data references from local
|
|
user records to <code>userId</code> from the JWT payload.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
Roject — session history
|
|
</footer>
|
|
|
|
</div>
|
|
<script>var NAV_ROOT = '../../../../';</script>
|
|
<script src="../../../../_assets_/nav-data.js"></script>
|
|
<script src="../../../../_assets_/nav.js"></script>
|
|
</body>
|
|
</html>
|