From c893574cb15c4ed5ac9e25e8b0de409c71c46c49 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 18 May 2026 16:59:35 +0200 Subject: [PATCH] KI-AGENT: Matrix-Kontoerstellung nutzbarer machen --- backend/src/modules/matrix.service.ts | 83 +++++++++++++++++++++++--- backend/src/routes/communication.ts | 2 +- frontend/pages/communication/index.vue | 18 +++++- matrix/README.md | 2 + 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/backend/src/modules/matrix.service.ts b/backend/src/modules/matrix.service.ts index 4e14b77..4784e77 100644 --- a/backend/src/modules/matrix.service.ts +++ b/backend/src/modules/matrix.service.ts @@ -1,4 +1,6 @@ -import { createHmac, randomBytes } from "node:crypto" +import { createHash, createHmac, randomBytes } from "node:crypto" +import { existsSync, readFileSync } from "node:fs" +import { resolve } from "node:path" import { FastifyInstance } from "fastify" import { authProfiles, authUsers } from "../../db/schema" import { and, eq } from "drizzle-orm" @@ -10,6 +12,43 @@ type MatrixErrorResponse = { } const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "") +const readLocalDevRegistrationSharedSecret = () => { + if (process.env.NODE_ENV === "production") return "" + + const candidates = [ + resolve(process.cwd(), "../matrix/dev/synapse/homeserver.yaml"), + resolve(process.cwd(), "matrix/dev/synapse/homeserver.yaml"), + ] + + for (const candidate of candidates) { + if (!existsSync(candidate)) continue + + const content = readFileSync(candidate, "utf8") + const match = content.match(/^registration_shared_secret:\s*["']?(.+?)["']?\s*$/m) + + if (match?.[1]) { + return match[1] + } + } + + return "" +} + +const normalizeMatrixLocalpartSeed = (value: string) => { + const normalized = value + .toLowerCase() + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/ä/g, "a") + .replace(/ö/g, "o") + .replace(/ü/g, "u") + .replace(/ß/g, "ss") + .replace(/[^a-z0-9._=-]+/g, "_") + .replace(/_+/g, "_") + .replace(/^[._=-]+|[._=-]+$/g, "") + + return normalized || "user" +} export function matrixService(server: FastifyInstance) { const homeserverUrl = () => @@ -27,13 +66,38 @@ export function matrixService(server: FastifyInstance) { const registrationSharedSecret = () => process.env.MATRIX_REGISTRATION_SHARED_SECRET || secrets.MATRIX_REGISTRATION_SHARED_SECRET || + readLocalDevRegistrationSharedSecret() || "" - const matrixLocalpartForUser = (userId: string) => - `u_${userId.toLowerCase()}` + const getUserIdentitySeed = async (userId: string, tenantId: number | null) => { + const [user] = await server.db + .select({ email: authUsers.email }) + .from(authUsers) + .where(eq(authUsers.id, userId)) + .limit(1) - const matrixUserIdForUser = (userId: string) => - `@${matrixLocalpartForUser(userId)}:${serverName()}` + if (user?.email) { + return user.email.split("@")[0] || user.email + } + + if (tenantId) { + const displayName = await getCurrentUserDisplayName(userId, tenantId) + if (displayName && !displayName.startsWith("@")) { + return displayName + } + } + + return "user" + } + + const matrixLocalpartForUser = async (userId: string, tenantId: number | null) => { + const seed = normalizeMatrixLocalpartSeed(await getUserIdentitySeed(userId, tenantId)) + const hash = createHash("sha256").update(userId).digest("hex").slice(0, 8) + return `${seed}_${hash}` + } + + const matrixUserIdForUser = async (userId: string, tenantId: number | null) => + `@${await matrixLocalpartForUser(userId, tenantId)}:${serverName()}` const buildSharedSecretMac = ( nonce: string, @@ -80,7 +144,7 @@ export function matrixService(server: FastifyInstance) { .where(eq(authUsers.id, userId)) .limit(1) - return user?.email || matrixUserIdForUser(userId) + return user?.email || await matrixUserIdForUser(userId, tenantId) } const requestJson = async (url: string, init?: RequestInit): Promise => { @@ -111,6 +175,7 @@ export function matrixService(server: FastifyInstance) { configured: false, homeserverUrl: homeserverUrl(), serverName: serverName(), + provisioningConfigured: Boolean(registrationSharedSecret()), reachable: false, } } @@ -124,6 +189,7 @@ export function matrixService(server: FastifyInstance) { configured: true, homeserverUrl: homeserverUrl(), serverName: serverName(), + provisioningConfigured: Boolean(registrationSharedSecret()), reachable: true, versions: versions.versions, } @@ -132,6 +198,7 @@ export function matrixService(server: FastifyInstance) { configured: true, homeserverUrl: homeserverUrl(), serverName: serverName(), + provisioningConfigured: Boolean(registrationSharedSecret()), reachable: false, error: err.message, } @@ -146,8 +213,8 @@ export function matrixService(server: FastifyInstance) { ) } - const username = matrixLocalpartForUser(userId) - const matrixUserId = matrixUserIdForUser(userId) + const username = await matrixLocalpartForUser(userId, tenantId) + const matrixUserId = await matrixUserIdForUser(userId, tenantId) const password = randomBytes(32).toString("base64url") const displayName = await getCurrentUserDisplayName(userId, tenantId) diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index 1430268..27c1ede 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -12,7 +12,7 @@ export default async function communicationRoutes(server: FastifyInstance) { const userId = req.user.user_id return { - matrixUserId: matrix.matrixUserIdForUser(userId), + matrixUserId: await matrix.matrixUserIdForUser(userId, req.user.tenant_id), displayName: await matrix.getCurrentUserDisplayName(userId, req.user.tenant_id), } }) diff --git a/frontend/pages/communication/index.vue b/frontend/pages/communication/index.vue index 8512bc6..52d7e16 100644 --- a/frontend/pages/communication/index.vue +++ b/frontend/pages/communication/index.vue @@ -28,6 +28,12 @@ const statusItems = computed(() => [ icon: "i-heroicons-identification", color: "neutral" }, + { + label: "Provisionierung", + value: status.value?.provisioningConfigured ? "Bereit" : "Nicht eingerichtet", + icon: status.value?.provisioningConfigured ? "i-heroicons-key" : "i-heroicons-exclamation-triangle", + color: status.value?.provisioningConfigured ? "success" : "warning" + }, { label: "Erreichbarkeit", value: status.value?.reachable ? "Erreichbar" : "Nicht erreichbar", @@ -133,6 +139,7 @@ onMounted(loadMatrixInfo) :class="{ 'text-success': item.color === 'success', 'text-error': item.color === 'error', + 'text-warning': item.color === 'warning', 'text-muted': item.color === 'neutral' }" /> @@ -196,6 +203,15 @@ onMounted(loadMatrixInfo) title="Matrix-Homeserver nicht erreichbar" :description="status.error || 'Bitte prüfe den lokalen Matrix-Stack und die Backend-Konfiguration.'" /> + +