Project Documentation

rokojori-auth

Centralized auth service for all rokojori projects, deployed at account.rokojori.com. Developer reference for human and agent contributors.

What it is

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.

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 HttpOnly cookies on .rokojori.com, making them automatically available to all subdomains. Non-browser clients (Electron, CLI, Godot) receive the tokens in the response body and manage them locally.

Features

Authentication

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 .rokojori.com. The refresh token is also returned in the response body for non-browser clients.

POST /api/auth/register POST /api/auth/login POST /api/auth/logout

Token refresh

Browser clients whose access token has expired are redirected to GET /api/auth/refresh-session?redirect=.... The server reads the refresh token cookie, rotates both tokens, and redirects back. Non-browser clients call POST /api/auth/refresh with the refresh token in the request body.

GET /api/auth/refresh-session POST /api/auth/refresh

Password reset

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 { ok: true } regardless of whether the email exists, to avoid leaking account information.

POST /api/auth/forgot-password POST /api/auth/reset-password

Profile and account management

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.

GET /api/auth/me PATCH /api/auth/me/settings POST /api/auth/me/password DELETE /api/auth/me

Roles

Three built-in roles: user (default on registration), admin (enhanced profile, user list), and superadmin (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 userId.

Bootstrap: set INITIAL_SUPERADMIN_EMAIL in .env. The first registration with that email is automatically promoted to superadmin, provided no superadmin exists yet.

Products

Each user record has a products array. Products can be assigned manually via PATCH /api/admin/users/:id/products (superadmin only) or automatically when a purchase is confirmed (planned: Polar webhook). Apps read the product list from the JWT at runtime — no round-trip to rokojori-auth needed.

Server-to-server lookup

Other rokojori services can resolve an email address to a user ID without a user JWT — useful for migrating member records from email-based to ID-based storage. The caller authenticates with a shared SERVICE_SECRET environment variable, not a user token. Returns { id, email } or 404.

POST /api/auth/lookup-email Authorization: Bearer SERVICE_SECRET

Rate limiting

In-memory per-IP rate limits on all sensitive endpoints. Windows and thresholds:

  • Login — 10 attempts / 15 min; delay of 3 s added after attempt 5
  • Register — 5 attempts / hour, hard block
  • Forgot-password — 20 attempts / 20 min; escalating delay 5 s → 15 s → 30 s

HTML pages

Five self-contained pages served at account.rokojori.com, shared across all rokojori apps. All pages are plain HTML with inline CSS and JavaScript — no framework, no build step.

  • login.html — accepts ?redirect= query param
  • register.html — accepts ?redirect= query param
  • profile.html — change password, delete account; admin/superadmin see user list
  • forgot-password.html
  • reset-password.html — reads ?token= from URL; includes hidden email field for browser password-manager integration

Technical Implementation

Stack

Node.js + Express, TypeScript compiled on the fly with ts-node. No database — all data lives as JSON files in build/data/ (auto-created on first run). All entity IDs are UUIDs via crypto.randomUUID(). Email via Nodemailer (SMTPEmailSender), configured entirely through environment variables.

Node.js Express ts-node jsonwebtoken bcryptjs nodemailer cookie-parser

File structure

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)
        requireAccess.ts — flexible role+product access rules (copy into client services)
      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

Data model

// 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": "..." }

JWT payload

Access tokens are signed HS256 with JWT_SECRET (shared across all services). They carry everything a client app needs to know — no round-trip to rokojori-auth required at runtime.

{
  "userId":   "uuid",
  "email":    "user@example.com",
  "roles":    ["user"],
  "products": ["roject-pro"],
  "settings": { "theme": "dark" }
}

Deployment

Running on Server A at account.rokojori.com. nginx handles TLS termination (Let's Encrypt) and reverse-proxies to the Node.js process on port 3001, managed by a systemd service (rokojori-auth.service). trust proxy is enabled so the rate limiter sees real client IPs.

account.rokojori.com nginx + Let's Encrypt systemd port 3001

Per-service access control

Design

Each rokojori service defines an access rule list — a small array that declares who is allowed in. The JWT already carries both roles and products, so no round-trip to rokojori-auth is needed at request time.

superadmin is implicitly allowed on every service. No service needs to include it in its rule list.

AccessRule type

type AccessRule = {
  role: string;     // required — user must have this role
  product?: string; // optional — user must also have this product
};

Multiple rules are OR-combined: access is granted if any rule matches. Within a single rule, role and product are AND-combined.

Example — styles.rokojori.com

// Grant access to:
//   • any admin (role alone is sufficient)
//   • any user who has the "styles" product
//   • any user who has the "premium" product
const rules: AccessRule[] = [
  { role: 'admin' },
  { role: 'user', product: 'styles' },
  { role: 'user', product: 'premium' },
];

requireAccess middleware

// source/server/middleware/requireAccess.ts

import { Request, Response, NextFunction } from 'express';

type AccessRule = { role: string; product?: string };

export function requireAccess( rules: AccessRule[] )
{
  return ( req: Request, res: Response, next: NextFunction ): void =>
  {
    const auth = req.auth;
    if ( !auth ) { res.status( 401 ).json( { error: 'Not authenticated' } ); return; }

    if ( auth.roles.includes( 'superadmin' ) ) { next(); return; }

    const allowed = rules.some( rule =>
      auth.roles.includes( rule.role ) &&
      ( !rule.product || auth.products.includes( rule.product ) )
    );

    if ( allowed ) { next(); return; }
    res.status( 403 ).json( { error: 'Forbidden' } );
  };
}

Use after requireAuth. For browser pages that should redirect rather than return JSON, check req.accepts('html') in the 401/403 branches and redirect to account.rokojori.com/login?redirect=<current-url> or back to /.

source/server/middleware/requireAccess.ts

Configuration per service

Each service defines its own rules inline where the router is mounted. Copy requireAccess.ts into the service's middleware folder — it has no dependencies beyond the shared AuthPayload type from requireAuth.ts.

Admin API for product management

Superadmins can set the full product list on any user via the admin route. The body is an array of product ID strings; the server records acquiredAt (now) and source: "manual" for each. The updated list is reflected in the user's next JWT.

PATCH /api/admin/users/:id/products superadmin only

Planned

Polar payment webhook

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.

POST /api/webhooks/polar signature verification products array

Security hardening pass

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.

Related — Roject

Roject is the first client app of rokojori-auth. It is a self-hosted, agent-based IDE deployed at roject.rokojori.com. After the auth integration is complete, Roject will drop its own user system entirely and become a JWT-validating client: it redirects to account.rokojori.com/login for login, reads the shared cookie, and verifies the JWT locally on every request.

For deeper context on the overall ecosystem, coding conventions, and how to work with the codebase, consult the Roject workspace documentation. 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.

roject.rokojori.com JWT client shared .rokojori.com cookie