Chat about this codebase

AI-powered code exploration

Online

Project Overview

light-auth is a lightweight, flexible authentication module for Express.js applications. It provides a ready-made solution for common auth needs while offering deep customization through hooks and configuration options.

What is light-auth?

light-auth initializes all authentication workflows via a single setupAuth function. It ships with:

  • JWT and session-based login
  • User registration and email verification
  • Password reset flows
  • Role-based access control (RBAC)
  • Default REST routes for all auth actions
  • Customizable middleware and lifecycle hooks

Why light-auth?

Express projects often reimplement:

  • Token/session management
  • Email flows (verification, reset)
  • Guarded routes by role
  • Custom hooks for business logic

light-auth centralizes these concerns, reduces boilerplate, and scales from simple apps to complex systems.

Core Features

  • Dual Strategies: JWT or cookie-based sessions
  • User Management: Registration, login, logout
  • Email Workflows: Verification and password reset
  • Access Control: Define roles and protect routes
  • Lifecycle Hooks: Customize database queries, email templates, token lifetimes
  • Default Routes: /register, /login, /logout, /verify-email, /reset-password
  • Flexible Config: Override any default, integrate with ORMs or custom stores

Quick Start

Install package:

npm install light-auth

Initialize in your Express app:

const express = require('express');
const { setupAuth } = require('light-auth');

const app = express();

app.use(express.json());

setupAuth(app, {
  jwtSecret: process.env.JWT_SECRET,
  sessionSecret: process.env.SESSION_SECRET,
  db: {
    // e.g., Sequelize or custom connectors
    client: require('./dbClient'),
  },
  email: {
    transporter: require('./emailTransporter'),
    from: 'no-reply@yourapp.com'
  },
  roles: ['user', 'admin']
});

// Your protected route
app.get('/admin', app.auth.requireRole('admin'), (req, res) => {
  res.send('Welcome, Admin!');
});

app.listen(3000, () => console.log('Server running on port 3000'));

Advanced Usage

Override default hooks to inject business logic:

setupAuth(app, {
  // ...other config
  hooks: {
    // Modify user payload before registration
    beforeRegister: async (userData) => {
      userData.profileCreatedAt = new Date();
      return userData;
    },
    // Log every successful login
    onLoginSuccess: async ({ user }) => {
      await logAuthEvent({ userId: user.id, event: 'login' });
    }
  }
});

With its minimal setup and rich feature set, light-auth accelerates building secure, production-ready Express applications.

Quick Start

Get up and running with user registration, login and logout in minutes.

1. Install

Install the package and peer dependencies:

npm install light-auth express

2. Minimal Express App

Create server.js:

const express = require('express');
const { setupAuth, auth } = require('light-auth');

const app = express();

// Parse JSON bodies
app.use(express.json());

// Initialize authentication
// - session-based by default
// - default routes under /auth
setupAuth(app, {
  // replace with a strong secret in production
  sessionSecret: 'your-session-secret',
  // optional: change route prefix (default: '/auth')
  // routePrefix: '/api/auth',
});

app.get('/', (req, res) => {
  res.send('Welcome to Light Auth Quick Start');
});

// Protected route example
app.get('/profile', auth.required, (req, res) => {
  // req.user is set after successful login
  res.json({ email: req.user.email, id: req.user.id });
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});

3. Register a User

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"P@ssw0rd"}'

Successful response returns HTTP 201 and a JSON payload:

{
  "id": "605c2b9f4f1a2c0015e8a9d3",
  "email": "user@example.com"
}

4. Log In

curl -X POST http://localhost:3000/auth/login \
  -c cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"P@ssw0rd"}'
  • Session cookie is stored in cookies.txt.
  • Response HTTP 200.

5. Access Protected Route

curl http://localhost:3000/profile \
  -b cookies.txt

Response HTTP 200 returns the user’s profile:

{
  "email": "user@example.com",
  "id": "605c2b9f4f1a2c0015e8a9d3"
}

6. Log Out

curl -X POST http://localhost:3000/auth/logout \
  -b cookies.txt
  • Server clears session.
  • Subsequent access to /profile returns HTTP 401.

You now have registration, login, logout and protected routes running with Light Auth. For advanced configuration (JWT, email verification, hooks), see the full documentation.

Core Concepts & Configuration

This section covers the foundational modules you must understand before configuring Light-Auth: session management, one-time passwords, request authentication, and password policy validation. Each subsection explains key concepts, exposes configuration points, and provides practical code examples.

Configuring Session Middleware with setupSession

Initialize and secure Express session handling with sensible defaults, environment-aware settings, and optional custom stores.

The setupSession helper wraps express-session configuration, enforcing:

  • A strong session secret (≥16 chars)
  • Explicit resave and saveUninitialized flags
  • Secure cookie defaults (httpOnly, sameSite: 'lax', secure in production)
  • Optional session store injection (e.g., Redis)

API

import { setupSession } from "./modules/auth/session.js";

/**
 * @param {import('express').Application} app
 * @param {string} secret           // ≥16-character string
 * @param {object} sessionConfig
 * @param {boolean} sessionConfig.resave
 * @param {boolean} sessionConfig.saveUninitialized
 * @param {object} sessionConfig.cookie
 * @param {boolean} [sessionConfig.cookie.secure]
 * @param {import('express-session').Store} [sessionConfig.store]
 */
setupSession(app, secret, sessionConfig);

Key Behaviors

  1. Secret Validation
    Throws if !secret || typeof secret!=="string" || secret.length<16.
  2. Flag Enforcement
    Requires boolean resave and saveUninitialized.
  3. Environment Awareness
    In NODE_ENV==="production", sets cookie.secure = true unless explicitly false.
  4. Cookie Defaults
    Merges user-supplied cookie with:
    • httpOnly: true
    • sameSite: "lax"
    • secure: as above
  5. Custom Store
    Accepts any compatible store (e.g., connect-redis). Falls back to in-memory (not for production).

Example: Basic Setup

import express from "express";
import RedisStore from "connect-redis";
import session from "express-session";
import { setupSession } from "./modules/auth/session.js";

const app = express();
const redisClient = /* initialized ioredis or node-redis client */;
const store = new RedisStore({ client: redisClient });

setupSession(app, process.env.SESSION_SECRET, {
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 1000 * 60 * 60 * 24,  // 1 day
    secure: true                  // override only if not in production
  },
  store
});

Practical Guidance

  • Always supply a strong, unique secret (process.env.SESSION_SECRET ≥16 chars).
  • Set saveUninitialized: false to avoid unnecessary sessions.
  • In production:
    • Ensure cookie.secure is true (HTTPS only).
    • Use a persistent store (Redis, MongoDB) to avoid memory leaks.
  • Customize cookie.maxAge as needed; defaults to session-only.
  • Call setupSession once per app instance; repeated calls throw or override existing middleware.

OTP Service: Generating and Validating One-Time Passwords

Secure, configurable functions to generate OTPs and verify them safely (timing-safe comparison and expiry checks).

API

import { generateOTP, isOTPValid } from "./modules/email/services/otpService.js";

/**
 * generateOTP(length = 6, type = "numeric"): string
 * - length: number of characters in the OTP
 * - type: "numeric" | "alphanumeric"
 */
const otp = generateOTP();
/**
 * isOTPValid(storedOtp, expiry, providedOtp): boolean
 * - storedOtp: string
 * - expiry: Date | number | ISO string
 * - providedOtp: string
 */
const valid = isOTPValid(storedOtp, expiry, providedOtp);

Examples

// 1. Generate a 6-digit numeric OTP
const otp1 = generateOTP(); // e.g. "527394"

// 2. Generate an 8-character alphanumeric OTP
const otp2 = generateOTP(8, "alphanumeric"); // e.g. "A4b9Z1qL"

// 3. Store OTP and expiry in user document
user.emailOtp = otp1;
user.emailOtpExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
await user.save();

// 4. Later, validate incoming OTP
if (!isOTPValid(user.emailOtp, user.emailOtpExpires, req.body.otp)) {
  return res.status(400).json({ error: "Invalid or expired OTP." });
}
// proceed to verify user or reset password

Practical Tips

  • Always pair generateOTP with an expiry timestamp stored alongside (Date.now() + minutes*60000).
  • Use isOTPValid to reject missing or expired OTPs and prevent timing attacks via crypto.timingSafeEqual.
  • Choose length and type based on security needs:
    • Numeric: simpler input (SMS/email)
    • Alphanumeric: higher entropy
  • Integrate in Express routes to centralize OTP logic.
