MCP um dauerhafte tenantgebundene Tokens erweitern
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 23s
Build and Push Docker Images / build-frontend (push) Successful in 1m4s
Build and Push Docker Images / build-docs (push) Successful in 15s

This commit is contained in:
2026-05-12 17:39:20 +02:00
parent 252021acee
commit 4e49dd18a1
2 changed files with 241 additions and 11 deletions

View File

@@ -2,16 +2,67 @@ 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
@@ -30,11 +81,31 @@ export default fp(async (server: FastifyInstance) => {
}
try {
// 2⃣ JWT verifizieren
const payload = jwt.verify(token, secrets.JWT_SECRET!) as {
// 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) {
@@ -44,15 +115,19 @@ export default fp(async (server: FastifyInstance) => {
// Payload an Request hängen
req.user = payload
const [currentUser] = await server.db
.select({
is_admin: authUsers.is_admin,
})
.from(authUsers)
.where(eq(authUsers.id, payload.user_id))
.limit(1)
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)
req.user.is_admin = Boolean(currentUser?.is_admin)
}
// Multi-Tenant Modus ohne ausgewählten Tenant → keine Rollenprüfung
if (!req.user.tenant_id) {