initial wcag-ada
This commit is contained in:
parent
042b8cb83a
commit
d52cfe7de2
112 changed files with 9069 additions and 0 deletions
69
apps/wcag-ada/api/src/middleware/auth.ts
Normal file
69
apps/wcag-ada/api/src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { Context, Next } from 'hono';
|
||||
import { verify } from 'hono/jwt';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import { config } from '../config';
|
||||
|
||||
export const authenticate = async (c: Context, next: Next) => {
|
||||
try {
|
||||
// Check for Bearer token
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const payload = await verify(token, config.JWT_SECRET, 'HS256') as {
|
||||
sub: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
c.set('userId', payload.sub);
|
||||
c.set('userRole', payload.role);
|
||||
c.set('authType', 'jwt');
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
// Invalid JWT, continue to check API key
|
||||
}
|
||||
}
|
||||
|
||||
// Check for API key
|
||||
const apiKey = c.req.header('X-API-Key');
|
||||
if (apiKey) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
apiKey,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
c.set('userId', user.id);
|
||||
c.set('userRole', user.role);
|
||||
c.set('authType', 'apiKey');
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
} catch (error) {
|
||||
return c.json({ error: 'Authentication failed' }, 401);
|
||||
}
|
||||
};
|
||||
|
||||
export const requireRole = (role: string) => {
|
||||
return async (c: Context, next: Next) => {
|
||||
const userRole = c.get('userRole');
|
||||
|
||||
if (!userRole || userRole !== role) {
|
||||
return c.json({ error: 'Insufficient permissions' }, 403);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
59
apps/wcag-ada/api/src/middleware/error-handler.ts
Normal file
59
apps/wcag-ada/api/src/middleware/error-handler.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Context } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { ZodError } from 'zod';
|
||||
import { appConfig } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export const errorHandler = (err: Error, c: Context) => {
|
||||
logger.error('Error:', err);
|
||||
|
||||
// Handle Hono HTTP exceptions
|
||||
if (err instanceof HTTPException) {
|
||||
return c.json(
|
||||
{ error: err.message },
|
||||
err.status
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (err instanceof ZodError) {
|
||||
return c.json(
|
||||
{
|
||||
error: 'Validation error',
|
||||
details: err.errors.map(e => ({
|
||||
path: e.path.join('.'),
|
||||
message: e.message,
|
||||
})),
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Prisma errors
|
||||
if (err.constructor.name === 'PrismaClientKnownRequestError') {
|
||||
const prismaError = err as any;
|
||||
|
||||
if (prismaError.code === 'P2002') {
|
||||
return c.json(
|
||||
{ error: 'Duplicate entry' },
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (prismaError.code === 'P2025') {
|
||||
return c.json(
|
||||
{ error: 'Record not found' },
|
||||
404
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Default error response
|
||||
return c.json(
|
||||
{
|
||||
error: 'Internal server error',
|
||||
message: appConfig.env === 'development' ? err.message : undefined,
|
||||
},
|
||||
500
|
||||
);
|
||||
};
|
||||
75
apps/wcag-ada/api/src/middleware/rate-limiter.ts
Normal file
75
apps/wcag-ada/api/src/middleware/rate-limiter.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Context, Next } from 'hono';
|
||||
import Redis from 'ioredis';
|
||||
import { config } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getWorkerConfig } from '@wcag-ada/config';
|
||||
|
||||
const workerConfig = getWorkerConfig();
|
||||
const redis = new Redis({
|
||||
host: workerConfig.redis.host,
|
||||
port: workerConfig.redis.port,
|
||||
password: workerConfig.redis.password,
|
||||
db: workerConfig.redis.db,
|
||||
});
|
||||
|
||||
interface RateLimitOptions {
|
||||
limit?: number;
|
||||
window?: number; // in milliseconds
|
||||
keyGenerator?: (c: Context) => string;
|
||||
}
|
||||
|
||||
export const rateLimiter = (options: RateLimitOptions = {}) => {
|
||||
const {
|
||||
limit = config.API_RATE_LIMIT,
|
||||
window = config.API_RATE_WINDOW,
|
||||
keyGenerator = (c) => {
|
||||
const userId = c.get('userId');
|
||||
const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown';
|
||||
return userId ? `rate:user:${userId}` : `rate:ip:${ip}`;
|
||||
},
|
||||
} = options;
|
||||
|
||||
return async (c: Context, next: Next) => {
|
||||
const key = keyGenerator(c);
|
||||
|
||||
try {
|
||||
// Get current count
|
||||
const current = await redis.get(key);
|
||||
const count = current ? parseInt(current) : 0;
|
||||
|
||||
// Check if limit exceeded
|
||||
if (count >= limit) {
|
||||
const ttl = await redis.pttl(key);
|
||||
|
||||
c.header('X-RateLimit-Limit', limit.toString());
|
||||
c.header('X-RateLimit-Remaining', '0');
|
||||
c.header('X-RateLimit-Reset', new Date(Date.now() + ttl).toISOString());
|
||||
|
||||
return c.json(
|
||||
{
|
||||
error: 'Rate limit exceeded',
|
||||
retryAfter: Math.ceil(ttl / 1000),
|
||||
},
|
||||
429
|
||||
);
|
||||
}
|
||||
|
||||
// Increment counter
|
||||
const pipeline = redis.pipeline();
|
||||
pipeline.incr(key);
|
||||
pipeline.pexpire(key, window);
|
||||
await pipeline.exec();
|
||||
|
||||
// Set headers
|
||||
c.header('X-RateLimit-Limit', limit.toString());
|
||||
c.header('X-RateLimit-Remaining', (limit - count - 1).toString());
|
||||
c.header('X-RateLimit-Reset', new Date(Date.now() + window).toISOString());
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error('Rate limiter error:', error);
|
||||
// Continue on error - don't block requests due to rate limiter failure
|
||||
return next();
|
||||
}
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue