import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import { secrets } from "../utils/secrets"; import { and, eq } from "drizzle-orm"; import { authUsers, m2mApiKeys } from "../../db/schema"; import { createHash } from "node:crypto"; /** * Fastify Plugin für Machine-to-Machine Authentifizierung. * * Dieses Plugin prüft, ob der Header `x-api-key` vorhanden ist * und mit dem in der .env hinterlegten M2M_API_KEY übereinstimmt. * * Verwendung: * server.register(m2mAuthPlugin, { allowedPrefix: '/internal' }) */ export default fp(async (server: FastifyInstance, opts: { allowedPrefix?: string } = {}) => { const hashApiKey = (apiKey: string) => createHash("sha256").update(apiKey, "utf8").digest("hex") server.addHook("preHandler", async (req, reply) => { try { const apiKeyHeader = req.headers["x-api-key"]; const apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader; if (!apiKey) { server.log.warn(`[M2M Auth] Ungültiger oder fehlender API-Key bei ${req.url}`); return reply.status(401).send({ error: "Unauthorized" }); } const keyHash = hashApiKey(apiKey); const keyRows = await server.db .select({ id: m2mApiKeys.id, tenantId: m2mApiKeys.tenantId, userId: m2mApiKeys.userId, active: m2mApiKeys.active, expiresAt: m2mApiKeys.expiresAt, name: m2mApiKeys.name, userEmail: authUsers.email, }) .from(m2mApiKeys) .innerJoin(authUsers, eq(authUsers.id, m2mApiKeys.userId)) .where(and( eq(m2mApiKeys.keyHash, keyHash), eq(m2mApiKeys.active, true) )) .limit(1) let key = keyRows[0] if (!key) { const fallbackValid = apiKey === secrets.M2M_API_KEY if (!fallbackValid) { server.log.warn(`[M2M Auth] Ungültiger API-Key bei ${req.url}`) return reply.status(401).send({ error: "Unauthorized" }) } // Backward compatibility mode for one global key. // The caller must provide user/tenant identifiers in headers. const tenantIdHeader = req.headers["x-tenant-id"] const userIdHeader = req.headers["x-user-id"] const tenantId = Number(Array.isArray(tenantIdHeader) ? tenantIdHeader[0] : tenantIdHeader) const userId = Array.isArray(userIdHeader) ? userIdHeader[0] : userIdHeader if (!tenantId || !userId) { return reply.status(401).send({ error: "Missing x-tenant-id or x-user-id for legacy M2M key" }) } const users = await server.db .select({ email: authUsers.email }) .from(authUsers) .where(eq(authUsers.id, userId)) .limit(1) if (!users[0]) { return reply.status(401).send({ error: "Unknown user for legacy M2M key" }) } req.user = { user_id: userId, email: users[0].email, tenant_id: tenantId } } else { if (key.expiresAt && new Date(key.expiresAt).getTime() < Date.now()) { return reply.status(401).send({ error: "Expired API key" }) } req.user = { user_id: key.userId, email: key.userEmail, tenant_id: key.tenantId } await server.db .update(m2mApiKeys) .set({ lastUsedAt: new Date(), updatedAt: new Date() }) .where(eq(m2mApiKeys.id, key.id)) } (req as any).m2m = { verified: true, type: "internal", key: apiKey, }; req.role = "m2m" req.permissions = [] req.hasPermission = () => false } catch (err) { // @ts-ignore server.log.error("[M2M Auth] Fehler beim Prüfen des API-Keys:", err); return reply.status(500).send({ error: "Internal Server Error" }); } }); }); declare module "fastify" { interface FastifyRequest { m2m?: { verified: boolean; type: "internal"; key: string; }; } }