router.post("/verify-otp", async (req, res) => {
  const { email, otp } = req.body;
  const user = await UserModel.findOne({ email });
  if (!user || !isOTPValid(user.otp, user.otpExpires, otp)) {
    return res.status(400).json({ error: "Invalid or expired OTP." });
  }
  res.json({ message: "OTP verified." });
});

authenticate(useSession, jwtSecret) Middleware

Verify incoming requests via session or JWT, attach a unified req.user payload on success, and return 401 on failure.

Behavior

  • If useSession is true, checks req.session.user; else returns 401.
  • If useSession is false, expects Authorization: Bearer <token> header:
    • Verifies token with jwtSecret.
    • Ensures decoded payload contains id and role.
  • On success, sets:
    req.user = {
      id: decoded.id,
      role: decoded.role,
      ...decoded
    };
    
  • Catches JWT errors, distinguishing expired tokens (TokenExpiredError) from other invalid tokens.

Usage

import express from "express";
import { authenticate } from "./modules/middleware/authMiddleware.js";

const app = express();
const JWT_SECRET = process.env.JWT_SECRET;

// JWT-based protection
app.get(
  "/api/profile",
  authenticate(false, JWT_SECRET),
  (req, res) => {
    // req.user → { id, role, ... }
    res.json({ profile: /* fetch by req.user.id */ });
  }
);

// Session-based protection
app.get(
  "/dashboard",
  authenticate(true, JWT_SECRET),
  (req, res) => {
    // req.user comes from req.session.user
    res.render("dashboard", { user: req.user });
  }
);

Practical Tips

  • Mount body parsers and setupSession before this middleware.
  • For JWT flows, ensure clients handle 401 by refreshing tokens or re-authenticating.
  • Combine with authorize(roles) downstream for role-based access control.

Password Policy Validator

Generate a reusable password-validation function based on your security requirements.

API

import { validatePassword, defaultPasswordPolicy }
  from "./modules/utils/validators.js";

/**
 * validatePassword(policy?): (password: string) => string|null
 * - policy.minLength?: number
 * - policy.requireUppercase?: boolean
 * - policy.requireLowercase?: boolean
 * - policy.requireNumbers?: boolean
 * - policy.requireSymbols?: boolean
 */
const validator = validatePassword({
  minLength: 10,
  requireNumbers: true,
  requireSymbols: true
});

Default policy:

// defaultPasswordPolicy === {
//   minLength: 8,
//   requireUppercase: false,
//   requireLowercase: false,
//   requireNumbers: false,
//   requireSymbols: false
// }

Example: Direct Express Use

import express from "express";
import { validatePassword } from "./modules/utils/validators.js";

const app = express();
app.use(express.json());

const passwordValidator = validatePassword({
  minLength: 8,
  requireUppercase: true,
  requireLowercase: true,
  requireNumbers: true,
  requireSymbols: true
});

app.post("/change-password", (req, res) => {
  const { newPassword } = req.body;
  const error = passwordValidator(newPassword);
  if (error) return res.status(400).json({ error });
  // proceed to hash and update password...
  res.sendStatus(204);
});

Integration in setupAuth

import express from "express";
import { setupAuth } from "./setupAuth.js";

const app = express();
await setupAuth(app, {
  jwtSecret: process.env.JWT_SECRET,
  db: mongooseConnection,
  passwordPolicy: {
    minLength: 12,
    requireUppercase: true,
    requireNumbers: true
  }
});

setupAuth deep-merges your passwordPolicy with defaults and applies the validator in registration and password-change routes.

Practical Guidance

  • Enforce at least one lowercase, uppercase, number, and symbol in production.
  • Set minLength per organizational policy (e.g., 12+ chars).
  • Validate before hashing or DB writes to avoid wasted work.
  • Reuse the same validator across all password endpoints for consistency.

Routes & Middleware

This section documents the Express routes and middleware exposed by light-auth after initialization. You’ll find:

  • A registration endpoint (registerRoute)
  • Email-based OTP routes (setupEmailRoutes)
  • Authentication and authorization middleware (authenticate, authorize)

User Registration Route (registerRoute)

Purpose: Mounts a POST /register endpoint with rate limiting, input validation, password hashing, role enforcement and optional lifecycle hooks.

Function Signature

