npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

codewithrocky

v1.0.4

Published

CLI to scaffold production-ready Express + TypeScript + MongoDB backends with RBAC, JWT auth, refresh tokens, and CRUD generators.

Downloads

45

Readme


  • Full auth system — registration, login, OTP, password reset, refresh tokens (one command)
  • 3 login types — email/password, Google OAuth, Apple Sign In
  • Role-based access control — 3 default roles, field-level permissions, scope-based filtering (built in)
  • CRUD generator — 12 API endpoints per module with search, filter, sort, pagination (one command)
npx codewithrocky

Table of Contents


What You Get

| Feature | What It Does | |---------|-------------| | Auth System | Register, login, OTP verification, password reset — 11 endpoints | | 3 Login Types | Email/password, Google OAuth, Apple Sign In — each with different flows | | RBAC | Super admin, admin, user — with field-level and scope-level permissions | | CRUD Generator | Generate a full module (model + controller + routes) — 12 endpoints each | | Refresh Tokens | Automatic rotation, theft detection, multi-device session management | | Profile Management | Update profile, change password, change email, deactivate/delete account | | Docker | Dev mode with hot reload, production mode, one-command cleanup | | Email Service | SMTP-based OTP delivery with graceful fallback | | Cron Service | Automatic cleanup of expired OTPs and refresh tokens |

Zero config. Zero boilerplate. From install to first API call in under 2 minutes.


Quick Start

Prerequisites

Step 1: Create a project

npx codewithrocky

The interactive CLI starts. Type create:

codewithrocky > create

You'll be asked for a project name:

? Project name: my-api
✔ Project created at ./my-api
✔ Dependencies installed

Step 2: Add the auth system

codewithrocky > add auth
✔ Auth system installed (11 endpoints)
✔ RBAC system installed
✔ Profile system installed (7 endpoints)

This installs authentication, refresh tokens, RBAC roles, and profile management in one command.

Step 3: Generate a CRUD module

codewithrocky > add crud products

The interactive builder walks you through your schema:

? Add a field: title
? Field type: String
? Required? Yes
? Add another field? Yes

? Add a field: price
? Field type: Decimal128
? Required? Yes
? Add another field? Yes

? Add a field: category
? Field type: Enum
? Enum values (comma-separated): electronics,clothing,food
? Required? Yes
? Add another field? No

✔ CRUD module "products" created (12 endpoints)

Step 4: Start the server

codewithrocky > start

Or with Docker:

codewithrocky > local

Step 5: Make your first API call

You can test the API using curl (terminal), Postman, Thunder Client (VS Code extension), or any HTTP client.

Register a user:

curl -X POST http://localhost:3100/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "mypassword123",
    "login_type": "manual_login",
    "first_name": "John",
    "last_name": "Doe"
  }'

Response:

{
  "status": 1,
  "message": "OTP sent",
  "data": {
    "email": "[email protected]"
  }
}

Where's the OTP? If you haven't configured SMTP email, the OTP is printed in your terminal/server console. Look for a line like [OTP] [email protected] → 482917. Use that code in the next step.

Confirm the OTP:

curl -X POST http://localhost:3100/api/auth/confirm-otp \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "otp": "482917",
    "password": "mypassword123",
    "first_name": "John",
    "last_name": "Doe"
  }'

Response:

{
  "status": 1,
  "message": "Account created",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "d8f2a1b4e6c9...",
    "role": "super_admin",
    "roles": ["super_admin"],
    "email": "[email protected]",
    "created_at": "2026-03-19T10:30:00.000Z"
  }
}

Save the token value. You'll need it for every other API call. Send it in the Authorization header like this: Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

First user = super_admin. The very first user to register automatically gets the super_admin role. All subsequent users get the user role.

Create a product (using your token):

curl -X POST http://localhost:3100/api/products/create \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "title": "Wireless Headphones",
    "price": "79.99",
    "category": "electronics"
  }'

That's it. You have a working API with auth, RBAC, and CRUD — ready for production.


Key Terms

| Term | What It Means | |------|--------------| | JWT | JSON Web Token — a signed string that proves who you are. Sent in every API request as Authorization: Bearer <token> | | OTP | One-Time Password — a 6-digit code sent to your email to verify your identity | | RBAC | Role-Based Access Control — different users have different permissions based on their role | | SMTP | Simple Mail Transfer Protocol — the standard for sending emails. Configure it in .env to send real OTP emails | | Refresh Token | A long-lived token used to get a new access token when the old one expires, without logging in again |


Login Types

The auth system supports 3 login types. Each has a different flow and requires different fields.

Overview

| Login Type | Required Fields | Flow | OTP Required? | |-----------|----------------|------|--------------| | manual_login | email, password | Register → OTP → Confirm OTP → Logged in | Yes | | google_login | social_account_id, platform | Register → Logged in immediately | No | | apple_login | social_account_id, platform | Register → Logged in immediately | No |

Manual Login (email + password)

This is the default. The user registers with email and password, receives an OTP for verification, then confirms it.

Register (email + password)
    ↓
OTP sent to email (check terminal if SMTP not configured)
    ↓
Confirm OTP (email + otp + password)
    ↓
Account created → access_token + refresh_token returned

Google Login

The user signs in with Google on your frontend. Your frontend gets an ID token from Google's SDK. You send that token to your backend.

Frontend: User signs in with Google → gets ID token
    ↓
Backend: POST /register or /login with social_account_id = ID token
    ↓
Account created immediately → access_token + refresh_token returned

Required fields:

  • login_type: "google_login"
  • social_account_id: the ID token from Google Sign-In SDK
  • platform: "web", "android", or "ios" (must match your Google Client ID)

Apple Login

The user signs in with Apple on your frontend. Your frontend gets an identity token from Apple Sign In SDK. You send that token to your backend.

Frontend: User signs in with Apple → gets identity token
    ↓
Backend: POST /register or /login with social_account_id = identity token
    ↓
Account created immediately → access_token + refresh_token returned

Required fields:

  • login_type: "apple_login"
  • social_account_id: the identity token from Apple Sign In SDK
  • platform: "ios" (Apple Sign In is iOS only)

What social_account_id Actually Is

