KI-AGENT: Erste Matrix-Backendintegration ergänzen
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
|
||||
|
||||
201
backend/src/modules/matrix.service.ts
Normal file
201
backend/src/modules/matrix.service.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
30
backend/src/routes/communication.ts
Normal file
30
backend/src/routes/communication.ts
Normal 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" })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user