registerRoute(router, UserModel, roles, validatePassword, rateLimitOptions, config = {})

Parameters

  • router: Express Router instance
  • UserModel: Mongoose-style model with .findOne() and .create()
  • roles: Array of allowed role strings (e.g. ['user','admin'])
  • validatePassword: fn(password) → null for valid or error message string
  • rateLimitOptions: Options for express-rate-limit (e.g. { windowMs:60000, max:5 })
  • config.hooks (optional):
    • onRegister(user): called after successful creation
    • onError({ type, error, req }): called on any registration error

Behavior

  • Applies rate limiting
  • Validates email format via validateEmail
  • Validates password via validatePassword
  • Ensures role is in roles (defaults to "user")
  • Returns 409 Conflict if user exists
  • Hashes password with bcrypt (12 rounds)
  • Creates user, responds with { _id, email, role }
  • Invokes hooks on success or error

Code Example

import express from 'express'
import mongoose from 'mongoose'
import { registerRoute } from './modules/routes/authRoutes.js'
import { validatePassword } from './utils/passwordPolicy.js'
import UserModel from './models/User.js'

const app = express()
const router = express.Router()

const roles = ['user', 'admin']
const rateLimitOptions = { windowMs: 60_000, max: 10 }
const config = {
  hooks: {
    onRegister: async user => {
      // e.g. send welcome email
      await sendWelcomeEmail(user.email)
    },
    onError: async ({ type, error }) => {
      console.error(`Error in ${type}`, error)
    }
  }
}

app.use(express.json())
registerRoute(router, UserModel, roles, validatePassword, rateLimitOptions, config)
app.use('/auth', router)

app.listen(3000)

cURL Example

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com","password":"Str0ngP@ss!","role":"user"}'

Practical Tips

  • Customize validatePassword for your policy; return string or null.
  • Tune rateLimitOptions per traffic.
  • Use onRegister to trigger side-effects.
  • Handle errors in onError without leaking internals.

Email Routes Setup (setupEmailRoutes)

Purpose: Mount email OTP endpoints for verification and password reset with minimal configuration.

Function Signature

setupEmailRoutes(app, UserModel, config)
  • app: Express application instance
  • UserModel: Mongoose-style model with fields email, password, emailOtp, emailOtpExpires, resetOtp, resetOtpExpires, verified
  • config: Controls routes, toggles, OTP params and hooks

Configuration Shape

const emailConfig = {
  route: "/auth",                   // Base path
  emailVerification: {
    enabled: true,
    otpLength: 6,
    otpType: "numeric",             // "numeric" | "alphanumeric"
    otpExpiryMinutes: 10,
    sendMail: async ({ email, otp, type, url }) => { /* your mailer */ },
    url: "https://example.com/verify"
  },
  forgotPassword: {
    enabled: true,
    otpLength: 8,
    otpType: "alphanumeric",
    otpExpiryMinutes: 15,
    sendMail: async ({ email, otp, type, url }) => { /* your mailer */ },
    url: "https://example.com/reset"
  },
  hooks: {
    onVerify: async user => { /* post-verification logic */ }
  }
}

Initialization Example

import express from "express"
import mongoose from "mongoose"
import { setupEmailRoutes } from "./modules/email/routes/emailRoutes.js"
import UserModel from "./models/User.js"

const app = express()
app.use(express.json())

setupEmailRoutes(app, UserModel, emailConfig)

app.listen(3000, () => console.log("Server running on port 3000"))

Exposed Endpoints (at {route})

  1. POST /send-verification-otp
    • Body: { email }
    • Sends OTP via sendMail
    • Response: { message: "Verification OTP sent." }
  2. POST /verify-email
    • Body: { email, otp }
    • Marks user.verified = true, clears OTP, calls hooks.onVerify
    • Response: { message: "Email verified successfully." }
  3. POST /send-forgot-otp
    • Body: { email }
    • Sends reset OTP
    • Response: { message: "Password reset OTP sent." }
  4. POST /reset-password
    • Body: { email, otp, newPassword }
    • Hashes new password, updates user.password, clears OTP
    • Response: { message: "Password reset successful." }

Practical Tips

  • Extend UserModel schema with emailOtp, resetOtp, expiry fields, and verified boolean.
  • Use express-validator for payload checks via bundled validators.
  • Integrate sendMail with your SMTP or transactional email service.
  • Adjust otpLength, otpType, and otpExpiryMinutes per security policy.