This is not a user ID you create. It's a token that your frontend gets from the Google/Apple SDK after the user signs in on the client side. Your backend verifies this token with Google/Apple servers to confirm the user's identity.

| Login Type | What social_account_id Contains | |---|---| | Google (web) | OAuth2 access token from Google Sign-In | | Google (android/ios) | ID token (JWT) from Google Sign-In | | Apple | Identity token (JWT) from Apple Sign In |

What platform Does

The platform field tells the backend which Google Client ID to verify against. Each platform (web, android, ios) uses a different Client ID in the Google Developer Console.

For Apple, always use "ios".


Social Login Setup

Google Login — Step by Step

Step 1: Enable in config

Open src/utility/local/config/auth_config.ts and set:

social_login: {
  google: { enabled: true },  // ← change from false to true
}

Step 2: Set environment variables

Add your Google Client IDs to .env:

GOOGLE_CLIENT_ID_WEB=your-google-client-id-for-web.apps.googleusercontent.com
GOOGLE_CLIENT_ID_ANDROID=your-google-client-id-for-android.apps.googleusercontent.com
GOOGLE_CLIENT_ID_IOS=your-google-client-id-for-ios.apps.googleusercontent.com

Get these from Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client IDs.

Step 3: Frontend integration

Your frontend app uses the Google Sign-In SDK to let the user sign in. After sign-in, the SDK gives you a token.

Step 4: Call the API

Register:

curl -X POST http://localhost:3100/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "google_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "web"
  }'

Login (for returning users):

curl -X POST http://localhost:3100/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "google_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "web"
  }'

Step 5: Restart your server for config changes to take effect.

Apple Login — Step by Step

Step 1: Enable in config

social_login: {
  apple: { enabled: true },  // ← change from false to true
}

Step 2: No environment variables needed

Apple Sign In uses public JWKS keys from Apple's servers. No client IDs required on the backend.

Step 3: Frontend integration

Your iOS app uses Apple Sign In SDK. After sign-in, the SDK gives you an identity token.

Step 4: Call the API

curl -X POST http://localhost:3100/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "apple_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "ios",
    "first_name": "John",
    "last_name": "Doe"
  }'

Note: Apple only sends the user's name on the first sign-in. If you don't capture first_name and last_name in the register call, they'll be empty.

Step 5: Restart your server for config changes to take effect.


Social Login Not Working? Checklist

If you're passing the right params but social login isn't working, check these in order:

| # | Check | How to Fix | |---|-------|-----------| | 1 | Is it enabled in config? | Open auth_config.tssocial_login.google.enabled must be true | | 2 | Are env vars set? (Google only) | .env must have GOOGLE_CLIENT_ID_WEB (or ANDROID/IOS depending on platform) | | 3 | Is social_account_id in your request? | This is the token from Google/Apple SDK — not an email or user ID | | 4 | Is platform in your request? | Must be "web", "android", or "ios" | | 5 | Does platform match your Client ID? | If you're testing from web, use GOOGLE_CLIENT_ID_WEB and platform: "web" | | 6 | Is the token expired? | Google/Apple tokens are short-lived. Get a fresh one from the frontend | | 7 | Did you restart the server after config changes? | Config is loaded on startup. Changes need a restart |


CLI Commands

The CLI is interactive. Run npx codewithrocky and type commands inside the shell.

Project Commands

| Command | Description | |---------|-------------| | create | Scaffold a new Express + TypeScript + MongoDB project | | doctor | Check system dependencies (Node, MongoDB, Docker) | | status | Show which features are installed in the current project | | help | Show all available commands |

Feature Commands

| Command | Description | |---------|-------------| | add auth | Install auth system (11 endpoints + RBAC + refresh tokens + profile) | | add crud <name> | Generate a CRUD module (12 endpoints) |

Note: add auth installs the profile system automatically. You don't need a separate command for profile.

Run Commands

| Command | Description | |---------|-------------| | start | Start the project (non-Docker) | | local | Start in Docker dev mode (hot reload, volumes mounted) | | server | Start in Docker production mode | | stop | Stop the running project / containers | | build | Build TypeScript to JavaScript |

Help Guides

| Command | Description | |---------|-------------| | crud help | CRUD generator reference guide | | auth help | Auth system reference guide | | rbac help | RBAC system reference guide |

npm Scripts (run from terminal, not the CLI)

These are available as npm scripts in your generated project:

| Script | Description | |--------|-------------| | npm run server:detach | Start Docker production in background | | npm run docker:build | Build Docker image only | | npm run docker:clean | Remove Docker cache + volumes |


Auth System

