import { FastifyInstance } from "fastify" import fp from "fastify-plugin" import jwt from "jsonwebtoken" import { secrets } from "../utils/secrets" import { createHash } from "node:crypto" import { authUserRoles, authRolePermissions, authUsers, m2mApiKeys, } from "../../db/schema" import { eq, and, inArray } from "drizzle-orm" export default fp(async (server: FastifyInstance) => { const hashApiKey = (apiKey: string) => createHash("sha256").update(apiKey, "utf8").digest("hex") const isMcpRoute = (url: string) => url === "/mcp" || url.startsWith("/mcp/") || url === "/api/mcp" || url.startsWith("/api/mcp/") const authenticateMcpApiKey = async (apiKey: string) => { if (!apiKey.startsWith("fedeo_mcp_")) return false const keyHash = hashApiKey(apiKey) const rows = await server.db .select({ id: m2mApiKeys.id, tenantId: m2mApiKeys.tenantId, userId: m2mApiKeys.userId, active: m2mApiKeys.active, expiresAt: m2mApiKeys.expiresAt, userEmail: authUsers.email, isAdmin: authUsers.is_admin, }) .from(m2mApiKeys) .innerJoin(authUsers, eq(authUsers.id, m2mApiKeys.userId)) .where(and( eq(m2mApiKeys.keyHash, keyHash), eq(m2mApiKeys.active, true) )) .limit(1) const key = rows[0] if (!key) return false if (key.expiresAt && new Date(key.expiresAt).getTime() < Date.now()) return false await server.db .update(m2mApiKeys) .set({ lastUsedAt: new Date(), updatedAt: new Date() }) .where(eq(m2mApiKeys.id, key.id)) return { user_id: key.userId, email: key.userEmail, tenant_id: key.tenantId, is_admin: Boolean(key.isAdmin), } } server.addHook("preHandler", async (req, reply) => { // 1️⃣ Token aus Header oder Cookie lesen const cookieToken = req.cookies?.token const authHeader = req.headers.authorization const headerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null const token = headerToken && headerToken.length > 10 ? headerToken : cookieToken || null if (!token) { return reply.code(401).send({ error: "Authentication required" }) } try { // 2️⃣ JWT verifizieren oder für MCP dauerhaften Token akzeptieren let payload: { user_id: string email: string tenant_id: number | null is_admin?: boolean } try { payload = jwt.verify(token, secrets.JWT_SECRET!) as { user_id: string email: string tenant_id: number | null } } catch (jwtError) { const mcpPayload = isMcpRoute(req.url) ? await authenticateMcpApiKey(token) : false if (!mcpPayload) { throw jwtError } payload = mcpPayload ;(req as any).mcpTokenAuth = true } if (!payload?.user_id) { return reply.code(401).send({ error: "Invalid token" }) } // Payload an Request hängen req.user = payload if (typeof payload.is_admin === "boolean") { req.user.is_admin = payload.is_admin } else { const [currentUser] = await server.db .select({ is_admin: authUsers.is_admin, }) .from(authUsers) .where(eq(authUsers.id, payload.user_id)) .limit(1) req.user.is_admin = Boolean(currentUser?.is_admin) } // Multi-Tenant Modus ohne ausgewählten Tenant → keine Rollenprüfung if (!req.user.tenant_id) { return } const tenantId = req.user.tenant_id const userId = req.user.user_id // -------------------------------------------------------- // 3️⃣ Rollen des Nutzers im Tenant holen // -------------------------------------------------------- const roleRows = await server.db .select({ role_id: authUserRoles.role_id, }) .from(authUserRoles) .where( and( eq(authUserRoles.user_id, userId), eq(authUserRoles.tenant_id, tenantId) ) ) if (roleRows.length === 0) { if (req.user.is_admin) { req.role = "" req.permissions = [] req.hasPermission = () => false return } return reply .code(403) .send({ error: "No role assigned for this tenant" }) } const roleIds = Array.from(new Set(roleRows.map((role) => role.role_id))) // -------------------------------------------------------- // 4️⃣ Berechtigungen der Rollen laden // -------------------------------------------------------- const permissionRows = await server.db .select() .from(authRolePermissions) .where(inArray(authRolePermissions.role_id, roleIds)) const permissions = Array.from(new Set(permissionRows.map((p) => p.permission))) // -------------------------------------------------------- // 5️⃣ An Request hängen für spätere Nutzung // -------------------------------------------------------- req.role = roleIds[0] req.permissions = permissions req.hasPermission = (perm: string) => permissions.includes(perm) } catch (err) { console.error("JWT verification error:", err) return reply.code(401).send({ error: "Invalid or expired token" }) } }) }) // --------------------------------------------------------------------------- // Fastify TypeScript Erweiterungen // --------------------------------------------------------------------------- declare module "fastify" { interface FastifyRequest { user: { user_id: string email: string tenant_id: number | null is_admin?: boolean } role: string permissions: string[] hasPermission: (permission: string) => boolean } }