feat(backend): add foundation services and sql idempotency
This commit is contained in:
45
backend/command-api/src/middleware/auth.js
Normal file
45
backend/command-api/src/middleware/auth.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { AppError } from '../lib/errors.js';
|
||||
import { can } from '../services/policy.js';
|
||||
import { verifyFirebaseToken } from '../services/firebase-auth.js';
|
||||
|
||||
function getBearerToken(header) {
|
||||
if (!header) return null;
|
||||
const [scheme, token] = header.split(' ');
|
||||
if (!scheme || scheme.toLowerCase() !== 'bearer' || !token) return null;
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function requireAuth(req, _res, next) {
|
||||
try {
|
||||
const token = getBearerToken(req.get('Authorization'));
|
||||
if (!token) {
|
||||
throw new AppError('UNAUTHENTICATED', 'Missing bearer token', 401);
|
||||
}
|
||||
|
||||
if (process.env.AUTH_BYPASS === 'true') {
|
||||
req.actor = { uid: 'test-user', email: 'test@krow.local', role: 'TEST' };
|
||||
return next();
|
||||
}
|
||||
|
||||
const decoded = await verifyFirebaseToken(token);
|
||||
req.actor = {
|
||||
uid: decoded.uid,
|
||||
email: decoded.email || null,
|
||||
role: decoded.role || null,
|
||||
};
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) return next(error);
|
||||
return next(new AppError('UNAUTHENTICATED', 'Token verification failed', 401));
|
||||
}
|
||||
}
|
||||
|
||||
export function requirePolicy(action, resource) {
|
||||
return (req, _res, next) => {
|
||||
if (!can(action, resource, req.actor)) {
|
||||
return next(new AppError('FORBIDDEN', 'Not allowed to perform this action', 403));
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
25
backend/command-api/src/middleware/error-handler.js
Normal file
25
backend/command-api/src/middleware/error-handler.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { toErrorEnvelope } from '../lib/errors.js';
|
||||
|
||||
export function notFoundHandler(req, res) {
|
||||
res.status(404).json({
|
||||
code: 'NOT_FOUND',
|
||||
message: `Route not found: ${req.method} ${req.path}`,
|
||||
details: {},
|
||||
requestId: req.requestId,
|
||||
});
|
||||
}
|
||||
|
||||
export function errorHandler(error, req, res, _next) {
|
||||
const envelope = toErrorEnvelope(error, req.requestId);
|
||||
if (req.log) {
|
||||
req.log.error(
|
||||
{
|
||||
errCode: envelope.body.code,
|
||||
status: envelope.status,
|
||||
details: envelope.body.details,
|
||||
},
|
||||
envelope.body.message
|
||||
);
|
||||
}
|
||||
res.status(envelope.status).json(envelope.body);
|
||||
}
|
||||
10
backend/command-api/src/middleware/idempotency.js
Normal file
10
backend/command-api/src/middleware/idempotency.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AppError } from '../lib/errors.js';
|
||||
|
||||
export function requireIdempotencyKey(req, _res, next) {
|
||||
const idempotencyKey = req.get('Idempotency-Key');
|
||||
if (!idempotencyKey) {
|
||||
return next(new AppError('MISSING_IDEMPOTENCY_KEY', 'Missing Idempotency-Key header', 400));
|
||||
}
|
||||
req.idempotencyKey = idempotencyKey;
|
||||
return next();
|
||||
}
|
||||
9
backend/command-api/src/middleware/request-context.js
Normal file
9
backend/command-api/src/middleware/request-context.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
export function requestContext(req, res, next) {
|
||||
const incoming = req.get('X-Request-Id');
|
||||
req.requestId = incoming || randomUUID();
|
||||
res.setHeader('X-Request-Id', req.requestId);
|
||||
res.locals.startedAt = Date.now();
|
||||
next();
|
||||
}
|
||||
Reference in New Issue
Block a user