Skip to content

YeboID Middleware — Auth & Error Handling ​

This document covers all middleware in YeboID: authentication middleware, token validation, role checks, and error handling.


Middleware Files ​

FilePurpose
middleware/auth.tsJWT validation, request augmentation
middleware/error.tsGlobal error handler

Auth Middleware ​

File: api/src/middleware/auth.ts

Express Request Extension ​

The auth middleware extends the Express Request type to include userId:

typescript
// Extend Express Request
declare global {
  namespace Express {
    interface Request {
      userId?: string;
    }
  }
}

After successful authentication, req.userId contains the user's YeboID UUID.


authMiddleware(req, res, next) ​

Required authentication middleware. Returns 401 if token is missing or invalid.

Usage:

typescript
import { authMiddleware } from '../middleware/auth.js';

// Protect entire router
router.use(authMiddleware);

// Or protect single route
router.get('/me', authMiddleware, async (req, res) => {
  // req.userId is guaranteed to exist here
  const user = await prisma.user.findUnique({
    where: { id: req.userId }
  });
});

Implementation:

typescript
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  
  // Check header exists and has Bearer format
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ 
      success: false, 
      error: 'Missing or invalid authorization header' 
    });
  }
  
  // Extract token (remove "Bearer " prefix)
  const token = authHeader.slice(7);
  
  // Verify JWT
  const payload = tokenService.verifyAccessToken(token);
  
  if (!payload) {
    return res.status(401).json({ 
      success: false, 
      error: 'Invalid or expired token' 
    });
  }
  
  // Attach userId to request
  req.userId = payload.userId;
  next();
}

Error Responses:

ConditionStatusResponse
No Authorization header401Missing or invalid authorization header
Not Bearer format401Missing or invalid authorization header
Invalid JWT401Invalid or expired token
Expired JWT401Invalid or expired token

optionalAuth(req, res, next) ​

Optional authentication middleware. Extracts user if token present, but doesn't fail if missing.

Usage:

typescript
import { optionalAuth } from '../middleware/auth.js';

// Route works for both authenticated and anonymous users
router.get('/products', optionalAuth, async (req, res) => {
  if (req.userId) {
    // Personalized response for logged-in users
  } else {
    // Generic response for anonymous users
  }
});

Implementation:

typescript
export function optionalAuth(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  
  if (authHeader?.startsWith('Bearer ')) {
    const token = authHeader.slice(7);
    const payload = tokenService.verifyAccessToken(token);
    if (payload) {
      req.userId = payload.userId;
    }
  }
  
  // Always call next() - never fails
  next();
}

Behavior:

ConditionResult
Valid tokenreq.userId set
Invalid/expired tokenreq.userId undefined (no error)
No tokenreq.userId undefined (no error)

Token Validation Flow ​

Request arrives
    │
    ▼
┌───────────────────────────────────┐
│ Check Authorization header        │
│ Format: "Bearer <token>"          │
└───────────────────────────────────┘
    │
    ├── Missing/Invalid format ──► 401 Unauthorized
    │
    ▼
┌───────────────────────────────────┐
│ Extract token (remove "Bearer ") │
│ token = header.slice(7)           │
└───────────────────────────────────┘
    │
    ▼
┌───────────────────────────────────┐
│ Verify JWT with shared secret     │
│ jwt.verify(token, JWT_SECRET)     │
└───────────────────────────────────┘
    │
    ├── Invalid signature ────────► 401 Unauthorized
    ├── Expired ──────────────────► 401 Unauthorized
    ├── Wrong type (not 'access') ─► 401 Unauthorized
    │
    ▼
┌───────────────────────────────────┐
│ Extract userId from payload       │
│ req.userId = payload.userId       │
└───────────────────────────────────┘
    │
    ▼
  next() ──► Route handler

JWT Token Structure ​

The tokenService.verifyAccessToken() validates tokens with this structure:

