Skip to content

YeboLearn Middleware

Authentication, authorization, validation, and utility middleware.

Authentication Middleware

Location: backend/src/middleware/auth.middleware.ts

authenticate

Verifies JWT and attaches user to request.

typescript
export const authenticate = async (
  req: AuthRequest,
  res: Response,
  next: NextFunction
) => {
  try {
    // Extract token from Authorization header
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
      throw new UnauthorizedError('No token provided');
    }

    const token = authHeader.substring(7);
    const payload = JwtUtil.verify(token);

    // Verify token version (for invalidation)
    const user = await database.query(
      'SELECT * FROM users WHERE id = $1',
      [payload.userId]
    );

    if (!user.rows[0]) {
      throw new UnauthorizedError('User not found');
    }

    if (payload.tokenVersion !== user.rows[0].token_version) {
      throw new UnauthorizedError('Token has been invalidated');
    }

    if (!user.rows[0].is_active) {
      throw new UnauthorizedError('Account is inactive');
    }

    // Attach to request
    req.user = {
      id: user.rows[0].id,
      schoolId: user.rows[0].school_id,
      role: user.rows[0].role,
      email: user.rows[0].email,
    };

    next();
  } catch (error) {
    next(error);
  }
};

Usage:

typescript
router.get('/students', authenticate, listStudents);

Authorization Middleware

Location: backend/src/middleware/authorize.middleware.ts

authorize(...roles)

Role-based access control.

typescript
export const authorize = (...allowedRoles: UserRole[]) => {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user) {
      throw new UnauthorizedError('Authentication required');
    }

    if (!allowedRoles.includes(req.user.role)) {
      throw new ForbiddenError('Insufficient permissions');
    }

    next();
  };
};

Usage:

typescript
router.post(
  '/students',
  authenticate,
  authorize('school_admin', 'teacher'),
  createStudent
);

schoolScope

Ensures user can only access their school's data.

typescript
export const schoolScope = (req: AuthRequest, res: Response, next: NextFunction) => {
  // Super admins can access any school
  if (req.user.role === 'super_admin') {
    return next();
  }

  // Others scoped to their school
  const requestedSchoolId = req.params.schoolId || req.body.school_id || req.query.school_id;

  if (requestedSchoolId && requestedSchoolId !== req.user.schoolId) {
    throw new ForbiddenError('Cannot access other school data');
  }

  next();
};

Validation Middleware

Location: backend/src/middleware/validation.middleware.ts

validate(schema)

Request validation using Joi.

typescript
import Joi from 'joi';

export const validate = (schema: Joi.Schema, source: 'body' | 'query' | 'params' = 'body') => {
  return (req: Request, res: Response, next: NextFunction) => {
    const { error, value } = schema.validate(req[source], {
      abortEarly: false,
      stripUnknown: true,
    });

    if (error) {
      const errors = error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message,
      }));
      throw new ValidationError('Validation failed', errors);
    }

    req[source] = value;  // Replace with sanitized values
    next();
  };
};

Usage:

typescript
const createStudentSchema = Joi.object({
  first_name: Joi.string().required().max(100),
  last_name: Joi.string().required().max(100),
  date_of_birth: Joi.date().optional(),
  grade_level: Joi.string().required(),
});

router.post(
  '/students',
  authenticate,
  validate(createStudentSchema),
  createStudent
);

Rate Limiting Middleware

Location: backend/src/middleware/rateLimit.middleware.ts

rateLimiter

Prevents abuse with configurable limits.

typescript
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import redisClient from '@utils/redis';

export const createRateLimiter = (options: {
  windowMs: number;
  max: number;
  message?: string;
}) => {
  return rateLimit({
    store: new RedisStore({
      sendCommand: (...args) => redisClient.sendCommand(args),
    }),
    windowMs: options.windowMs,
    max: options.max,
    message: {
      success: false,
      error: options.message || 'Too many requests',
    },
    standardHeaders: true,
    legacyHeaders: false,
  });
};