The auth system gives you 11 endpoints out of the box. All endpoints are at /api/auth/*.

Flow Overview

Manual Login (email + password):

Register → OTP sent → Confirm OTP → Logged in (token + refresh_token)
                                          ↓
                                    Use access_token for API calls
                                          ↓
                                    Token expires → Use refresh_token → New tokens

Social Login (Google / Apple):

Register/Login → Logged in immediately (token + refresh_token)
                       ↓
                 Use access_token for API calls
                       ↓
                 Token expires → Use refresh_token → New tokens

Register a new user. The flow differs based on login_type.

Manual Login — sends OTP to email:

curl -X POST http://localhost:3100/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "manual_login",
    "email": "[email protected]",
    "password": "mypassword123",
    "first_name": "John",
    "last_name": "Doe"
  }'

Google Login — creates account immediately:

curl -X POST http://localhost:3100/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "google_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "web"
  }'

Apple Login — creates account immediately:

curl -X POST http://localhost:3100/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "apple_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "ios",
    "first_name": "John",
    "last_name": "Doe"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | login_type | string | Yes | "manual_login", "google_login", or "apple_login" | | email | string | Manual only | User's email address | | password | string | Manual only | Minimum 6 characters (configurable) | | social_account_id | string | Social only | ID token from Google/Apple SDK | | platform | string | Social only | "web", "android", or "ios" | | first_name | string | No | User's first name | | last_name | string | No | User's last name | | email_notifications | boolean | No | Enable email notifications (default: true) | | time_zone | string | No | IANA timezone string (e.g., "America/New_York") |

Response — Manual Login (200):

{
  "status": 1,
  "message": "OTP sent",
  "data": {
    "email": "[email protected]"
  }
}

After this, call /confirm-otp with the OTP code. Check your terminal for the OTP if SMTP is not configured.

Response — Social Login (200):

{
  "status": 1,
  "message": "Account created",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]",
    "login_type": "google_login",
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "d8f2a1b4e6c9...",
    "role": "user",
    "roles": ["user"],
    "created_at": "2026-03-19T10:30:00.000Z"
  }
}

Note: The first user to register (any login type) becomes the super_admin. All subsequent users get the user role.


Confirm the OTP sent during manual registration. This completes the registration and logs the user in.

This endpoint is only for manual_login. Social login users are logged in immediately on register — they don't need OTP confirmation.

Request:

curl -X POST http://localhost:3100/api/auth/confirm-otp \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "otp": "482917",
    "password": "mypassword123",
    "first_name": "John",
    "last_name": "Doe"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Same email used in register | | otp | string | Yes | 6-digit OTP from email (or terminal) | | password | string | Yes | Same password used in register | | first_name | string | No | User's first name | | last_name | string | No | User's last name | | email_notifications | boolean | No | Enable email notifications (default: true) | | time_zone | string | No | IANA timezone string |

Response (200):

{
  "status": 1,
  "message": "Account created",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "d8f2a1b4e6c9...",
    "role": "user",
    "roles": ["user"],
    "email": "[email protected]",
    "created_at": "2026-03-19T10:30:00.000Z"
  }
}

Log in with an existing account. Works with all 3 login types.

Manual Login:

curl -X POST http://localhost:3100/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "manual_login",
    "email": "[email protected]",
    "password": "mypassword123"
  }'

Google Login:

curl -X POST http://localhost:3100/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "google_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "web"
  }'

Apple Login:

curl -X POST http://localhost:3100/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "apple_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "ios"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | login_type | string | Yes | "manual_login", "google_login", or "apple_login" | | email | string | Manual only | Registered email | | password | string | Manual only | Account password | | social_account_id | string | Social only | Fresh ID token from Google/Apple SDK | | platform | string | Social only | "web", "android", or "ios" |

Response (200):

{
  "status": 1,
  "message": "Login successful",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]",
    "login_type": "manual_login",
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "d8f2a1b4e6c9...",
    "role": "user",
    "roles": ["user"],
    "created_at": "2026-03-19T10:30:00.000Z"
  }
}

Blocked account responses:

If the account is suspended or deleted, login returns a special status code instead of the normal response. See Account Status & Lifecycle and Status Codes for details.


Send an OTP for password reset.

Manual login only. Social login users (Google/Apple) don't have passwords — they authenticate through their provider. If a social login user calls this endpoint, it returns an error.

Request:

curl -X POST http://localhost:3100/api/auth/send-otp \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "auth_type": "forget_password"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Registered email | | auth_type | string | Yes | Must be "forget_password" |

Response (200):

{
  "status": 1,
  "message": "OTP sent",
  "data": {
    "email": "[email protected]"
  }
}

Verify the OTP sent for password reset. This does not reset the password — it confirms the OTP is valid.

Request:

curl -X POST http://localhost:3100/api/auth/verify-forget-password-otp \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "otp": "593817"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Email that received the OTP | | otp | string | Yes | 6-digit OTP from email |

Response (200):

{
  "status": 1,
  "message": "OTP verified",
  "data": {
    "email": "[email protected]",
    "otp_verified": true
  }
}

Reset the password after OTP verification. This kills all existing refresh tokens for the user (logged out on every device).

Manual login only. Social login users cannot reset passwords.

Request:

curl -X POST http://localhost:3100/api/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "otp": "593817",
    "new_password": "newsecurepassword",
    "confirm_password": "newsecurepassword"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Registered email | | otp | string | Yes | Verified OTP | | new_password | string | Yes | New password (min 6 chars) | | confirm_password | string | Yes | Must match new_password |

Response (200):

{
  "status": 1,
  "message": "Password reset",
  "data": null
}

Security: Password reset kills ALL refresh tokens. The user must log in again on every device.


Check if a user exists. Useful for pre-validation before registration. Works with all 3 login types.

Manual Login:

curl -X POST http://localhost:3100/api/auth/check-user-exists \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "manual_login",
    "email": "[email protected]"
  }'

Google/Apple Login:

curl -X POST http://localhost:3100/api/auth/check-user-exists \
  -H "Content-Type: application/json" \
  -d '{
    "login_type": "google_login",
    "social_account_id": "eyJhbGciOiJSUzI1NiIs...",
    "platform": "web"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | login_type | string | Yes | "manual_login", "google_login", or "apple_login" | | email | string | Manual only | Email to check | | social_account_id | string | Social only | ID token from Google/Apple SDK | | platform | string | Social only | "web", "android", or "ios" |

Response — User Found (200):

{
  "status": 1,
  "message": "User found",
  "data": {
    "user_exists": true,
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]",
    "login_type": "manual_login"
  }
}

Response — User Not Found (200):

{
  "status": 1,
  "message": "User not found",
  "data": {
    "user_exists": false
  }
}

Get a new access token using a refresh token. No authorization header needed.

Request:

curl -X POST http://localhost:3100/api/auth/refresh-token \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "d8f2a1b4e6c9..."
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | refresh_token | string | Yes | Valid refresh token from login |

Response (200):

{
  "status": 1,
  "message": "Token refreshed",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "a1b2c3d4e5f6...",
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]",
    "role": "user",
    "roles": ["user"]
  }
}

Important: Each refresh token can only be used once. After use, a new refresh token is returned. Save it — the old one is dead.

Error Responses:

| Status | error_code | Meaning | |--------|-----------|---------| | 401 | REFRESH_TOKEN_INVALID | Token not found, expired, or invalid | | 401 | TOKEN_REUSE_DETECTED | This token was already used — possible theft. All sessions for this device are killed. Log in again. |


Verify that an access token is still valid. Returns the user's basic info. Useful for auto-login on app start.

Request:

curl -X POST http://localhost:3100/api/auth/verify-token \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Headers:

| Header | Value | Required | |--------|-------|----------| | Authorization | Bearer {access_token} | Yes |

Response (200):

{
  "status": 1,
  "message": "Token valid",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]"
  }
}

Log out the current session. Optionally pass the refresh token to kill it.

Request:

curl -X POST http://localhost:3100/api/auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "d8f2a1b4e6c9..."
  }'

Headers:

| Header | Value | Required | |--------|-------|----------| | Authorization | Bearer {access_token} | Yes |

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | refresh_token | string | No | Kills this specific refresh token |

Response (200):

{
  "status": 1,
  "message": "Logged out",
  "data": null
}

Log out from ALL devices. Kills every refresh token for this user.

Request:

curl -X POST http://localhost:3100/api/auth/logout-all \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Headers:

| Header | Value | Required | |--------|-------|----------| | Authorization | Bearer {access_token} | Yes |

Response (200):

{
  "status": 1,
  "message": "All sessions ended",
  "data": null
}

Refresh Tokens

How It Works

Refresh tokens solve a common problem: you want short-lived access tokens (for security) but you don't want users to log in every hour.

Here's the flow:

1. Login
   └── You get: access_token (short-lived) + refresh_token (long-lived)

2. Use access_token
   └── Send it in the Authorization header for every API call

3. Access token expires
   └── POST /api/auth/refresh-token with your refresh_token

4. Get new tokens
   └── New access_token + NEW refresh_token (old one is destroyed)

5. Repeat from step 2

Theft Detection

Every refresh token can only be used once. When you use it, you get a new one. The old one dies immediately.

If an attacker steals your refresh token and uses it before you do:

  • They get new tokens
  • When YOU try to use the old refresh token, it fails
  • The system detects this as theft and kills ALL tokens for that session
  • Both you and the attacker are logged out

This is called refresh token rotation with reuse detection.

How Refresh Tokens Are Stored

Refresh tokens are never stored in plain text. The backend stores a bcrypt hash. Even if the database is compromised, the tokens can't be extracted.

Each token has:

  • token_id — first 16 characters, used for fast database lookup
  • token_hash — bcrypt hash of the full token
  • family_id — groups tokens from the same login session (for theft detection)
  • device_info — from the User-Agent header (for multi-device tracking)

Multi-Device Sessions

Each role has a maximum number of simultaneous sessions:

| Role | Max Sessions | |------|-------------| | super_admin | 1 | | admin | 3 | | user | 5 |

If a user logs in on a 6th device, the oldest session is automatically killed.

What Kills Refresh Tokens

| Action | Effect | |--------|--------| | Logout | Kills the specific refresh token you pass | | Logout All | Kills ALL refresh tokens for the user | | Password Reset | Kills ALL refresh tokens for the user | | Password Change (profile) | Kills ALL refresh tokens for the user | | Token Reuse Detected | Kills ALL tokens for that session (family) | | Max Sessions Exceeded | Kills the oldest session |


Account Status & Lifecycle

Every user has an account_status that determines whether they can log in and what message they see.

Status Values

| Status | What It Means | Can Log In? | How It's Set | |--------|--------------|-------------|-------------| | active | Normal account | Yes | Default for new accounts | | inactive_by_admin | Suspended by admin | No (status 5) | Admin sets this in database | | inactive_by_user | Deactivated by user | Yes (reactivates on login) | User calls /profile/deactivate-account | | deleted_by_user | User deleted their account | No (status 6) | User calls /profile/delete-account | | deleted_by_admin | Admin deleted the account | No (status 7) | Admin sets this in database |

What the User Sees on Login

When a blocked user tries to log in, they get a special status code:

| Status Code | Message | Support Email Shown? | |------------|---------|---------------------| | 5 | "Your account has been suspended. Contact support for assistance." | Yes (if configured) | | 6 | "This account was previously deleted. You can create a new account." | No | | 7 | "Your account has been removed by admin. Contact support for assistance." | Yes (if configured) | | 10 | "User not found" / "No account linked with this social account" | No |

Configuring the Support Email

When users see status 5 or 7, the response can include a support email. Set it in auth_config.ts:

account: {
  blocked_account_support_email: "[email protected]"
}

If left empty, no support email is shown in the response.


RBAC — Role-Based Access Control

The Simple Version

Every user has a role. Every role has permissions. Permissions say what the role can do, where, and under what conditions.

Default Roles

| Role | Level | What They Can Do | |------|-------|-----------------| | super_admin | 1000 | Everything. Full access to all modules, all data, all fields. | | admin | 100 | Full CRUD on all modules. Can see all data. | | user | 1 | Basic CRUD on all modules. Own data only. Active records only. |

Level determines hierarchy. A role can only manage roles with a lower level. A super_admin (1000) can manage admin (100), but not the other way around.

Permission Structure

Each permission answers 5 questions:

| Question | Field | Example | |----------|-------|---------| | WHO can do this? | role | "admin" | | WHAT can they do? | actions | { create: true, read: true, update: true, delete: true } | | WHERE does it apply? | module | "products" or "*" (all modules) | | WHEN / under what conditions? | scope | { ownership: "all", status: ["*"] } | | EFFECT — allow or deny? | effect | "allow" or "deny" |

When both allow and deny exist for the same action, deny always wins.

How Permissions Are Evaluated

When a request comes in, the RBAC middleware checks in this order:

1. Is there an EXPLICIT DENY for this role + module?     → BLOCKED (deny always wins)
2. Is there an EXPLICIT ALLOW for this role + module?     → check actions, scope, fields
3. Is there a WILDCARD (*) ALLOW for this role?           → check actions, scope, fields
4. Does the role have defaultAccess = true?               → use defaultPermissions
5. None of the above?                                     → BLOCKED (default deny)

This is the same model as AWS IAM: everything is denied unless explicitly allowed, and any deny overrides any allow.

What the RBAC Middleware Checks (in order)

Request comes in
    ↓
1. Extract JWT from Authorization header
    ↓ (fails → 401 "Authorization token is required")
2. Verify JWT signature and expiry
    ↓ (fails → 401 TOKEN_EXPIRED or INVALID_TOKEN)
3. Find user in database
    ↓ (fails → 401 USER_NOT_FOUND)
4. Check account_status is active
    ↓ (fails → 401 ACCOUNT_INACTIVE)
5. Look up user's role
    ↓ (fails → 403 ROLE_NOT_FOUND)
6. Check role is active
    ↓ (fails → 403 ROLE_INACTIVE)
7. Resolve permissions for this role + module + action
    ↓ (fails → 403 ACCESS_DENIED)
8. Apply scope filters (ownership, status, etc.)
    ↓
9. Apply field filters (prevent/allow)
    ↓
Request proceeds to controller

Scope Options

Scopes let you restrict what data a role can access:

| Scope | Values | Description | |-------|--------|-------------| | ownership | "own", "team", "department", "assigned", "all" | Whose data can they see? | | status | ["active"], ["active", "deleted"], ["*"] | Which record statuses? | | customFilter | { region: "$user.region" } | Dynamic filter using user's own fields | | maxRecords | 10, 50, 100 | Cap the number of results returned | | dateRange | 30 | Only records from the last N days |

Example: A user role with ownership: "own" and status: ["active"] can only see their own active records.

Field-Level Access

Control which fields each role can see or modify:

Prevent mode (blacklist) — block specific fields, everything else allowed:

{
  mode: "prevent",
  prevent: {
    read: ["internal_notes", "cost_price"],   // won't appear in responses
    create: ["is_featured"],                   // silently stripped on create
    update: ["sku"]                            // silently stripped on update
  }
}

Allow mode (whitelist) — only these fields, everything else blocked:

{
  mode: "allow",
  allow: {
    read: ["title", "price", "description", "category"],
    create: ["title", "price", "description", "category"]
  }
}

Hidden field override:

Fields marked as hidden in the CRUD schema are hidden from API responses by default. Roles with canOverrideHidden: true can see them using ?fields=hidden_field.


Managing Roles

How to Create a Custom Role

Step 1: Open src/config/roles.seed.ts

Step 2: Add your role to the seedRoles array:

{
  name: "editor",
  label: "Content Editor",
  level: 50,
  is_active: true,
  is_system: false,
  is_protected: false,
  description: "Can read and edit content, cannot delete or manage users",
  default_access: false,
  default_permissions: {
    actions: ["read"],
    scope: {
      ownership: "all",
      status: ["active"],
      customFilter: null,
      maxRecords: null,
      dateRange: null,
    },
    fields: {
      mode: "prevent",
      prevent: { read: [], create: [], update: [] },
      allow: { read: [], create: [], update: [] },
      canOverrideHidden: false,
    },
  },
},

Step 3: Add permissions for each module the role can access. Add to seedPermissions array:

{
  role: "editor",
  module: "products",
  effect: "allow",
  actions: {
    create: false,
    read: true,
    update: true,
    delete: false,
    hardDelete: false,
    restore: false,
    bulkDelete: false,
    bulkUpdate: false,
    copy: false,
    move: false,
    export: true,
    import: false,
  },
  scope: {
    ownership: "all",
    status: ["active"],
    customFilter: null,
    maxRecords: 50,
    dateRange: null,
  },
  fields: {
    mode: "prevent",
    prevent: {
      read: ["cost_price", "supplier_id"],
      create: [],
      update: ["price", "cost_price"],
    },
    allow: { read: [], create: [], update: [] },
    canOverrideHidden: false,
  },
  is_active: true,
},

Step 4: Re-seed the database (see below).

How to Assign a Role to a User

Update the user's roles field in MongoDB:

// In MongoDB shell (mongosh):
use myproject
db.users.updateOne(
  { email: "[email protected]" },
  { $set: { roles: ["editor"] } }
)

The first role in the array is the primary role — it determines token expiry and session limits.

How to Re-Seed Roles After Changes

Roles and permissions are seeded into the database on the first server start (when the collections are empty). After that, the database is the source of truth.

To apply changes from roles.seed.ts:

// In MongoDB shell (mongosh):
use myproject
db.roles.drop()
db.permissions.drop()
// Then restart the server — it will re-seed from roles.seed.ts

Warning: This deletes any runtime changes you made to roles/permissions directly in the database. Only the seed file survives.

How to Block a Role (Without Deleting It)

Set is_active: false on the role. Users with that role will get a 403 ROLE_INACTIVE error on every request.


Audit Logging

RBAC actions are logged to the audit_logs collection.

Audit levels (set in roles.seed.tsauditConfig.level):

| Level | What's Logged | |-------|--------------| | "none" | Nothing | | "deny" | Only denied requests (default) | | "write" | Denied requests + all write operations (create, update, delete) | | "all" | Every request |

What's logged: user ID, email, role, module, action, effect (allow/deny), reason, IP address, user agent, timestamp.

Sensitive fields are auto-redacted: password, new_password, old_password, token, refresh_token, otp, social_account_id.

Retention: Old audit logs are kept for 90 days by default (configurable in auditConfig.retentionDays).


CRUD Generator

Generating a Module

codewithrocky > add crud products

The interactive builder asks you for each field:

  1. Field name — e.g., title, price, category
  2. Field type — one of the 11 supported types
  3. Options — required, unique, default value, min/max, etc.

Supported Field Types

| # | Type | Description | Options | |---|------|-------------|---------| | 1 | String | Text field | trim, unique, index, minLength, maxLength | | 2 | Number | Integer or float | min, max | | 3 | Boolean | true/false | default | | 4 | Date | ISO date | — | | 5 | ObjectId | Reference to another model | ref (model name) | | 6 | Enum | One of predefined values | values (array) | | 7 | Array of Strings | List of text values | — | | 8 | Array of Objects | Nested objects with sub-fields | Sub-field builder | | 9 | Array of ObjectIds | List of references | ref (model name) | | 10 | Object/Mixed | Flexible structure | — | | 11 | Decimal128 | High-precision number (currency) | — |

What Values Can I Send? — By Field Type

When you create or update a record, here's exactly what each field type accepts:

{
  "title": "My Product",
  "price": 29.99,
  "in_stock": true,
  "release_date": "2026-06-15T10:00:00Z",
  "category_id": "6507f1f77bcf86cd799439011",
  "status": "draft",
  "tags": ["electronics", "sale", "new"],
  "variants": [
    { "color": "red", "size": "M", "qty": 10 },
    { "color": "blue", "size": "L", "qty": 5 }
  ],
  "related_products": [
    "6507f1f77bcf86cd799439012",
    "6507f1f77bcf86cd799439013"
  ],
  "metadata": { "weight": "2kg", "material": "cotton" },
  "exact_price": "29.99"
}

What you CANNOT send (auto-managed by the system):

  • _id — auto-generated
  • created_by — set from your JWT token
  • created_at — auto-set to current time
  • updated_at — auto-set on every update
  • document_entry_status — managed by delete/restore
  • __v — Mongoose version key
  • Fields marked as createOnly — can't change after creation
  • Fields marked as updateOnly — can't set during creation
  • Fields marked as systemManaged — you can never set these

What happens with wrong values:

  • Missing required field → "Field 'title' is required"
  • Invalid ObjectId → "Invalid ID format for 'category_id'"
  • Wrong enum value → "'invalid' is not a valid value for 'status'"
  • Duplicate unique field → "A record with this title already exists"
  • Extra unknown fields → silently ignored (no error)

Generated Endpoints

Every CRUD module generates 12 endpoints at /api/{module}/*.

For a module called products, the base URL is /api/products.


Create a new record.

Request:

curl -X POST http://localhost:3100/api/products/create \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "title": "Wireless Headphones",
    "price": "79.99",
    "category": "electronics"
  }'

Response (200):

{
  "status": 1,
  "message": "Record created",
  "data": {
    "_id": "6650b2c3d4e5f6a7b8c9d0e1",
    "title": "Wireless Headphones",
    "price": "79.99",
    "category": "electronics",
    "created_by": "6650a1b2c3d4e5f6a7b8c9d0",
    "is_deleted": false,
    "created_at": "2026-03-19T11:00:00.000Z",
    "updated_at": "2026-03-19T11:00:00.000Z"
  }
}

Get a single record by ID. Use ?fields= to return only specific fields.

Request:

curl -X GET "http://localhost:3100/api/products/get/6650b2c3d4e5f6a7b8c9d0e1" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

With field projection:

curl -X GET "http://localhost:3100/api/products/get/6650b2c3d4e5f6a7b8c9d0e1?fields=title,price" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Record found",
  "data": {
    "_id": "6650b2c3d4e5f6a7b8c9d0e1",
    "title": "Wireless Headphones",
    "price": "79.99",
    "category": "electronics",
    "created_by": "6650a1b2c3d4e5f6a7b8c9d0",
    "is_deleted": false,
    "created_at": "2026-03-19T11:00:00.000Z",
    "updated_at": "2026-03-19T11:00:00.000Z"
  }
}

List records with pagination, search, sorting, and filtering.

Request:

curl -X GET "http://localhost:3100/api/products/list?page=1&limit=20&search=headphones&sort_by=price&sort_order=asc" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

With filters:

curl -X GET "http://localhost:3100/api/products/list?category=electronics&price_min=50&price_max=200" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Records found",
  "data": {
    "docs": [
      {
        "_id": "6650b2c3d4e5f6a7b8c9d0e1",
        "title": "Wireless Headphones",
        "price": "79.99",
        "category": "electronics",
        "created_by": "6650a1b2c3d4e5f6a7b8c9d0",
        "created_at": "2026-03-19T11:00:00.000Z"
      }
    ],
    "totalDocs": 1,
    "limit": 20,
    "page": 1,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

Get the total count of records matching the filters. Accepts the same filters as /list.

Request:

curl -X GET "http://localhost:3100/api/products/count?category=electronics" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Count retrieved",
  "data": {
    "count": 42
  }
}

Partial update — send only the fields you want to change.

Request:

curl -X POST http://localhost:3100/api/products/update/6650b2c3d4e5f6a7b8c9d0e1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "price": "69.99"
  }'

Response (200):

{
  "status": 1,
  "message": "Record updated",
  "data": {
    "_id": "6650b2c3d4e5f6a7b8c9d0e1",
    "title": "Wireless Headphones",
    "price": "69.99",
    "category": "electronics",
    "created_by": "6650a1b2c3d4e5f6a7b8c9d0",
    "is_deleted": false,
    "created_at": "2026-03-19T11:00:00.000Z",
    "updated_at": "2026-03-19T11:15:00.000Z"
  }
}

Soft delete — marks the record as deleted but keeps it in the database.

Request:

curl -X DELETE "http://localhost:3100/api/products/delete/6650b2c3d4e5f6a7b8c9d0e1" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Record deleted",
  "data": {
    "_id": "6650b2c3d4e5f6a7b8c9d0e1",
    "is_deleted": true
  }
}

Soft delete multiple records at once.

Request:

curl -X POST http://localhost:3100/api/products/bulk-delete \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "ids": [
      "6650b2c3d4e5f6a7b8c9d0e1",
      "6650b2c3d4e5f6a7b8c9d0e2",
      "6650b2c3d4e5f6a7b8c9d0e3"
    ]
  }'

Response (200):

{
  "status": 1,
  "message": "3 records deleted",
  "data": {
    "deleted_count": 3
  }
}

Undo a soft delete.

Request:

curl -X POST "http://localhost:3100/api/products/restore/6650b2c3d4e5f6a7b8c9d0e1" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Record restored",
  "data": {
    "_id": "6650b2c3d4e5f6a7b8c9d0e1",
    "is_deleted": false
  }
}

Reverse lookup — find all records that reference a specific document.

For example, find all products created by a specific user:

Request:

curl -X GET "http://localhost:3100/api/products/by/created_by/6650a1b2c3d4e5f6a7b8c9d0" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Records found",
  "data": [
    {
      "_id": "6650b2c3d4e5f6a7b8c9d0e1",
      "title": "Wireless Headphones",
      "price": "79.99",
      "category": "electronics",
      "created_by": "6650a1b2c3d4e5f6a7b8c9d0"
    }
  ]
}

List all reference (ObjectId) fields on this model and what they point to.

Request:

curl -X GET "http://localhost:3100/api/products/relations" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Relations found",
  "data": [
    {
      "field": "created_by",
      "ref": "users"
    }
  ]
}

Schema introspection — returns the full schema definition including field types, validations, and options.

Request:

curl -X GET "http://localhost:3100/api/products/metadata" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Metadata retrieved",
  "data": {
    "model": "products",
    "fields": {
      "title": { "type": "String", "required": true },
      "price": { "type": "Decimal128", "required": true },
      "category": { "type": "String", "enum": ["electronics", "clothing", "food"], "required": true },
      "created_by": { "type": "ObjectId", "ref": "users" },
      "is_deleted": { "type": "Boolean", "default": false }
    }
  }
}

Cross-model discovery — get a record and include related records from other modules.

Request:

curl -X GET "http://localhost:3100/api/products/get/6650b2c3d4e5f6a7b8c9d0e1/related?include=reviews&reviews.status=active" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Query Parameters:

| Param | Description | |-------|-------------| | include | Comma-separated list of related modules to include | | {module}.{field} | Filter on the included module's fields |

Response (200):

{
  "status": 1,
  "message": "Record with relations",
  "data": {
    "_id": "6650b2c3d4e5f6a7b8c9d0e1",
    "title": "Wireless Headphones",
    "price": "79.99",
    "category": "electronics",
    "related": {
      "reviews": [
        {
          "_id": "6650c3d4e5f6a7b8c9d0e1f2",
          "rating": 5,
          "comment": "Great sound quality",
          "status": "active"
        }
      ]
    }
  }
}

Query Parameters Reference

Every /list and /count endpoint supports these query parameters:

| Parameter | Example | Description | |-----------|---------|-------------| | page | ?page=2 | Page number (default: 1) | | limit | ?limit=50 | Items per page (default: 20, max: 100) | | search | ?search=keyword | Full-text search across all string fields | | sort_by | ?sort_by=price | Field to sort by (default: created_at) | | sort_order | ?sort_order=asc | asc or desc (default: desc) | | fields | ?fields=title,price | Return only these fields (projection) | | {field} | ?status=draft | Exact match filter on any field | | {field}=a,b | ?status=draft,published | IN filter — matches any of the values | | {field}_min | ?price_min=10 | Range filter: greater than or equal to | | {field}_max | ?price_max=100 | Range filter: less than or equal to | | created_from | ?created_from=2026-01-01 | Records created on or after this date | | created_to | ?created_to=2026-12-31 | Records created on or before this date | | {ref}.{field} | [email protected] | Filter on a populated reference field |

Combine everything:

curl -X GET "http://localhost:3100/api/products/list?page=1&limit=10&search=headphones&category=electronics&price_min=50&price_max=200&sort_by=price&sort_order=asc&fields=title,price,category" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Filter Examples — Every Type Explained

Here's every way you can filter data, with real examples:

1. Exact Match — find records where a field equals a value

# Find all products in the "electronics" category
curl "http://localhost:3100/api/products/list?category=electronics" -H "Authorization: Bearer TOKEN"

# Find completed tasks
curl "http://localhost:3100/api/tasks/list?completed=true" -H "Authorization: Bearer TOKEN"

# Find tasks assigned to a specific user (ObjectId)
curl "http://localhost:3100/api/tasks/list?assigned_to=6507f1f77bcf86cd799439011" -H "Authorization: Bearer TOKEN"

2. Multiple Values (IN filter) — find records matching ANY of the values

# Products in electronics OR clothing
curl "http://localhost:3100/api/products/list?category=electronics,clothing" -H "Authorization: Bearer TOKEN"

3. Number Range — find records within a min/max range

# Products priced between $10 and $100
curl "http://localhost:3100/api/products/list?price_min=10&price_max=100" -H "Authorization: Bearer TOKEN"

# Products priced $50 or more (no max)
curl "http://localhost:3100/api/products/list?price_min=50" -H "Authorization: Bearer TOKEN"

4. Date Range — find records created within a time period

# Products created in January 2026
curl "http://localhost:3100/api/products/list?created_from=2026-01-01&created_to=2026-01-31" -H "Authorization: Bearer TOKEN"

5. Text Search — search across ALL string fields

# Search for "wireless" in title, description, category, or any string field
curl "http://localhost:3100/api/products/list?search=wireless" -H "Authorization: Bearer TOKEN"

6. Populated Field Filter — filter by a referenced document's field

# Find products created by a user with a specific email
curl "http://localhost:3100/api/products/[email protected]" -H "Authorization: Bearer TOKEN"

7. Field Projection — return only specific fields

# Only return title and price (smaller response, faster)
curl "http://localhost:3100/api/products/list?fields=title,price" -H "Authorization: Bearer TOKEN"

8. Sorting

# Cheapest first
curl "http://localhost:3100/api/products/list?sort_by=price&sort_order=asc" -H "Authorization: Bearer TOKEN"

# Alphabetical by title
curl "http://localhost:3100/api/products/list?sort_by=title&sort_order=asc" -H "Authorization: Bearer TOKEN"

9. Pagination

# First page, 10 items
curl "http://localhost:3100/api/products/list?page=1&limit=10" -H "Authorization: Bearer TOKEN"

# Second page
curl "http://localhost:3100/api/products/list?page=2&limit=10" -H "Authorization: Bearer TOKEN"

10. Combine Everything

# Electronics under $200, search "headphone", cheapest first, page 1, only title+price
curl "http://localhost:3100/api/products/list?category=electronics&price_max=200&search=headphone&sort_by=price&sort_order=asc&page=1&limit=5&fields=title,price" \
  -H "Authorization: Bearer TOKEN"

Tip: All these filters work on /count too — just replace /list with /count to get the matching count without fetching data.


Profile System

All profile endpoints require authentication. Base URL: /api/profile.

Note: Profile is installed automatically when you run add auth. You don't need a separate command.


Get the current user's profile.

Request:

curl -X GET http://localhost:3100/api/profile/get \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Profile found",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "John",
    "last_name": "Doe",
    "email": "[email protected]",
    "login_type": "manual_login",
    "email_notifications": true,
    "time_zone": null,
    "profile_image": null,
    "created_at": "2026-03-19T10:30:00.000Z"
  }
}

Update profile fields. Send only the fields you want to change.

Request:

curl -X POST http://localhost:3100/api/profile/update \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "first_name": "Jonathan",
    "time_zone": "America/New_York",
    "email_notifications": false
  }'

Updatable Fields:

| Field | Type | Description | |-------|------|-------------| | first_name | string | First name | | last_name | string | Last name | | email_notifications | boolean | Enable/disable email notifications | | time_zone | string | IANA timezone string | | profile_image | string or null | URL or null to remove |

Response (200):

{
  "status": 1,
  "message": "Profile updated",
  "data": {
    "user_id": "6650a1b2c3d4e5f6a7b8c9d0",
    "first_name": "Jonathan",
    "last_name": "Doe",
    "email": "[email protected]",
    "login_type": "manual_login",
    "email_notifications": false,
    "time_zone": "America/New_York",
    "profile_image": null,
    "created_at": "2026-03-19T10:30:00.000Z"
  }
}

Change the account password.

Manual login only. Social login users (Google/Apple) don't have passwords. If a social login user calls this endpoint, it returns: "Password management is not available for social login accounts".

Request:

curl -X POST http://localhost:3100/api/profile/change-password \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "old_password": "mypassword123",
    "new_password": "newstrongpassword",
    "confirm_password": "newstrongpassword"
  }'

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | old_password | string | Yes | Current password | | new_password | string | Yes | New password (min 6 chars) | | confirm_password | string | Yes | Must match new_password |

Response (200):

{
  "status": 1,
  "message": "Password changed",
  "data": null
}

Security: All existing sessions are invalidated. The user should log in again with the new password.


Request an email change. Sends an OTP to the new email for verification.

Not available for Google login accounts. Google email is linked to the Google account and cannot be changed independently. Apple login users CAN change their email.

Request:

curl -X POST http://localhost:3100/api/profile/change-email \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "new_email": "[email protected]"
  }'

Response (200):

{
  "status": 1,
  "message": "OTP sent to new email",
  "data": {
    "new_email": "[email protected]"
  }
}

Confirm the email change with the OTP sent to the new email.

Request:

curl -X POST http://localhost:3100/api/profile/verify-email-change \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "new_email": "[email protected]",
    "otp": "718293"
  }'

Response (200):

{
  "status": 1,
  "message": "Email changed",
  "data": {
    "new_email": "[email protected]"
  }
}

A notification email is sent to the old email address informing them of the change.


Temporarily deactivate the account. Sets status to inactive_by_user. The user can log in again to reactivate.

Request:

curl -X POST http://localhost:3100/api/profile/deactivate-account \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{
  "status": 1,
  "message": "Account deactivated",
  "data": null
}

Permanently delete the account (soft delete). Sets status to deleted_by_user.

Manual login users must confirm with their password. Social login users can delete without a password.

Request (manual login):

curl -X POST http://localhost:3100/api/profile/delete-account \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -d '{
    "password": "mypassword123"
  }'

Request (social login):

curl -X POST http://localhost:3100/api/profile/delete-account \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Request Body:

| Field | Type | Required | Description | |-------|------|----------|-------------| | password | string | Manual login only | Current password for confirmation |

Response (200):

{
  "status": 1,
  "message": "Account deleted",
  "data": null
}

What happens: Account status set to deleted_by_user, all OTPs deleted, all refresh tokens deleted, cache cleared. The user can register again with the same email.


Customization Guide

Generators Run Once

When you run add auth or add crud products, the generator creates files in your project. After that, the code is yours. The generator will not overwrite or update these files.

This means:

  • You can freely edit any generated file
  • Adding fields to a model? Edit src/models/{module}.ts directly
  • Changing controller logic? Edit src/controllers/common/{module}/controller.ts
  • The generator is a starting point, not a framework that controls your code

Can I Re-Run a Generator?

  • add auth — checks if auth is already installed. If yes, it skips (won't overwrite).
  • add crud products — checks if the model already exists. If yes, it skips (won't overwrite).

To regenerate, you'd need to delete the existing files first. But generally, just edit the files directly.

What's Safe to Edit

| File | Safe to Edit? | Notes | |------|--------------|-------| | src/models/*.ts | Yes | Add fields, change validations, add indexes | | src/controllers/**/*.ts | Yes | Change business logic, add custom endpoints | | src/routes/**/*.ts | Yes | Add routes, change middleware | | src/config/roles.seed.ts | Yes | Add roles, change permissions (re-seed required) | | src/utility/local/config/auth_config.ts | Yes | Change token expiry, password rules, enable social login | | src/middlewares/*.ts | Yes | Customize auth checks | | src/utility/services/*.ts | Yes | Customize email templates, cron schedules |


Configuration

Auth Config (src/utility/local/config/auth_config.ts)

Restart your server after changing any config. Config is loaded on startup.

| Setting | Default | Description | |---------|---------|-------------| | manual_login.enabled | true | Enable email/password login | | social_login.google.enabled | false | Enable Google OAuth. Also requires GOOGLE_CLIENT_ID_* env vars | | social_login.apple.enabled | false | Enable Apple Sign In. No env vars needed | | token.expiry | "24h" | Default access token expiry | | token.remember_me_expiry | "30d" | Expiry when "remember me" is used | | token.role_expiry.super_admin | "1h" | Access token expiry for super admins | | token.role_expiry.admin | "4h" | Access token expiry for admins | | token.role_expiry.user | "24h" | Access token expiry for users | | token.refresh.expiry_days.super_admin | 3 | Refresh token lifetime (days) for super admins | | token.refresh.expiry_days.admin | 7 | Refresh token lifetime (days) for admins | | token.refresh.expiry_days.user | 30 | Refresh token lifetime (days) for users | | token.refresh.max_sessions.super_admin | 1 | Max concurrent sessions for super admins | | token.refresh.max_sessions.admin | 3 | Max concurrent sessions for admins | | token.refresh.max_sessions.user | 5 | Max concurrent sessions for users | | otp.expiry_minutes | 10 | OTP expiration time in minutes | | otp.length | 6 | Number of digits in OTP | | password.min_length | 6 | Minimum password length | | password.require_uppercase | false | Require