+36
-33
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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*
|
||||
|
||||
Reference in New Issue
Block a user