59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
import fp from 'fastify-plugin';
|
|
import { eq } from 'drizzle-orm';
|
|
import { verifyToken, isAccessPayload } from '../utils/jwt.js';
|
|
import { unauthorized, forbidden } from '../utils/errors.js';
|
|
import { users } from '../db/schema/users.js';
|
|
|
|
declare module 'fastify' {
|
|
interface FastifyInstance {
|
|
authenticate: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
authenticateAdmin: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
}
|
|
interface FastifyRequest {
|
|
user?: { id: string; email: string };
|
|
}
|
|
}
|
|
|
|
export async function authenticate(req: FastifyRequest, _reply: FastifyReply): Promise<void> {
|
|
const authHeader = req.headers.authorization;
|
|
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
throw unauthorized('Missing or invalid authorization header');
|
|
}
|
|
|
|
const token = authHeader.slice(7);
|
|
|
|
try {
|
|
const payload = await verifyToken(token);
|
|
|
|
if (!isAccessPayload(payload)) {
|
|
throw unauthorized('Invalid token type');
|
|
}
|
|
|
|
req.user = { id: payload.sub, email: payload.email };
|
|
} catch {
|
|
throw unauthorized('Invalid or expired token');
|
|
}
|
|
}
|
|
|
|
export async function authenticateAdmin(req: FastifyRequest, _reply: FastifyReply): Promise<void> {
|
|
if (!req.user) {
|
|
throw unauthorized('Authentication required');
|
|
}
|
|
|
|
const [user] = await req.server.db.select({ role: users.role }).from(users).where(eq(users.id, req.user.id));
|
|
|
|
if (!user || user.role !== 'admin') {
|
|
throw forbidden('Admin access required');
|
|
}
|
|
}
|
|
|
|
const authPlugin = async (app: FastifyInstance) => {
|
|
app.decorateRequest('user', undefined);
|
|
app.decorate('authenticate', authenticate);
|
|
app.decorate('authenticateAdmin', authenticateAdmin);
|
|
};
|
|
|
|
export default fp(authPlugin, { name: 'auth', dependencies: ['database'] });
|