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.