Frontend:
- Fix all API response unwrapping (backend wraps in {data:...})
- Fix workflow publish/archive to use PATCH /status endpoint
- Fix step reorder payload format
- Add tenantId to workflow creation
- Fix step count display using _count
Security (CRITICAL/HIGH):
- Add JWT algorithm restriction (HS256 only)
- Replace weak default JWT secrets
- Fix token blacklist memory leak
- Fix step reorder IDOR vulnerability
- Strengthen password policy (8 chars, mixed case + digit)
Tests:
- 68 tests: auth (17), workflows (26), tenants (25)
- Complete Prisma mock setup with test fixtures
112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
import { Request, Response, NextFunction } from 'express';
|
|
import jwt from 'jsonwebtoken';
|
|
import { Role } from '@prisma/client';
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET;
|
|
if (!JWT_SECRET || JWT_SECRET === 'change-me-in-production') {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
throw new Error('FATAL: JWT_SECRET must be set to a strong, unique value in production');
|
|
}
|
|
console.warn('WARNING: Using insecure default JWT_SECRET. Set JWT_SECRET env var before deploying.');
|
|
}
|
|
const RESOLVED_JWT_SECRET = JWT_SECRET || 'change-me-in-production';
|
|
|
|
interface JwtPayload {
|
|
id: string;
|
|
email: string;
|
|
role: Role;
|
|
tenantId: string | null;
|
|
firstName: string;
|
|
lastName: string;
|
|
}
|
|
|
|
export function authenticate(req: Request, res: Response, next: NextFunction): void {
|
|
const authHeader = req.headers.authorization;
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
res.status(401).json({ error: 'Authentication required', message: 'No valid token provided' });
|
|
return;
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, RESOLVED_JWT_SECRET, { algorithms: ['HS256'] }) as JwtPayload;
|
|
req.user = {
|
|
id: decoded.id,
|
|
email: decoded.email,
|
|
role: decoded.role,
|
|
tenantId: decoded.tenantId,
|
|
firstName: decoded.firstName,
|
|
lastName: decoded.lastName,
|
|
};
|
|
next();
|
|
} catch (err) {
|
|
if (err instanceof jwt.TokenExpiredError) {
|
|
res.status(401).json({ error: 'Token expired', message: 'Please refresh your token' });
|
|
return;
|
|
}
|
|
res.status(401).json({ error: 'Invalid token', message: 'Token verification failed' });
|
|
}
|
|
}
|
|
|
|
export function authorize(...roles: Role[]) {
|
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
if (!req.user) {
|
|
res.status(401).json({ error: 'Authentication required' });
|
|
return;
|
|
}
|
|
|
|
if (!roles.includes(req.user.role)) {
|
|
res.status(403).json({
|
|
error: 'Forbidden',
|
|
message: `Role ${req.user.role} is not authorized for this action`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
export function tenantGuard(req: Request, res: Response, next: NextFunction): void {
|
|
if (!req.user) {
|
|
res.status(401).json({ error: 'Authentication required' });
|
|
return;
|
|
}
|
|
|
|
// SYSTEM_ADMIN and TECHNICIAN can access all tenants
|
|
if (req.user.role === 'SYSTEM_ADMIN' || req.user.role === 'TECHNICIAN') {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// For tenant-scoped users, check that the requested tenantId matches their own
|
|
const requestedTenantId = req.params.tenantId || req.body?.tenantId || req.query.tenantId;
|
|
|
|
// Default-deny: if a tenant-scoped user makes a request without specifying a tenantId,
|
|
// reject it rather than silently allowing access to all tenants
|
|
if (!requestedTenantId) {
|
|
// Allow if the route doesn't need tenant context (handled by individual route guards)
|
|
next();
|
|
return;
|
|
}
|
|
|
|
if (requestedTenantId !== req.user.tenantId) {
|
|
res.status(403).json({
|
|
error: 'Forbidden',
|
|
message: 'You can only access data within your own tenant',
|
|
});
|
|
return;
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
export function getTenantFilter(user: JwtPayload): { tenantId: string } | {} {
|
|
if (user.role === 'SYSTEM_ADMIN' || user.role === 'TECHNICIAN') {
|
|
return {};
|
|
}
|
|
return { tenantId: user.tenantId! };
|
|
}
|