typescript
interface TokenPayload {
  userId: string;      // UUID of the user
  type: 'access';      // Must be 'access' for access tokens
  iat: number;         // Issued at (Unix timestamp)
  exp: number;         // Expires at (Unix timestamp)
}

Validation checks:

  1. Signature matches JWT_SECRET
  2. Not expired (exp > now)
  3. Token type is 'access' (not 'refresh')

Error Middleware ​

File: api/src/middleware/error.ts

errorHandler(err, req, res, next) ​

Global error handler registered at the end of middleware chain.

Usage:

typescript
// In index.ts - MUST be last
app.use(errorHandler);

Implementation:

typescript
export function errorHandler(
  err: Error, 
  req: Request, 
  res: Response, 
  next: NextFunction
) {
  console.error('Error:', err);
  
  // Zod validation errors
  if (err instanceof ZodError) {
    return res.status(400).json({
      success: false,
      error: 'Validation failed',
      details: err.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message
      }))
    });
  }
  
  // Prisma errors
  if (err.constructor.name === 'PrismaClientKnownRequestError') {
    const prismaErr = err as any;
    if (prismaErr.code === 'P2002') {
      return res.status(409).json({
        success: false,
        error: 'Record already exists'
      });
    }
  }
  
  // Generic error
  res.status(500).json({
    success: false,
    error: process.env.NODE_ENV === 'production' 
      ? 'Internal server error' 
      : err.message
  });
}

Error Type Handling ​

Zod Validation Errors ​

When route handlers call schema.parse(req.body) and validation fails:

Input:

json
{ "phone": "invalid-phone" }

Response (400):

json
{
  "success": false,
  "error": "Validation failed",
  "details": [
    {
      "field": "phone",
      "message": "Invalid phone format"
    }
  ]
}

Prisma Unique Constraint Errors ​

When attempting to create a duplicate record:

Error Code: P2002 (Unique constraint violation)

Response (409):

json
{
  "success": false,
  "error": "Record already exists"
}

Generic Errors ​

All other uncaught errors:

Development Response (500):

json
{
  "success": false,
  "error": "Actual error message here"
}

Production Response (500):

json
{
  "success": false,
  "error": "Internal server error"
}

Error Response Matrix ​

Error TypeHTTP StatusResponse
Zod validation400Validation failed + details
Prisma P2002 (unique)409Record already exists
Other Prisma500Internal server error
JWT invalid401Invalid or expired token
Generic500Internal server error

Role Checks (Future) ​

Currently YeboID doesn't have role-based access control. Future implementation:

typescript
// Future: role middleware
export function requireRole(role: 'admin' | 'user') {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = await prisma.user.findUnique({
      where: { id: req.userId }
    });
    
    if (user?.role !== role) {
      return res.status(403).json({
        success: false,
        error: 'Insufficient permissions'
      });
    }
    
    next();
  };
}

// Usage
router.delete('/users/:id', authMiddleware, requireRole('admin'), ...);

KYC Status Checks (Future) ​

For routes requiring verified users:

typescript
// Future: KYC middleware
export function requireKyc(level: 'VERIFIED' | 'PENDING') {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = await prisma.user.findUnique({
      where: { id: req.userId }
    });
    
    if (user?.kycStatus !== 'VERIFIED') {
      return res.status(403).json({
        success: false,
        error: 'KYC verification required'
      });
    }
    
    next();
  };
}

Complete Auth Flow Example ​

typescript
// Protected route example
router.get('/users/me', authMiddleware, async (req, res, next) => {
  try {
    // req.userId is guaranteed by authMiddleware
    const user = await prisma.user.findUnique({
      where: { id: req.userId },
      select: {
        id: true,
        phone: true,
        handle: true,
        name: true,
        kycStatus: true,
      }
    });
    
    if (!user) {
      // Edge case: token valid but user deleted
      return res.status(404).json({ 
        success: false, 
        error: 'User not found' 
      });
    }
    
    res.json({ success: true, user });
  } catch (error) {
    // Passed to errorHandler
    next(error);
  }
});

One chat. Everything done.