KI-AGENT: Erste Matrix-Backendintegration ergänzen
This commit is contained in:
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user