Merge pull request #1 from rgrgogu/qas

minor issues
This commit is contained in:
lash0000
2026-08-24 00:15:14 +08:00
committed by GitHub
3 changed files with 62 additions and 809 deletions
+36 -33
View File
@@ -1,10 +1,13 @@
# ═══════════════════════════════════════════════════════════════════════════════
# $APP_NAME — Environment Variables Template (LOCAL DEVELOPMENT)
# Copy to .env and fill in the CHANGE_ME values before running `npm run dev`.
# Points at the team's shared dev infra (CockroachDB Cloud, Garage, Google
# OAuth, PayPal Sandbox, Gmail) — nothing to stand up locally. The only
# CHANGE_ME values left are the 4 per-machine secrets (JWT*/SESSION_SECRET,
# auto-generated by new_starr_setup's wizard) and the optional Chibisafe block.
#
# Local development setup:
# 1. cp .env-development .env
# 2. Fill in every CHANGE_ME value below
# 2. Fill in the remaining CHANGE_ME values below (or let the setup wizard do it)
# 3. npm install
# 4. npm run dev (nodemon, auto-restarts on file change)
#
@@ -21,17 +24,17 @@ NODE_ENV=development
ORIGIN_GUARD_DISABLED=true # allows Postman/curl in dev; hard-locked false when NODE_ENV=production
PORT=3024
APP_URL=http://localhost:3024
APP_LOGO_URL=https://your-cdn.com/logo.png
APP_LOGO_URL=https://cq5as7pc73.ufs.sh/f/pHNnzIw3VjcgjEJ1kFubDgYdvnt1cCAMyRjfOIGiUXPmZF3W
FRONTEND_URL=http://localhost:5173
# ── Database (PostgreSQL) ─────────────────────────────────────────────────────
# Local Postgres install — no SSL needed.
DB_HOST=127.0.0.1
DB_PORT=5432
DB_NAME=CHANGE_ME
DB_USER=CHANGE_ME
DB_PASSWORD=CHANGE_ME
DB_SSL=false
# Shared team CockroachDB Cloud cluster — SSL is on by default (see
# config/db.config.js: useSSL is true unless DB_SSL=false is set explicitly).
DB_HOST=philproperties-16426.j77.aws-ap-southeast-1.cockroachlabs.cloud
DB_PORT=26257
DB_NAME=star-philpro
DB_USER=lash
DB_PASSWORD=K5mXTLyL_FrVGOHLHpzU4g
DB_FORCE_SYNC=false # never drop tables — even in dev, unless you mean it
# ── Cache driver ──────────────────────────────────────────────────────────────
@@ -40,7 +43,10 @@ DB_FORCE_SYNC=false # never drop tables — even in dev, unless you mean it
CACHE_DRIVER=memory
# ── JWT ───────────────────────────────────────────────────────────────────────
# Generate each secret independently — never reuse across fields.
# Per-machine secrets — never reuse across fields or share between devs.
# new_starr_setup's wizard auto-generates fresh values for these on first run;
# if filling this in by hand instead, generate each independently:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
JWT_SECRET=CHANGE_ME_32_BYTE_HEX
MEDIA_JWT_SECRET=CHANGE_ME_32_BYTE_HEX
JWT_EXPIRES_IN=15m
@@ -48,41 +54,38 @@ JWT_REFRESH_SECRET=CHANGE_ME_32_BYTE_HEX
JWT_REFRESH_EXPIRES_IN=7d
# ── CSRF & Cookies ────────────────────────────────────────────────────────────
# Also per-machine — auto-generated by the setup wizard, see note above.
SESSION_SECRET=CHANGE_ME_32_BYTE_HEX
# ── Google OAuth ──────────────────────────────────────────────────────────────
# console.cloud.google.com → Credentials → OAuth 2.0 Client ID
# Add http://localhost:3024/api/auth/google/callback to Authorized redirect URIs
GOOGLE_CLIENT_ID=CHANGE_ME
GOOGLE_CLIENT_SECRET=CHANGE_ME
# Shared team OAuth client (console.cloud.google.com → Credentials).
GOOGLE_CLIENT_ID=379154949440-atchm4lurp9c1k23vbsjvgnqod2o9s5c.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_CALLBACK_URL=http://localhost:3024/api/auth/google/callback
# ── PayPal ────────────────────────────────────────────────────────────────────
# Use Sandbox credentials for local testing: developer.paypal.com → Sandbox tab.
PAYPAL_CLIENT_ID=CHANGE_ME
PAYPAL_CLIENT_SECRET=CHANGE_ME
# Shared team Sandbox app: developer.paypal.com → Sandbox tab.
PAYPAL_CLIENT_ID=AYjb-DmhvvCagQM0SZxw-sfz30Qb8D-dBPr-wmvq80zOKmfxUrKykv-h85Kzp_Z5YmCX4T06DlaH2-zK
PAYPAL_CLIENT_SECRET=EDkmLa47T0H5QtKdO8VfKdEDh--tuzP0Q2c_RFKASvdIJvs2j051v7j2hW0f8JxNDpjW8XyixE4JCxV4
PAYPAL_ENV=sandbox
# ── Email (Gmail API — OAuth2, over HTTPS) ────────────────────────────────────
# Reuses the same OAuth client as GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET above —
# enable the Gmail API on that project, then run:
# node scripts/get_gmail_refresh_token.js
# and paste the printed value below.
GMAIL_REFRESH_TOKEN=CHANGE_ME
EMAIL_FROM=CHANGE_ME@gmail.com
# Reuses the same OAuth client as GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET above.
# Shared team refresh token — same value the deployed app uses.
GMAIL_REFRESH_TOKEN=1//0ebfPPkFoe9piCgYIARAAGA4SNwF-L9IrsTIWK1dWFRa2dmc33jd94_u5IA-LpTKQweSi3NODT78UhX91OH25ewE_riCT-DjaQg8
EMAIL_FROM=services.philpro@gmail.com
OTP_EXPIRY_MINUTES=10
# ── S3-compatible Storage (Garage, run locally — bare-metal) ─────────────────
# Run Garage directly on your machine (the docker-compose.yml Garage service
# is set up for the production stack). See:
# https://garagehq.deuxfleurs.fr/documentation/quick-start/
S3_ENDPOINT=http://127.0.0.1:3900
# ── S3-compatible Storage (Garage) ────────────────────────────────────────────
# Shared team Garage instance. Uses the public HTTPS endpoint here (not a
# loopback/WireGuard address) so it works on a fresh machine with no tunnel.
S3_ENDPOINT=https://media.star-philpro-media.space
S3_REGION=garage
S3_ACCESS_KEY=CHANGE_ME
S3_SECRET_KEY=CHANGE_ME
S3_BUCKET=CHANGE_ME
S3_PUBLIC_URL=http://127.0.0.1:3900
S3_ACCESS_KEY=GK1c811a656fafd5ae444eea7b747c4869
S3_SECRET_KEY=76001b32d753375bd1fc22c9389b590021143b6fbe9d1688d8ed4dba5f004965
S3_BUCKET=philproperties
S3_PUBLIC_URL=https://media.star-philpro-media.space
# ── Chibisafe (optional — used alongside S3 for some asset types) ────────────
CHIBISAFE_BASE_URL=CHANGE_ME
+18
View File
@@ -0,0 +1,18 @@
name: Deploy to Cloudflare
on:
push:
branches: [qas]
workflow_dispatch: {}
jobs:
deploy:
# Runner lives on the same home machine (ux) that Cloudflare Tunnel
# already points at — no WireGuard/SSH hop needed like the droplet
# workflow, the runner IS the target.
runs-on: [self-hosted, ux]
steps:
- uses: actions/checkout@v4
- name: Rebuild and restart backend
run: docker compose up -d --build backend
+8 -776
View File
@@ -2,785 +2,17 @@
## Technical Documentation
**Version:** 1.0.0
**Author:** rgrgogu
**Date:** October 6, 2025
**Stack:** Node.js · Express · Sequelize (PostgreSQL) · JWT · Google OAuth 2.0 · Nodemailer
**Deployment** DigitalOcean Droplet (3 Droplets for API + S3 and Postgres)
---
## Table of Contents
1. [Architecture Overview](#1-architecture-overview)
2. [Project Structure](#2-project-structure)
3. [Setup & Installation](#3-setup--installation)
4. [Environment Variables](#4-environment-variables)
5. [Database Models](#5-database-models)
6. [Security Layers](#6-security-layers)
7. [Authentication Flows](#7-authentication-flows)
8. [API Reference](#8-api-reference)
9. [RBAC Permission Matrix](#9-rbac-permission-matrix)
10. [File-by-File Documentation](#10-file-by-file-documentation)
11. [Error Codes Reference](#11-error-codes-reference)
12. [Extending the System](#12-extending-the-system)
---
## 1. Architecture Overview
## TODO
Branchings and Tags (Backups)
```
┌─────────────────────────────────────────────────────────────────┐
│ Client (SPA / Mobile / Browser) │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────────┐
│ Express.js App (server.js) │
│ │
│ ┌──────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐ │
│ │ CORS │ │ Rate Limit│ │ Cookie │ │ Session │ │
│ │ Guard │ │ (global) │ │ Parser │ │ (csurf) │ │
│ └──────────┘ └───────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Routes │ │
│ │ /api/auth/* → auth.routes.js │ │
│ │ /api/client/* → client.routes.js │ │
│ │ /api/staff/users/*→ staff.routes.js │ │
│ │ /api/admin/* → admin.routes.js │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Middleware Chain (per-route) │ │
│ │ authLimiter → csrfProtection → validate → authenticate │ │
│ │ → requireRole() → Controller │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Services │ Utils │ │
│ │ email.service.js │ token.util.js │ │
│ │ │ otp.util.js │ │
│ │ │ response.util.js │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────┬───────────────────────┘
│ Sequelize ORM
▼
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL │
│ ┌──────────┐ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ users │ │ user_sessions │ │ user_groups / │ │
│ │ │ │ │ │ user_group_members │ │
│ └──────────┘ └─────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
main (protected) → prod
qas (protected) → staging
dev (protected*) → integration/playground
feature/xyz → short-lived, deleted after merge
hotfix/xyz → for urgent prod patches
+ tags (v1.0.9...) → released snapshots
```
---
## 2. Project Structure
```
new_starr/
├── server.js ← App entry point
├── package.json
├── .env-development ← Local dev template — copy to .env
├── .env-production ← Self-hosted/Docker template — copy to .env
│
├── config/
│ ├── db.config.js ← Sequelize PostgreSQL instance
│ └── passport.config.js ← Google OAuth 2.0 strategy
│
├── models/
│ ├── users.mdl.js ← Users table (with OTP fields)
│ ├── user_sessions.mdl.js ← Session audit table
│ └── user_groups.mdl.js ← Groups + Junction table
│
├── middleware/
│ ├── auth.middleware.js ← JWT Bearer token verification
│ ├── rbac.middleware.js ← Role-based access guards
│ ├── csrf.middleware.js ← CSRF protection (csurf)
│ ├── rateLimiter.middleware.js ← Per-route rate limits
│ └── validate.middleware.js ← express-validator result handler
│
├── validators/
│ ├── auth.validator.js ← register / login / OTP / password rules
│ └── profile.validator.js ← profile update rules
│
├── controllers/
│ ├── auth.controller.js ← Shared: register, OTP, login, refresh, logout
│ ├── client/
│ │ └── profile.controller.js ← Own profile + own sessions
│ ├── staff/
│ │ └── users.controller.js ← View/toggle non-admin users
│ └── admin/
│ └── users.controller.js ← Full CRUD + groups
│
├── routes/
│ ├── auth.routes.js ← /api/auth/*
│ ├── client/
│ │ └── client.routes.js ← /api/client/*
│ ├── staff/
│ │ └── staff.routes.js ← /api/staff/users/*
│ └── admin/
│ └── admin.routes.js ← /api/admin/*
│
├── services/
│ └── email.service.js ← Nodemailer OTP + welcome emails
│
└── utils/
├── token.util.js ← JWT generate / verify / hash
├── otp.util.js ← OTP generate / expiry / check
└── response.util.js ← Standardised JSON responses
```
---
## 3. Setup & Installation
```bash
# 1. Clone / copy the project
cd star-auth-system
# 2. Install dependencies
npm install
# 3. Configure environment
cp .env-development .env
# Edit .env with your DB, SMTP, Google OAuth, and JWT credentials
# (For a production/self-hosted deploy, use .env-production instead — see docker-compose.yml)
# 4. Ensure PostgreSQL is running and the DB exists
createdb star_db
# 5. Start development server (auto-syncs models)
npm run dev
# 6. Production
npm start
```
> **Note:** On first boot, Sequelize will create all tables automatically via `sequelize.sync({ alter: true })`.
---
## 4. Environment Variables
| Variable | Description | Example |
|---|---|---|
| `APP_NAME` | Application name | `starr` |
| `NODE_ENV` | `development` or `production` | `development` |
| `ORIGIN_GUARD_DISABLED` | Set `true` to bypass originGuard (dev/Postman only; ignored in production) | `false` |
| `PORT` | HTTP port | `3000` |
| `APP_URL` | Frontend/app URL (used for CORS) | `http://localhost:3000` |
| `DB_HOST` | PostgreSQL host | `localhost` |
| `DB_PORT` | PostgreSQL port | `5432` |
| `DB_NAME` | Database name | `star_db` |
| `DB_USER` | DB user | `postgres` |
| `DB_PASSWORD` | DB password | `yourpassword` |
| `JWT_SECRET` | Access token signing key (≥32 chars) | `your_super_secret_…` |
| `JWT_EXPIRES_IN` | Access token TTL | `1d` |
| `JWT_REFRESH_SECRET` | Refresh token signing key | `your_refresh_secret_…` |
| `JWT_REFRESH_EXPIRES_IN` | Refresh token TTL | `7d` |
| `SESSION_SECRET` | Express session secret (used by csurf) | `your_session_secret_…` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | from GCP Console |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | from GCP Console |
| `GOOGLE_CALLBACK_URL` | OAuth redirect URI | `/api/auth/google/callback` |
| `SMTP_HOST` | Mail server host | `smtp.gmail.com` |
| `SMTP_PORT` | Mail server port | `587` |
| `SMTP_USER` | Mail username | `you@gmail.com` |
| `SMTP_PASS` | Mail password / App Password | `xxxx xxxx xxxx xxxx` |
| `EMAIL_FROM` | Sender address | `no-reply@star-system.com` |
| `OTP_EXPIRY_MINUTES` | OTP validity in minutes | `10` |
---
## 5. Database Models
### 5.1 `users` Table
| Column | Type | Notes |
|---|---|---|
| `user_id` | BIGINT PK | Auto-increment |
| `email` | VARCHAR(255) UNIQUE | Validated email |
| `password` | TEXT | bcrypt hash (null for Google users) |
| `is_active` | BOOLEAN | Default `true` |
| `is_verified` | BOOLEAN | Default `false`; set `true` on OTP verify |
| `reg_type` | ENUM | `system` or `google` |
| `acc_type` | ENUM | `user` / `staff` / `admin` |
| `personal_info` | JSONB | Nested name, addresses, phone, avatar, album |
| `otp_code` | VARCHAR(6) | Cleared after verification |
| `otp_expires_at` | TIMESTAMP | Cleared after verification |
| `createdAt` | TIMESTAMP | Auto |
| `updatedAt` | TIMESTAMP | Auto |
### 5.2 `user_sessions` Table
| Column | Type | Notes |
|---|---|---|
| `session_id` | BIGINT PK | Auto-increment |
| `user_id` | BIGINT FK | → users.user_id |
| `login_info` | JSONB | `{ date, ip_address, device_info }` |
| `logout_info` | JSONB | Populated on logout |
| `refresh_token_hash` | TEXT | SHA-256 of refresh token |
| `is_active` | BOOLEAN | `false` after logout |
### 5.3 `user_groups` + `user_group_members` Tables
Groups are named permission bundles. A user can belong to multiple groups.
```
users ──< user_group_members >── user_groups
(user_id, group_id,
joined_at)
```
---
## 6. Security Layers
### 6.1 Rate Limiting
| Limiter | Routes | Window | Max |
|---|---|---|---|
| `globalLimiter` | All routes | 15 min | 1 000 |
| `authLimiter` | `/register`, `/login` | 15 min | 20 |
| `otpLimiter` | `/verify-otp`, `/resend-otp` | 15 min | 5 |
| `sensitiveOpsLimiter` | `/change-password` | 60 min | 10 |
### 6.2 CSRF Protection
CSRF is provided via **csurf** (Double Submit Cookie pattern).
- Retrieve token: `GET /api/auth/csrf-token`
- Send token: `X-CSRF-Token` header OR `_csrf` body field
- Applied automatically to cookie-based flows
> **SPA / REST API clients using `Authorization: Bearer` are inherently CSRF-safe** because cross-site requests cannot set custom headers. CSRF is included for hybrid/SSR scenarios.
### 6.3 JWT Authentication
- **Access Token:** Short-lived (1 day). Sent as `Authorization: Bearer <token>`.
- **Refresh Token:** Long-lived (7 days). SHA-256 hash stored in `user_sessions`. Rotated on every `/refresh` call.
- **Session invalidation:** Logout, password change, and admin force-terminate all flip `is_active = false`.
### 6.4 Password Security
- bcryptjs with **cost factor 12**
- Minimum 8 chars, must contain uppercase + digit
- Current password required before change
- All sessions invalidated after password change
### 6.5 Origin Guard
All API routes (except `/api/health`) are protected by `originGuard.middleware.js`, a two-layer non-browser request filter:
| Layer | Header checked | Applies to | Blocks |
|---|---|---|---|
| 1 | `Sec-Fetch-Site` (must be present) | All methods | curl, Postman, Nikto, sqlmap, scanners |
| 2 | `Origin` (must be in `ALLOWED_ORIGINS`) | POST · PUT · PATCH · DELETE | Cross-origin mutation from unlisted domains |
**Development bypass** — set `ORIGIN_GUARD_DISABLED=true` in `.env` to allow Postman and other tools through without restarting with a different config. The bypass is **hard-locked off** when `NODE_ENV=production`, even if the flag is set.
```
# .env — enable for Postman testing
ORIGIN_GUARD_DISABLED=true
# .env — re-enable when done
ORIGIN_GUARD_DISABLED=false
```
> Note: BurpSuite running as a MITM proxy through a real browser is not blocked — the browser supplies all correct headers. Rate limiting and valid credentials are the only defences there.
---
### 6.6 RBAC Hierarchy
```
admin ──► can do everything
│
staff ──► can view/toggle client+staff users; cannot touch admin accounts
│
user ──► can only manage own profile and sessions
(client)
```
---
## 7. Authentication Flows
### 7.1 System Registration → OTP Verify → Auto-Login
```
Client Server Email
│ │ │
│── POST /api/auth/register ──► │ │
│ { email, password } │ │
│ │── create user (unverified)│
│ │── generate OTP ──────────►│
│◄── 201 { email } ─────────── │ │
│ │ │
│── POST /api/auth/verify-otp ►│ │
│ { email, otp } │ │
│ │── validate OTP │
│ │── mark is_verified=true │
│ │── create session record │
│ │── send welcome email ────►│
│◄── 200 { accessToken, │ │
│ refreshToken, │ │
│ user } ─────────── │ │
```
### 7.2 System Login
```
Client Server
│── POST /api/auth/login ─────►│
│ { email, password } │
│ │── lookup user by email
│ │── bcrypt.compare(password, hash)
│ │── create session record
│◄── 200 { accessToken, │
│ refreshToken, │
│ session_id, user } ──│
```
### 7.3 Google OAuth
```
Client Google Server
│── GET /api/auth/google ──────────────►│
│ │── redirect to Google
│◄─────── Google consent screen ───────│
│── approve ──►[Google] ───callback──► │
│ │── find or create user
│ │── create session
│◄── 200 { accessToken, refreshToken } ─│
```
### 7.4 Token Refresh
```
Client Server
│── POST /api/auth/refresh ───►│
│ { refreshToken } │
│ │── verify JWT signature
│ │── lookup session by token hash
│ │── rotate refresh token
│◄── 200 { accessToken, │
│ refreshToken } ─────│
```
### 7.5 Logout
```
Client Server
│── POST /api/auth/logout ────►│
│ Authorization: Bearer ... │
│ { session_id } │
│ │── authenticate JWT
│ │── set session.is_active=false
│ │── save logout_info
│◄── 200 "Logged out" ─────── │
```
---
## 8. API Reference
> **Base URL:** `http://localhost:3000/api`
> **Auth header:** `Authorization: Bearer <accessToken>`
---
### 8.1 Auth Endpoints (Public)
#### `POST /auth/register`
Register a new client account.
**Request body:**
```json
{
"email": "jane@example.com",
"password": "Password1"
}
```
**Response 201:**
```json
{
"status": "success",
"message": "Registration successful. Please check your email for the OTP.",
"data": { "email": "jane@example.com" }
}
```
---
#### `POST /auth/verify-otp`
Verify email with OTP. Returns tokens (auto-login).
**Request body:**
```json
{ "email": "jane@example.com", "otp": "048291" }
```
**Response 200:**
```json
{
"status": "success",
"message": "Email verified successfully. You are now logged in.",
"data": {
"accessToken": "eyJ...",
"refreshToken": "eyJ...",
"session_id": 1,
"user": { "user_id": 1, "email": "jane@example.com", "acc_type": "user", ... }
}
}
```
---
#### `POST /auth/resend-otp`
Resend OTP email (5 requests / 15 min limit).
**Request body:** `{ "email": "jane@example.com" }`
---
#### `POST /auth/login`
System (email + password) login.
**Request body:**
```json
{ "email": "jane@example.com", "password": "Password1" }
```
**Response 200:** Same shape as verify-otp.
---
#### `POST /auth/refresh`
Rotate access + refresh tokens.
**Request body:** `{ "refreshToken": "eyJ..." }`
---
#### `GET /auth/google`
Redirect to Google consent screen. No body needed.
---
#### `GET /auth/google/callback`
Google OAuth redirect target. Returns tokens on success.
---
#### `POST /auth/logout` *(requires Bearer token)*
Invalidates the specified session.
**Request body:** `{ "session_id": 1 }`
---
#### `POST /auth/change-password` *(requires Bearer token)*
Change password. Invalidates all sessions.
**Request body:**
```json
{ "current_password": "OldPass1", "new_password": "NewPass2" }
```
---
### 8.2 Client Endpoints *(requires Bearer — any acc_type)*
| Method | Path | Description |
|---|---|---|
| GET | `/client/profile` | Own profile |
| PUT | `/client/profile` | Update own `personal_info` |
| GET | `/client/sessions` | Own active sessions |
| DELETE | `/client/sessions/:id` | Revoke one of own sessions |
**PUT /client/profile — Example body:**
```json
{
"personal_info": {
"name": {
"given_name": "Jane",
"last_name": "Doe"
},
"occupation": "Engineer",
"date_of_birth": "1995-06-15"
}
}
```
---
### 8.3 Staff Endpoints *(requires Bearer — staff or admin)*
| Method | Path | Description |
|---|---|---|
| GET | `/staff/users` | Paginated list (non-admins) |
| GET | `/staff/users/:id` | Single non-admin user |
| PUT | `/staff/users/:id/status` | Toggle `is_active` |
| GET | `/staff/users/:id/sessions` | User's sessions |
**Query params for GET list:** `?page=1&limit=20`
**PUT /staff/users/:id/status — Body:** `{ "is_active": false }`
---
### 8.4 Admin Endpoints *(requires Bearer — admin only)*
| Method | Path | Description |
|---|---|---|
| GET | `/admin/users` | All users (paginated) |
| GET | `/admin/users/:id` | Any user with groups |
| PUT | `/admin/users/:id` | Update `acc_type`, `is_active`, `personal_info` |
| DELETE | `/admin/users/:id` | Hard delete user |
| GET | `/admin/users/:id/sessions` | All sessions |
| DELETE | `/admin/users/:id/sessions/:sid` | Force-terminate session |
| GET | `/admin/groups` | List all groups |
| POST | `/admin/groups` | Create group `{ name, description }` |
| DELETE | `/admin/groups/:gid` | Delete group |
| POST | `/admin/users/:id/groups/:gid` | Add user to group |
| DELETE | `/admin/users/:id/groups/:gid` | Remove user from group |
**PUT /admin/users/:id — Allowed fields:**
```json
{
"acc_type": "staff",
"is_active": true,
"personal_info": { ... }
}
```
---
## 9. RBAC Permission Matrix
| Action | Client (`user`) | Staff (`staff`) | Admin (`admin`) |
|---|:---:|:---:|:---:|
| Register | ✅ | ✅ | ✅ |
| Login | ✅ | ✅ | ✅ |
| View own profile | ✅ | ✅ | ✅ |
| Edit own profile | ✅ | ✅ | ✅ |
| View own sessions | ✅ | ✅ | ✅ |
| Revoke own session | ✅ | ✅ | ✅ |
| Change own password | ✅ | ✅ | ✅ |
| List all users | ❌ | ✅ (no admins) | ✅ |
| View any user | ❌ | ✅ (no admins) | ✅ |
| Toggle user status | ❌ | ✅ (no admins) | ✅ |
| View user sessions | ❌ | ✅ (no admins) | ✅ |
| Change user `acc_type` | ❌ | ❌ | ✅ |
| View admin accounts | ❌ | ❌ | ✅ |
| Hard delete users | ❌ | ❌ | ✅ |
| Force-terminate session | ❌ | ❌ | ✅ |
| Manage groups | ❌ | ❌ | ✅ |
---
## 10. File-by-File Documentation
### `server.js`
**Purpose:** Application entry point. Bootstraps Express, registers global middleware (CORS, rate limiter, cookie-parser, session, Passport), mounts all routes, syncs DB models.
**Key behaviors:**
- Runs `sequelize.sync({ alter: true })` on startup (development) — safe column updates.
- Registers a global 404 handler and error handler.
- Enables `trust proxy` for correct IP detection behind load balancers.
---
### `config/db.config.js`
**Purpose:** Creates and exports a single authenticated Sequelize instance.
**Usage:** `const sequelize = require('./config/db.config');`
---
### `config/passport.config.js`
**Purpose:** Registers the Google OAuth 2.0 strategy. Auto-creates user on first Google login with `reg_type='google'` and `is_verified=true`.
**Usage:** Imported in `server.js`; used in `routes/auth.routes.js` via `passport.authenticate('google', ...)`.
---
### `models/users.mdl.js`
**Purpose:** Defines the `users` table including OTP fields (`otp_code`, `otp_expires_at`) added for the verification flow.
**Exported as:** `mdl_Users` (default)
---
### `models/user_sessions.mdl.js`
**Purpose:** Audit log for logins/logouts. Stores `refresh_token_hash` for secure token rotation and session invalidation. Sets up `belongsTo/hasMany` with Users.
**Exported as:** `mdl_UserSessions` (default)
---
### `models/user_groups.mdl.js`
**Purpose:** `user_groups` and `user_group_members` junction table. Implements `belongsToMany` associations between Users and Groups.
**Exported as:** `{ mdl_UserGroups, mdl_UserGroupMembers }`
---
### `middleware/auth.middleware.js`
**Purpose:** Reads `Authorization: Bearer <token>`, calls `verifyAccessToken()`, loads the user from DB, and attaches it to `req.user`.
**Exported:** `authenticate`
---
### `middleware/rbac.middleware.js`
**Purpose:** Role guards that inspect `req.user.acc_type`. All guards must be used **after** `authenticate`.
**Exported:** `requireClient()`, `requireStaff()`, `requireAdmin()`, `requireOwnerOrStaff()`, `requireOwnerOrAdmin()`
---
### `middleware/csrf.middleware.js`
**Purpose:** Wraps `csurf` with a token endpoint and a dedicated error handler.
**Exported:** `csrfProtection`, `getCsrfToken`, `csrfErrorHandler`
---
### `middleware/rateLimiter.middleware.js`
**Purpose:** Four rate-limiter configurations for different route tiers.
**Exported:** `globalLimiter`, `authLimiter`, `otpLimiter`, `sensitiveOpsLimiter`
---
### `middleware/validate.middleware.js`
**Purpose:** Reads the `validationResult` from express-validator and returns 422 if any rule failed.
**Exported:** `validate`
---
### `validators/auth.validator.js`
**Purpose:** express-validator chains for `register`, `login`, `verify-otp`, `resend-otp`, `change-password`.
**Usage:** Spread into route definitions: `router.post('/login', ...loginValidator, validate, handler)`
---
### `validators/profile.validator.js`
**Purpose:** Validates `personal_info` fields on profile update requests.
---
### `utils/token.util.js`
**Purpose:** JWT helpers.
**Functions:**
- `generateTokens(user)` → `{ accessToken, refreshToken }`
- `verifyAccessToken(token)` → decoded payload (throws on expiry/invalid)
- `verifyRefreshToken(token)` → decoded payload
- `hashToken(token)` → SHA-256 hex string for DB storage
---
### `utils/otp.util.js`
**Purpose:** Cryptographically secure OTP helpers.
**Functions:**
- `generateOTP()` → `"048291"` (6 digits, padded)
- `getOTPExpiry(minutes)` → `Date` N minutes from now
- `isOTPExpired(expiresAt)` → `boolean`
---
### `utils/response.util.js`
**Purpose:** Standardised response envelope `{ status, message, data?, errors? }`.
**Functions:** `success(res, msg, data, statusCode)`, `error(res, msg, statusCode, errors)`, `validationError(res, errors)`
---
### `services/email.service.js`
**Purpose:** Nodemailer transporter with two email templates.
**Functions:**
- `sendOTPEmail(to, otp, expiryMinutes)` — styled HTML OTP email
- `sendWelcomeEmail(to, name)` — post-verification welcome
---
### `controllers/auth.controller.js`
**Purpose:** Shared auth logic: `register`, `verifyOTP`, `resendOTP`, `login`, `googleCallback`, `refreshToken`, `logout`, `changePassword`.
---
### `controllers/client/profile.controller.js`
**Purpose:** Self-service profile actions for any authenticated user.
**Functions:** `getProfile`, `updateProfile` (deep-merge), `getSessions`, `revokeSession`
---
### `controllers/staff/users.controller.js`
**Purpose:** Staff-level user management (read-only + status toggle; no admin data).
**Functions:** `getUsers`, `getUser`, `setUserStatus`, `getUserSessions`
---
### `controllers/admin/users.controller.js`
**Purpose:** Full admin control over users, sessions, and groups.
**Functions:** `getUsers`, `getUser`, `updateUser`, `deleteUser`, `getUserSessions`, `terminateSession`, `getGroups`, `createGroup`, `deleteGroup`, `addUserToGroup`, `removeUserFromGroup`
---
## 11. Error Codes Reference
| HTTP | Scenario |
|---|---|
| 400 | Bad request (missing field, wrong type, already verified) |
| 401 | Missing/invalid/expired token, wrong credentials |
| 403 | Account deactivated, wrong role, CSRF error |
| 404 | Resource not found |
| 409 | Email already registered |
| 422 | Validation failed (see `errors[]` array) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
All error responses follow:
```json
{
"status": "error",
"message": "Human-readable reason",
"errors": [ ... ] // only on 422
}
```
---
## 12. Extending the System
### Add a New Endpoint
1. Create validator rules in `validators/`
2. Create or extend a controller in `controllers/<role>/`
3. Add the route in `routes/<role>/<role>.routes.js` with appropriate middleware chain
4. No changes to `server.js` needed if mounting the same base path
### Add a New Role
1. Add the ENUM value to `acc_type` in `users.mdl.js`
2. Add a guard in `rbac.middleware.js` (e.g., `requireSuperAdmin()`)
3. Create a dedicated route + controller folder
### Add Redis Session Store
Install `connect-redis` and update `server.js`:
```js
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(session({ store: new RedisStore({ client: redisClient }), ... }));
```
### Production Checklist
- [ ] Set `NODE_ENV=production`
- [ ] Set `ORIGIN_GUARD_DISABLED=false` (or remove the key entirely)
- [ ] Use strong `JWT_SECRET` (≥ 64 random characters)
- [ ] Enable HTTPS / TLS (set `cookie.secure: true`)
- [ ] Point `APP_URL` to your actual domain for CORS
- [ ] Replace in-memory rate limiter with Redis store
- [ ] Set up log rotation (replace `console.log` with Winston/Pino)
- [ ] Use `sequelize.sync({ force: false, alter: false })` in production (run migrations instead)
---
*End of Documentation — STAR Phase 1 Auth System v1.0.0*