YeboID Middleware — Auth & Error Handling
This document covers all middleware in YeboID: authentication middleware, token validation, role checks, and error handling.
Middleware Files
| File | Purpose |
|---|---|
middleware/auth.ts | JWT validation, request augmentation |
middleware/error.ts | Global error handler |
Auth Middleware
File: api/src/middleware/auth.ts
Express Request Extension
The auth middleware extends the Express Request type to include userId:
// 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:
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:
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:
| Condition | Status | Response |
|---|---|---|
| No Authorization header | 401 | Missing or invalid authorization header |
| Not Bearer format | 401 | Missing or invalid authorization header |
| Invalid JWT | 401 | Invalid or expired token |
| Expired JWT | 401 | Invalid or expired token |
optionalAuth(req, res, next)
Optional authentication middleware. Extracts user if token present, but doesn't fail if missing.
Usage:
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:
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:
| Condition | Result |
|---|---|
| Valid token | req.userId set |
| Invalid/expired token | req.userId undefined (no error) |
| No token | req.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 handlerJWT Token Structure
The tokenService.verifyAccessToken() validates tokens with this structure:
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:
- Signature matches
JWT_SECRET - Not expired (
exp > now) - 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:
// In index.ts - MUST be last
app.use(errorHandler);Implementation:
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:
{ "phone": "invalid-phone" }Response (400):
{
"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):
{
"success": false,
"error": "Record already exists"
}Generic Errors
All other uncaught errors:
Development Response (500):
{
"success": false,
"error": "Actual error message here"
}Production Response (500):
{
"success": false,
"error": "Internal server error"
}Error Response Matrix
| Error Type | HTTP Status | Response |
|---|---|---|
| Zod validation | 400 | Validation failed + details |
| Prisma P2002 (unique) | 409 | Record already exists |
| Other Prisma | 500 | Internal server error |
| JWT invalid | 401 | Invalid or expired token |
| Generic | 500 | Internal server error |
Role Checks (Future)
Currently YeboID doesn't have role-based access control. Future implementation:
// 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:
// 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
// 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);
}
});