Sunday, 13 July 2026

rokojori-auth — built and deployed

Full centralized auth service created from scratch, deployed to account.rokojori.com, and documented.

What we built

rokojori-auth — new standalone service

A complete auth service at c:\rokojori\projects\web-projects\rokojori-auth, same stack as Roject (Node.js, Express, ts-node, JSON file storage). Built entirely from scratch this session. Deployed to account.rokojori.com via nginx + systemd + Let's Encrypt on Server A, port 3001.

account.rokojori.com nginx + systemd port 3001

Auth endpoints

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 HttpOnly cookies on .rokojori.com and also returned in the response body for non-browser clients.

jsonwebtoken bcryptjs cookie-parser HttpOnly cookie .rokojori.com domain

Roles: user, admin, superadmin

Three built-in roles. superadmin is bootstrapped via INITIAL_SUPERADMIN_EMAIL in .env — 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.

Rate limiting

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). trust proxy enabled so real client IPs are seen behind nginx.

HTML pages

Five self-contained pages: login, register, profile, forgot-password, reset-password. Plain HTML with inline CSS and JS. Login and register accept a ?redirect= query param. Reset-password fetches the email for the token via GET /api/auth/reset-token-email 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).

Admin routes

GET /api/admin/users (admin + superadmin) lists all users without password hashes. PATCH /api/admin/users/:id/roles (superadmin only) sets roles on another user. Guards: cannot change own roles; user base role is always preserved.

Workspace documentation

Created workspace/ in the rokojori-auth repo with the same asset system as the Roject workspace (styles, nav, breadcrumb). A single workspace/index.html covers features, technical implementation, data model, JWT payload, deployment, planned work, and a reference to the Roject workspace for guides and actions.

Key decisions

Refresh token also set as HttpOnly cookie

Originally the refresh token was only returned in the response body. Added a refresh token cookie so browser clients can silently refresh via GET /api/auth/refresh-session?redirect=... without re-login. Non-browser clients continue to use the body token and POST /api/auth/refresh.

Superadmin cannot change their own roles

The PATCH /api/admin/users/:id/roles endpoint blocks changes to the calling user's own record to prevent accidental self-demotion or lockout.

Welcome email is non-blocking

SMTP failures on the welcome email are caught and silently ignored so that a misconfigured mail server cannot break user registration.

Forgot-password always returns OK

The response is always { ok: true } regardless of whether the email exists, preventing account enumeration via timing or response differences.

What's next

Integrate Roject with rokojori-auth: remove the local user system from Roject, add JWT verification middleware, redirect login/logout to account.rokojori.com, and update all data references from local user records to userId from the JWT payload.