// Presets
export const authLimiter = createRateLimiter({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 10,
  message: 'Too many auth attempts',
});

export const apiLimiter = createRateLimiter({
  windowMs: 60 * 1000,  // 1 minute
  max: 100,
});

export const aiLimiter = createRateLimiter({
  windowMs: 60 * 1000,
  max: 10,  // AI calls are expensive
  message: 'AI rate limit exceeded',
});

Error Handling Middleware

Location: backend/src/middleware/error.middleware.ts

errorHandler

Global error handling.

typescript
export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  // Log error
  logger.error({
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    userId: (req as AuthRequest).user?.id,
  });

  // Known error types
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      success: false,
      error: {
        code: err.code,
        message: err.message,
        details: err.details,
      },
    });
  }

  // Database errors
  if (err.code === '23505') {  // Unique violation
    return res.status(409).json({
      success: false,
      error: {
        code: 'DUPLICATE_ENTRY',
        message: 'Resource already exists',
      },
    });
  }

  // Default server error
  res.status(500).json({
    success: false,
    error: {
      code: 'INTERNAL_ERROR',
      message: config.nodeEnv === 'production' 
        ? 'Internal server error' 
        : err.message,
    },
  });
};

Request Logging Middleware

Location: backend/src/middleware/logging.middleware.ts

requestLogger

Logs all incoming requests.

typescript
import morgan from 'morgan';

// Custom format
morgan.token('user-id', (req: AuthRequest) => req.user?.id || 'anonymous');
morgan.token('school-id', (req: AuthRequest) => req.user?.schoolId || 'none');

export const requestLogger = morgan(
  ':method :url :status :response-time ms - user::user-id school::school-id',
  {
    stream: {
      write: (message) => logger.http(message.trim()),
    },
    skip: (req) => req.path === '/api/health',
  }
);

File Upload Middleware

Location: backend/src/middleware/upload.middleware.ts

uploadMiddleware

Handles multipart file uploads.

typescript
import multer from 'multer';
import { S3Client } from '@aws-sdk/client-s3';
import multerS3 from 'multer-s3';

const s3 = new S3Client({
  region: 'auto',
  endpoint: config.r2.endpoint,
  credentials: {
    accessKeyId: config.r2.accessKeyId,
    secretAccessKey: config.r2.secretAccessKey,
  },
});

export const uploadMiddleware = multer({
  storage: multerS3({
    s3,
    bucket: config.r2.bucket,
    key: (req, file, cb) => {
      const userId = (req as AuthRequest).user?.id;
      const ext = path.extname(file.originalname);
      const key = `uploads/${userId}/${Date.now()}${ext}`;
      cb(null, key);
    },
    contentType: multerS3.AUTO_CONTENT_TYPE,
  }),
  limits: {
    fileSize: 10 * 1024 * 1024,  // 10MB
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new ValidationError('Invalid file type'));
    }
  },
});

// Usage:
// router.post('/upload', authenticate, uploadMiddleware.single('file'), uploadController);

CORS Middleware

Location: backend/src/middleware/cors.middleware.ts

typescript
import cors from 'cors';

const allowedOrigins = [
  'https://student.yebolearn.com',
  'https://teacher.yebolearn.com',
  'https://parent.yebolearn.com',
  'https://admin.yebolearn.com',
  'https://kids.yebolearn.com',
];

export const corsMiddleware = cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin) || config.nodeEnv === 'development') {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
});

Middleware Stack Order

typescript
// app.ts
app.use(helmet());              // Security headers
app.use(corsMiddleware);        // CORS
app.use(express.json());        // Body parser
app.use(requestLogger);         // Request logging

// Rate limiting on sensitive routes
app.use('/api/auth', authLimiter);
app.use('/api/ai', authenticate, aiLimiter);

// API routes
app.use('/api', routes);

// Error handler (must be last)
app.use(errorHandler);

One chat. Everything done.