KI-AGENT: Erste Matrix-Backendintegration ergänzen

This commit is contained in:
2026-05-18 15:37:12 +02:00
parent 54ae136f0d
commit b322d0c173
6 changed files with 250 additions and 1 deletions

View File

@@ -28,3 +28,7 @@ MATRIX_DEV_LIVEKIT_RTC_MAX_PORT=50100
MATRIX_DEV_TURN_PORT=3478
MATRIX_DEV_TURN_MIN_PORT=49160
MATRIX_DEV_TURN_MAX_PORT=49200
# Backend-Integration gegen den lokalen Matrix-Stack
MATRIX_HOMESERVER_URL=http://localhost:8008
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml

View File

@@ -31,6 +31,7 @@ import publiclinksAuthenticatedRoutes from "./routes/publiclinks/publiclinks-aut
import wikiRoutes from "./routes/wiki";
import portalContractRoutes from "./routes/portal/contracts";
import mcpRoutes from "./routes/mcp";
import communicationRoutes from "./routes/communication";
//Public Links
import publiclinksNonAuthenticatedRoutes from "./routes/publiclinks/publiclinks-non-authenticated";
@@ -150,6 +151,7 @@ async function main() {
await subApp.register(wikiRoutes);
await subApp.register(portalContractRoutes);
await subApp.register(mcpRoutes);
await subApp.register(communicationRoutes);
},{prefix: "/api"})

View File