Securing Routes with authenticate and authorize

Purpose: Protect routes by verifying identity (session or JWT) and enforcing role-based access.

1. Obtain Middleware from setupAuth

import express from "express"
import mongoose from "mongoose"
import { setupAuth } from "./setupAuth.js"

async function init() {
  const app = express()
  const config = {
    db: mongooseConnection,
    jwtSecret: process.env.JWT_SECRET,
    useSession: false,
    roles: ["user", "admin"]
  }

  const { auth } = await setupAuth(app, config)
  // auth.authenticate: verifies session or JWT, sets req.user
  // auth.authorize: enforces req.user.role
}

2. Authenticate Only

app.get(
  "/profile",
  auth.authenticate,           // 401 if unauthenticated
  (req, res) => {
    res.json({ id: req.user.id, role: req.user.role })
  }
)

3. Authenticate + Authorize

// Only admins can delete users
app.delete(
  "/users/:id",
  auth.authenticate,             
  auth.authorize(["admin"]),     // 403 if not admin
  async (req, res) => {
    await UserModel.deleteOne({ _id: req.params.id })
    res.json({ success: true })
  }
)

Common Pitfalls & Tips

  • Call setupAuth before defining protected routes to register session/JWT middleware.
  • Always chain authenticate before authorize.
  • Pass multiple roles for mixed-role access: authorize(["admin","manager"]).
  • Wrap middleware in custom error handlers for bespoke response formats.

API Reference

setupAuth(app, config)

Initializes authentication routes, sessions, JWT settings, rate limiting, and email workflows on an Express application.

Signature

/**
 * @param {import('express').Application} app
 * @param {Object} config
 * @returns {Promise<{
 *   auth: { authenticate: Function, authorize: Function },
 *   models: { User: any }
 * }>}
 */
await setupAuth(app, config)

Key Configuration Options

  • route (string): Base path for auth routes (default "/auth").
  • useSession (boolean): Enable express-session alongside JWT (default false).
  • roles (string[]): Roles to seed into the User model (default ["user"]).
  • passwordPolicy (object):
    • minLength (number, default 8)
  • rateLimiting (object):
    • login/register: { windowMs, max, message, store }
  • sessionConfig (object): Options for express-session (cookie, store, etc.)
  • jwtConfig (object): JWT signing options (expiresIn, etc.)
  • jwtSecret (string, ≥16 chars): Secret for signing tokens
  • db: Mongoose connection (.model() must be available)
  • User: Optional pre-built Mongoose model
  • emailVerification / forgotPassword (objects):
    • enabled (boolean)
    • requiredToLogin (boolean)
    • sendMail (async function)

Behavior

  • Deep-merges config over defaults
  • Validates presence of jwtSecret and db
  • Mounts:
    1. Helmet security headers
    2. Optional session middleware
    3. JSON body parser on auth router
    4. POST /auth/register, /auth/login, /auth/logout (with rate limits)
    5. Email routes if enabled (/auth/verify, /auth/forgot-password)
  • Returns { auth: { authenticate, authorize }, models: { User } }

Example

import express from "express"
import mongoose from "mongoose"
import { setupAuth } from "./setupAuth.js"

async function bootstrap() {
  const app = express()
  await mongoose.connect(process.env.MONGO_URI)

  const { auth, models } = await setupAuth(app, {
    db: mongoose,
    jwtSecret: process.env.JWT_SECRET,
    useSession: true,
    passwordPolicy: { minLength: 10 },
    rateLimiting: {
      login: { max: 10, windowMs: 600_000 }
    },
    emailVerification: {
      enabled: true,
      requiredToLogin: true,
      sendMail: async ({ to, subject, html }) => {
        // integrate your mailer here
      }
    }
  })

  app.get("/profile", auth.authenticate, (req, res) => {
    res.json({ user: req.user })
  })

  app.listen(3000)
}

bootstrap()

Tips

  • Use a strong JWT_SECRET (≥16 random chars).
  • For sessions, configure a production-grade sessionConfig.store.
  • Provide a real sendMail implementation or enable ALLOW_MOCK_EMAILS.
  • Protect role-based routes with auth.authorize("admin", "user").

validatePassword(policy?)

Provides a policy-driven password validator that returns an error message or null if valid.

Signature

function validatePassword(
  policy?: {
    minLength?: number
    requireUppercase?: boolean
    requireLowercase?: boolean
    requireNumbers?: boolean
    requireSymbols?: boolean
  }
): (password: string) => string | null

const defaultPasswordPolicy = {
  minLength: 8,
  requireUppercase: false,
  requireLowercase: false,
  requireNumbers: false,
  requireSymbols: false
}

Usage

import { validatePassword, defaultPasswordPolicy } from "modules/utils/validators"

// Default policy
const checkDefault = validatePassword()

// Strong policy
const strongPolicy = {
  minLength: 12,
  requireUppercase: true,
  requireNumbers: true,
  requireSymbols: true
}
const checkStrong = validatePassword(strongPolicy)

console.log(checkDefault("short"))                 // "Password must be at least 8 characters long."
console.log(checkStrong("LongerPass1!"))           // null
console.log(checkStrong("NoNumbers!"))             // "Password must contain at least one number."

Policy Options

  • minLength (number, default 8): Minimum total characters
  • requireUppercase (boolean): Require ≥1 uppercase letter
  • requireLowercase (boolean): Require ≥1 lowercase letter
  • requireNumbers (boolean): Require ≥1 digit
  • requireSymbols (boolean): Require ≥1 symbol (!@#$%, etc.)

Integration

  • Run in registration or password-reset handlers
  • Display returned string directly as user feedback
  • Combine with validateEmail for full credential checks

generateOTP(length?, type?)

Creates a secure one-time password (OTP).

Signature

function generateOTP(
  length?: number,            // default 6
  type?: "numeric" | "alphanumeric" // default "numeric"
): string

Example

import { generateOTP } from "modules/email/services/otpService"

// 6-digit numeric
const otp1 = generateOTP()               // e.g. "483920"

// 8-character alphanumeric
const otp2 = generateOTP(8, "alphanumeric") // e.g. "A9bZ3kLp"

Tips

  • Use numeric for ease of entry on mobile
  • Use alphanumeric + longer length (≥8) for higher entropy
  • Store with an expiration timestamp in your database

isOTPValid(storedOtp, expiry, providedOtp)

Validates an OTP against its stored value and expiry using a timing-safe comparison.

Signature

function isOTPValid(
  storedOtp: string,
  expiry: Date | number | string,
  providedOtp: string
): boolean

Example Workflow

import { generateOTP, isOTPValid } from "modules/email/services/otpService"

// 1. Issue OTP
const otp = generateOTP()
const expiresAt = Date.now() + 5 * 60_000
await db.insert("otps", { userId, otp, expiresAt })

// 2. Validate later
const record = await db.find("otps", { userId })
const valid = isOTPValid(record.otp, record.expiresAt, userInputOtp)

if (valid) {
  // proceed
} else {
  // retry or deny
}

Best Practices

  • Set a short expiry (5–10 minutes)
  • Delete or mark used/expired OTPs to prevent reuse
  • Ensure storedOtp and providedOtp lengths match
  • Rely on built-in crypto.timingSafeEqual for security

callHook(fn, context)

Safely invokes a user-provided hook (sync or async), catching and logging errors without interrupting the flow.

Signature

async function callHook(
  fn: Function | null | undefined,
  context: Record<string, any>
): Promise<void>

Behavior

  • No-ops if fn is not a function
  • Awaits the result if fn returns a promise
  • Catches and logs any errors internally

Examples

  1. Single hook
import { callHook } from "modules/utils/hookUtils"

async function onDataSave({ data, logger }) {
  if (!data.id) throw new Error("Missing ID")
  logger.info("Data saved:", data)
}

await callHook(onDataSave, { data: { id: 123 }, logger: console })
// Errors are logged as “[Hook Error]” but do not bubble up
  1. Multiple hooks
import { callHook } from "modules/utils/hookUtils"

const hooks = [
  async ctx => { /* hook A */ },
  ctx => { /* hook B */ },
  null,                        // skipped
  () => { throw new Error() }  // caught and logged
]

for (const hook of hooks) {
  await callHook(hook, { user: { id: 1 }, timestamp: Date.now() })
}

Tips

  • Keep context minimal; include only needed properties
  • Use distinct logging prefixes for easier tracing
  • For parallel execution, wrap in Promise.all but handle logs individually:
    await Promise.all(hooks.map(h => callHook(h, ctx)))
    

Extending & Contribution

This section explains how to adapt Light-Auth to your project’s needs and contribute improvements back to the repository.

Customizing Library Behavior

Configuring the Error-Handling Hook (hooks.onError)

Allow your app to intercept internal setupAuth failures without leaking stack traces or crashing.

• setupAuth wraps critical checks in callHook(merged.hooks.onError, { type, error }).
• Your onError hook runs with { type: string, error: Error } before the error is thrown.

Example:

import express from "express";
import { setupAuth } from "@daksh-dev/light-auth";

const app = express();
await setupAuth(app, {
  jwtSecret: process.env.JWT_SECRET,
  db: mongoose.connection,
  hooks: {
    onError: async ({ type, error }) => {
      // log to Sentry, suppress in dev
      console.log(`Auth setup failed at phase="${type}"`, error.message);
      await monitoringService.notify({ module: "setupAuth", phase: type, message: error.stack });
    }
  }
});

Tips:

  • Mark onError async if you call external services.
  • Don’t rethrow inside the hook; errors are caught by callHook.
  • Use type to distinguish phases (e.g., "setup", "db").

Overriding the User Model (createUserModel)

Provide a custom Mongoose User model or scaffold a default one.

Basic usage:

import mongoose from "mongoose";
import { createUserModel } from "@daksh-dev/light-auth";

const roles = ["user", "admin"];
const authCfg = {
  // To override default:
  // User: require("./models/MyUser.js").default
};

async function init() {
  // Ensure mongoose.connect() has run
  const User = await createUserModel(authCfg, roles, mongoose);
  const u = await User.create({ email: "jane@doe.com", password: "s3cr3t" });
  console.log(u);
}

init();

Behavior:

  1. Returns config.User if provided.
  2. Returns db.models.User if registered.
  3. Attempts to load models/User.js.
  4. In non-production, generates and writes a scaffold if missing; errors out in production.

Tips:

  • Commit the generated models/User.js to avoid runtime scaffolding.
  • Supply config.User to use custom schemas or plugins.

Replacing the Mail Sender (defaultSendMail)

defaultSendMail logs OTPs and links to the console for development.

Signature:

async function defaultSendMail({
  email: string,
  otp: string,
  type: string,
  url?: string
}): Promise<void>

Usage:

import { defaultSendMail } from "@daksh-dev/light-auth";

async function sendLoginOtp(userEmail) {
  const otp = generateOtp();
  await defaultSendMail({ email: userEmail, otp, type: "login" });
}

await defaultSendMail({
  email: "user@domain.com",
  otp: "654321",
  type: "reset",
  url: `https://app.example.com/reset?token=${token}`
});

Tips:

  • Use only in local or test environments.
  • Replace with your SMTP provider in production.
  • Ensure your OTP generator produces time-bound, single-use codes.

Contribution Guidelines

Follow these steps to submit fixes or enhancements.

1. Setup Development Environment

git clone https://github.com/Daksh-Dixit21/light-auth.git
cd light-auth
pnpm install
pnpm test       # ensure existing tests pass

2. Code Style & Testing

  • Use Node.js 20+ and ES modules.
  • Run pnpm lint and pnpm test before committing.
  • Add tests under tests/ for new features or bug fixes.

3. Branching & Commit Messages

  • Create feature branches: git checkout -b feature/your-feature-name.
  • Write descriptive commit messages: <area>: <short description> (e.g., hooks: add onError metadata).

4. Pull Request Process

  1. Push your branch to GitHub: git push origin feature/your-feature-name.
  2. Open a PR against main with the following:
    • Purpose and high-level summary.
    • Link to related issue or ticket.
    • Screenshots or logs if applicable.
  3. Address review comments and squash/fixup commits as needed.

5. CI & Publishing

  • The GitHub Actions workflow (.github/workflows/ci.yml) runs lint, tests, and publishes on release.
  • To publish a new version:
    git tag vX.Y.Z
    git push origin vX.Y.Z
    gh release create vX.Y.Z --title "vX.Y.Z" --notes "Changelog..."
    
  • Ensure NPM_TOKEN is set in repo Secrets for automated publishing.

Thank you for helping improve Light-Auth! We welcome all contributions.