MCP um dauerhafte tenantgebundene Tokens erweitern
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user