@@ -0,0 +1,201 @@
import { createHmac, randomBytes } from "node:crypto"
import { FastifyInstance } from "fastify"
import { authProfiles, authUsers } from "../../db/schema"
import { and, eq } from "drizzle-orm"
import { secrets } from "../utils/secrets"
type MatrixErrorResponse = {
errcode?: string
error?: string
}
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
export function matrixService(server: FastifyInstance) {
const homeserverUrl = () =>
trimTrailingSlash(
process.env.MATRIX_HOMESERVER_URL ||
secrets.MATRIX_HOMESERVER_URL ||
"http://localhost:8008"
)
const serverName = () =>
process.env.MATRIX_SERVER_NAME ||
secrets.MATRIX_SERVER_NAME ||
"localhost"
const registrationSharedSecret = () =>
process.env.MATRIX_REGISTRATION_SHARED_SECRET ||
secrets.MATRIX_REGISTRATION_SHARED_SECRET ||
""
const matrixLocalpartForUser = (userId: string) =>
`u_${userId.toLowerCase()}`
const matrixUserIdForUser = (userId: string) =>
`@${matrixLocalpartForUser(userId)}:${serverName()}`
const buildSharedSecretMac = (
nonce: string,
username: string,
password: string,
admin: boolean
) => {
const hmac = createHmac("sha1", registrationSharedSecret())
hmac.update(nonce)
hmac.update("\0")
hmac.update(username)
hmac.update("\0")
hmac.update(password)
hmac.update("\0")
hmac.update(admin ? "admin" : "notadmin")
return hmac.digest("hex")
}
const getCurrentUserDisplayName = async (userId: string, tenantId: number | null) => {
if (tenantId) {
const [profile] = await server.db
.select({
firstName: authProfiles.first_name,
lastName: authProfiles.last_name,
})
.from(authProfiles)
.where(and(
eq(authProfiles.user_id, userId),
eq(authProfiles.tenant_id, tenantId)
))
.limit(1)
const profileName = [profile?.firstName, profile?.lastName]
.filter(Boolean)
.join(" ")
.trim()
if (profileName) return profileName
}
const [user] = await server.db
.select({ email: authUsers.email })
.from(authUsers)
.where(eq(authUsers.id, userId))
.limit(1)
return user?.email || matrixUserIdForUser(userId)
}
const requestJson = async <T>(url: string, init?: RequestInit): Promise<T> => {
const response = await fetch(url, init)
const text = await response.text()
const body = text ? JSON.parse(text) : {}
if (!response.ok) {
const error = body as MatrixErrorResponse
throw Object.assign(
new Error(error.error || `Matrix request failed with ${response.status}`),
{
statusCode: response.status,
errcode: error.errcode,
body,
}
)
}
return body as T
}
const getStatus = async () => {
const configured = Boolean(homeserverUrl() && serverName())
if (!configured) {
return {
configured: false,
homeserverUrl: homeserverUrl(),
serverName: serverName(),
reachable: false,
}
}
try {
const versions = await requestJson<{ versions: string[] }>(
`${homeserverUrl()}/_matrix/client/versions`
)
return {
configured: true,
homeserverUrl: homeserverUrl(),
serverName: serverName(),
reachable: true,
versions: versions.versions,
}
} catch (err: any) {
return {
configured: true,
homeserverUrl: homeserverUrl(),
serverName: serverName(),
reachable: false,
error: err.message,
}
}
}
const provisionCurrentUser = async (userId: string, tenantId: number | null) => {
if (!registrationSharedSecret()) {
throw Object.assign(
new Error("MATRIX_REGISTRATION_SHARED_SECRET is not configured"),
{ statusCode: 503 }
)
}
const username = matrixLocalpartForUser(userId)
const matrixUserId = matrixUserIdForUser(userId)
const password = randomBytes(32).toString("base64url")
const displayName = await getCurrentUserDisplayName(userId, tenantId)
const nonceResponse = await requestJson<{ nonce: string }>(
`${homeserverUrl()}/_synapse/admin/v1/register`
)
const mac = buildSharedSecretMac(nonceResponse.nonce, username, password, false)
try {
await requestJson(`${homeserverUrl()}/_synapse/admin/v1/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nonce: nonceResponse.nonce,
username,
password,
admin: false,
mac,
}),
})
return {
matrixUserId,
localpart: username,
displayName,
created: true,
alreadyExisted: false,
}
} catch (err: any) {
if (err.errcode === "M_USER_IN_USE") {
return {
matrixUserId,
localpart: username,
displayName,
created: false,
alreadyExisted: true,
}
}
throw err
}
}
return {
getStatus,
matrixUserIdForUser,
getCurrentUserDisplayName,
provisionCurrentUser,
}
}

View File

@@ -0,0 +1,30 @@
import { FastifyInstance } from "fastify"
import { matrixService } from "../modules/matrix.service"
export default async function communicationRoutes(server: FastifyInstance) {
const matrix = matrixService(server)
server.get("/communication/matrix/status", async () => {
return matrix.getStatus()
})
server.get("/communication/matrix/me", async (req) => {
const userId = req.user.user_id
return {
matrixUserId: matrix.matrixUserIdForUser(userId),
displayName: await matrix.getCurrentUserDisplayName(userId, req.user.tenant_id),
}
})
server.post("/communication/matrix/me/provision", async (req, reply) => {
try {
return await matrix.provisionCurrentUser(req.user.user_id, req.user.tenant_id)
} catch (err: any) {
req.log.error(err)
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix provisioning failed" })
}
})
}

View File

@@ -38,6 +38,9 @@ export let secrets = {
DOKUBOX_IMAP_PASSWORD: string
OPENAI_API_KEY: string
STIRLING_API_KEY: string
MATRIX_HOMESERVER_URL?: string
MATRIX_SERVER_NAME?: string
MATRIX_REGISTRATION_SHARED_SECRET?: string
}
export async function loadSecrets () {
@@ -58,4 +61,3 @@ export async function loadSecrets () {
console.log("✅ Secrets aus Infisical geladen");
console.log(Object.keys(secrets).length + " Stück")
}

View File

@@ -172,3 +172,13 @@ docker compose --profile matrix-dev exec matrix-dev-synapse \
Anschließend Element Web unter `http://localhost:8080` öffnen und mit dem lokalen Matrix-Nutzer anmelden.
Wenn FEDEO selbst parallel lokal laufen soll, starte die FEDEO-Dienste separat wie gewohnt. Der lokale Matrix-Stack ist absichtlich über direkte Ports erreichbar, damit er unabhängig von DNS, TLS und Traefik getestet werden kann.
## Erste FEDEO-Backend-Integration
Das Backend stellt geschützte Matrix-Endpunkte unter `/api/communication/matrix/*` bereit:
- `GET /api/communication/matrix/status`: prüft Konfiguration und Erreichbarkeit des Matrix-Homeservers
- `GET /api/communication/matrix/me`: zeigt die aus dem FEDEO-Nutzer abgeleitete Matrix-ID
- `POST /api/communication/matrix/me/provision`: legt den Matrix-Account für den angemeldeten FEDEO-Nutzer per Synapse-Shared-Secret-Registrierung an
Für lokale Provisionierung muss `MATRIX_REGISTRATION_SHARED_SECRET` aus `matrix/dev/synapse/homeserver.yaml` in der Backend-Umgebung gesetzt werden. Die lokale Synapse-Konfiguration ist absichtlich nicht versioniert, weil sie Secrets enthält.