KI-AGENT: Matrix-Kontoerstellung nutzbarer machen
This commit is contained in:
@@ -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 { FastifyInstance } from "fastify"
|
||||||
import { authProfiles, authUsers } from "../../db/schema"
|
import { authProfiles, authUsers } from "../../db/schema"
|
||||||
import { and, eq } from "drizzle-orm"
|
import { and, eq } from "drizzle-orm"
|
||||||
@@ -10,6 +12,43 @@ type MatrixErrorResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
|
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) {
|
export function matrixService(server: FastifyInstance) {
|
||||||
const homeserverUrl = () =>
|
const homeserverUrl = () =>
|
||||||
@@ -27,13 +66,38 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
const registrationSharedSecret = () =>
|
const registrationSharedSecret = () =>
|
||||||
process.env.MATRIX_REGISTRATION_SHARED_SECRET ||
|
process.env.MATRIX_REGISTRATION_SHARED_SECRET ||
|
||||||
secrets.MATRIX_REGISTRATION_SHARED_SECRET ||
|
secrets.MATRIX_REGISTRATION_SHARED_SECRET ||
|
||||||
|
readLocalDevRegistrationSharedSecret() ||
|
||||||
""
|
""
|
||||||
|
|
||||||
const matrixLocalpartForUser = (userId: string) =>
|
const getUserIdentitySeed = async (userId: string, tenantId: number | null) => {
|
||||||
`u_${userId.toLowerCase()}`
|
const [user] = await server.db
|
||||||
|
.select({ email: authUsers.email })
|
||||||
|
.from(authUsers)
|
||||||
|
.where(eq(authUsers.id, userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
const matrixUserIdForUser = (userId: string) =>
|
if (user?.email) {
|
||||||
`@${matrixLocalpartForUser(userId)}:${serverName()}`
|
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 = (
|
const buildSharedSecretMac = (
|
||||||
nonce: string,
|
nonce: string,
|
||||||
@@ -80,7 +144,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
.where(eq(authUsers.id, userId))
|
.where(eq(authUsers.id, userId))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
return user?.email || matrixUserIdForUser(userId)
|
return user?.email || await matrixUserIdForUser(userId, tenantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestJson = async <T>(url: string, init?: RequestInit): Promise<T> => {
|
const requestJson = async <T>(url: string, init?: RequestInit): Promise<T> => {
|
||||||
@@ -111,6 +175,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
configured: false,
|
configured: false,
|
||||||
homeserverUrl: homeserverUrl(),
|
homeserverUrl: homeserverUrl(),
|
||||||
serverName: serverName(),
|
serverName: serverName(),
|
||||||
|
provisioningConfigured: Boolean(registrationSharedSecret()),
|
||||||
reachable: false,
|
reachable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +189,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
configured: true,
|
configured: true,
|
||||||
homeserverUrl: homeserverUrl(),
|
homeserverUrl: homeserverUrl(),
|
||||||
serverName: serverName(),
|
serverName: serverName(),
|
||||||
|
provisioningConfigured: Boolean(registrationSharedSecret()),
|
||||||
reachable: true,
|
reachable: true,
|
||||||
versions: versions.versions,
|
versions: versions.versions,
|
||||||
}
|
}
|
||||||
@@ -132,6 +198,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
configured: true,
|
configured: true,
|
||||||
homeserverUrl: homeserverUrl(),
|
homeserverUrl: homeserverUrl(),
|
||||||
serverName: serverName(),
|
serverName: serverName(),
|
||||||
|
provisioningConfigured: Boolean(registrationSharedSecret()),
|
||||||
reachable: false,
|
reachable: false,
|
||||||
error: err.message,
|
error: err.message,
|
||||||
}
|
}
|
||||||
@@ -146,8 +213,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = matrixLocalpartForUser(userId)
|
const username = await matrixLocalpartForUser(userId, tenantId)
|
||||||
const matrixUserId = matrixUserIdForUser(userId)
|
const matrixUserId = await matrixUserIdForUser(userId, tenantId)
|
||||||
const password = randomBytes(32).toString("base64url")
|
const password = randomBytes(32).toString("base64url")
|
||||||
const displayName = await getCurrentUserDisplayName(userId, tenantId)
|
const displayName = await getCurrentUserDisplayName(userId, tenantId)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
const userId = req.user.user_id
|
const userId = req.user.user_id
|
||||||
|
|
||||||
return {
|
return {
|
||||||
matrixUserId: matrix.matrixUserIdForUser(userId),
|
matrixUserId: await matrix.matrixUserIdForUser(userId, req.user.tenant_id),
|
||||||
displayName: await matrix.getCurrentUserDisplayName(userId, req.user.tenant_id),
|
displayName: await matrix.getCurrentUserDisplayName(userId, req.user.tenant_id),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ const statusItems = computed(() => [
|
|||||||
icon: "i-heroicons-identification",
|
icon: "i-heroicons-identification",
|
||||||
color: "neutral"
|
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",
|
label: "Erreichbarkeit",
|
||||||
value: status.value?.reachable ? "Erreichbar" : "Nicht erreichbar",
|
value: status.value?.reachable ? "Erreichbar" : "Nicht erreichbar",
|
||||||
@@ -133,6 +139,7 @@ onMounted(loadMatrixInfo)
|
|||||||
:class="{
|
:class="{
|
||||||
'text-success': item.color === 'success',
|
'text-success': item.color === 'success',
|
||||||
'text-error': item.color === 'error',
|
'text-error': item.color === 'error',
|
||||||
|
'text-warning': item.color === 'warning',
|
||||||
'text-muted': item.color === 'neutral'
|
'text-muted': item.color === 'neutral'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -196,6 +203,15 @@ onMounted(loadMatrixInfo)
|
|||||||
title="Matrix-Homeserver nicht erreichbar"
|
title="Matrix-Homeserver nicht erreichbar"
|
||||||
:description="status.error || 'Bitte prüfe den lokalen Matrix-Stack und die Backend-Konfiguration.'"
|
:description="status.error || 'Bitte prüfe den lokalen Matrix-Stack und die Backend-Konfiguration.'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-else-if="status && !status.provisioningConfigured"
|
||||||
|
icon="i-heroicons-key"
|
||||||
|
color="warning"
|
||||||
|
variant="soft"
|
||||||
|
title="Matrix-Provisionierung nicht eingerichtet"
|
||||||
|
description="Bitte setze MATRIX_REGISTRATION_SHARED_SECRET in der Backend-Umgebung."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -206,7 +222,7 @@ onMounted(loadMatrixInfo)
|
|||||||
<UButton
|
<UButton
|
||||||
icon="i-heroicons-user-plus"
|
icon="i-heroicons-user-plus"
|
||||||
:loading="provisioning"
|
:loading="provisioning"
|
||||||
:disabled="!status?.reachable"
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
||||||
@click="provisionMatrixAccount"
|
@click="provisionMatrixAccount"
|
||||||
>
|
>
|
||||||
Matrix-Konto erstellen
|
Matrix-Konto erstellen
|
||||||
|
|||||||
@@ -182,3 +182,5 @@ Das Backend stellt geschützte Matrix-Endpunkte unter `/api/communication/matrix
|
|||||||
- `POST /api/communication/matrix/me/provision`: legt den Matrix-Account für den angemeldeten FEDEO-Nutzer per Synapse-Shared-Secret-Registrierung an
|
- `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.
|
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.
|
||||||
|
|
||||||
|
In der lokalen Entwicklung liest das Backend dieses Secret als Fallback direkt aus `matrix/dev/synapse/homeserver.yaml`, sofern `NODE_ENV` nicht `production` ist. Auf Servern muss das Secret weiterhin explizit über die Umgebung oder das Secret-Management gesetzt werden.
|
||||||
|
|||||||
Reference in New Issue
Block a user