KI-AGENT: Matrix-Räume in FEDEO provisionieren

This commit is contained in:
2026-05-18 17:24:46 +02:00
parent d0de3cb92e
commit 7f66f66cfa
3 changed files with 287 additions and 2 deletions

View File

@@ -122,6 +122,20 @@ export function matrixService(server: FastifyInstance) {
const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) => const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) =>
`#${tenantSpaceAliasLocalpart(tenant)}:${serverName()}` `#${tenantSpaceAliasLocalpart(tenant)}:${serverName()}`
const tenantRoomAliasLocalpart = (
tenant: { id: number, short?: string | null, name?: string | null },
roomKey: string
) => {
const tenantSeed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`)
const roomSeed = normalizeMatrixAliasSeed(roomKey)
return `fedeo_${tenantSeed}_${tenant.id}_${roomSeed}`
}
const tenantRoomAlias = (
tenant: { id: number, short?: string | null, name?: string | null },
roomKey: string
) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${serverName()}`
const buildSharedSecretMac = ( const buildSharedSecretMac = (
nonce: string, nonce: string,
username: string, username: string,
@@ -459,6 +473,140 @@ export function matrixService(server: FastifyInstance) {
} }
} }
const getTenantRoomStatus = async (
tenantId: number | null,
roomKey: string,
roomName: string
) => {
const tenant = await getCurrentTenant(tenantId)
const alias = tenantRoomAlias(tenant, roomKey)
try {
const directoryEntry = await requestJson<{
room_id: string
servers: string[]
}>(`${homeserverUrl()}/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`)
return {
tenantId: tenant.id,
tenantName: tenant.name,
key: roomKey,
name: roomName,
alias,
exists: true,
roomId: directoryEntry.room_id,
servers: directoryEntry.servers,
}
} catch (err: any) {
if (err.statusCode === 404 || err.errcode === "M_NOT_FOUND") {
return {
tenantId: tenant.id,
tenantName: tenant.name,
key: roomKey,
name: roomName,
alias,
exists: false,
roomId: null,
servers: [],
}
}
throw err
}
}
const provisionTenantRoom = async (
userId: string,
tenantId: number | null,
options: {
key?: string
name?: string
topic?: string
} = {}
) => {
const tenant = await getCurrentTenant(tenantId)
const key = normalizeMatrixAliasSeed(options.key || options.name || "allgemein")
const name = (options.name || "Allgemeiner Chat").trim() || "Allgemeiner Chat"
const topic = (options.topic || `Allgemeiner Kommunikationsraum für ${tenant.name}`).trim()
const existing = await getTenantRoomStatus(tenant.id, key, name)
const userAccount = await provisionCurrentUser(userId, tenant.id)
const tenantSpace = await provisionCurrentTenantSpace(userId, tenant.id)
if (existing.exists) {
return {
...existing,
created: false,
alreadyExisted: true,
parentSpaceRoomId: tenantSpace.roomId,
invitedUserId: userAccount.matrixUserId,
}
}
const serviceLogin = await ensureServiceAccessToken()
const createdRoom = await requestMatrixJson<{ room_id: string }>(
"/_matrix/client/v3/createRoom",
serviceLogin.access_token,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
topic,
preset: "private_chat",
visibility: "private",
room_alias_name: tenantRoomAliasLocalpart(tenant, key),
invite: [userAccount.matrixUserId],
initial_state: [
{
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "invited",
},
},
{
type: "m.space.parent",
state_key: tenantSpace.roomId,
content: {
via: [serverName()],
canonical: true,
},
},
],
}),
}
)
await requestMatrixJson(
`/_matrix/client/v3/rooms/${encodeURIComponent(tenantSpace.roomId)}/state/m.space.child/${encodeURIComponent(createdRoom.room_id)}`,
serviceLogin.access_token,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
via: [serverName()],
suggested: true,
order: "10",
}),
}
)
return {
tenantId: tenant.id,
tenantName: tenant.name,
key,
name,
alias: tenantRoomAlias(tenant, key),
exists: true,
created: true,
alreadyExisted: false,
roomId: createdRoom.room_id,
parentSpaceRoomId: tenantSpace.roomId,
invitedUserId: userAccount.matrixUserId,
serviceUserId: serviceLogin.user_id,
}
}
return { return {
getStatus, getStatus,
matrixUserIdForUser, matrixUserIdForUser,
@@ -466,5 +614,7 @@ export function matrixService(server: FastifyInstance) {
provisionCurrentUser, provisionCurrentUser,
getTenantSpaceStatus, getTenantSpaceStatus,
provisionCurrentTenantSpace, provisionCurrentTenantSpace,
getTenantRoomStatus,
provisionTenantRoom,
} }
} }

View File

@@ -49,4 +49,29 @@ export default async function communicationRoutes(server: FastifyInstance) {
.send({ error: err.message || "Matrix tenant space provisioning failed" }) .send({ error: err.message || "Matrix tenant space provisioning failed" })
} }
}) })
server.get("/communication/matrix/rooms/general", async (req, reply) => {
try {
return await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
} catch (err: any) {
req.log.error(err)
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix room status failed" })
}
})
server.post("/communication/matrix/rooms/general/provision", async (req, reply) => {
try {
return await matrix.provisionTenantRoom(req.user.user_id, req.user.tenant_id, {
key: "allgemein",
name: "Allgemeiner Chat",
})
} catch (err: any) {
req.log.error(err)
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix room provisioning failed" })
}
})
} }

View File

@@ -5,11 +5,14 @@ const { $api } = useNuxtApp()
const status = ref(null) const status = ref(null)
const identity = ref(null) const identity = ref(null)
const tenantSpace = ref(null) const tenantSpace = ref(null)
const generalRoom = ref(null)
const provisionResult = ref(null) const provisionResult = ref(null)
const tenantSpaceProvisionResult = ref(null) const tenantSpaceProvisionResult = ref(null)
const generalRoomProvisionResult = ref(null)
const loading = ref(false) const loading = ref(false)
const provisioning = ref(false) const provisioning = ref(false)
const tenantSpaceProvisioning = ref(false) const tenantSpaceProvisioning = ref(false)
const generalRoomProvisioning = ref(false)
const lastUpdated = ref(null) const lastUpdated = ref(null)
const statusItems = computed(() => [ const statusItems = computed(() => [
@@ -48,15 +51,17 @@ const statusItems = computed(() => [
const loadMatrixInfo = async () => { const loadMatrixInfo = async () => {
loading.value = true loading.value = true
try { try {
const [statusRes, identityRes, tenantSpaceRes] = await Promise.all([ const [statusRes, identityRes, tenantSpaceRes, generalRoomRes] = await Promise.all([
$api("/api/communication/matrix/status"), $api("/api/communication/matrix/status"),
$api("/api/communication/matrix/me"), $api("/api/communication/matrix/me"),
$api("/api/communication/matrix/tenant-space") $api("/api/communication/matrix/tenant-space"),
$api("/api/communication/matrix/rooms/general")
]) ])
status.value = statusRes status.value = statusRes
identity.value = identityRes identity.value = identityRes
tenantSpace.value = tenantSpaceRes tenantSpace.value = tenantSpaceRes
generalRoom.value = generalRoomRes
lastUpdated.value = new Date() lastUpdated.value = new Date()
} catch (error) { } catch (error) {
toast.add({ toast.add({
@@ -127,6 +132,44 @@ const provisionTenantSpace = async () => {
} }
} }
const provisionGeneralRoom = async () => {
generalRoomProvisioning.value = true
try {
const res = await $api("/api/communication/matrix/rooms/general/provision", {
method: "POST"
})
generalRoomProvisionResult.value = res
generalRoom.value = {
tenantId: res.tenantId,
tenantName: res.tenantName,
key: res.key,
name: res.name,
alias: res.alias,
exists: true,
roomId: res.roomId,
servers: res.servers || []
}
tenantSpace.value = {
...tenantSpace.value,
exists: true,
roomId: res.parentSpaceRoomId || tenantSpace.value?.roomId
}
toast.add({
title: res.alreadyExisted ? "Allgemeiner Chat ist bereits vorhanden" : "Allgemeiner Chat erstellt",
color: "success"
})
} catch (error) {
toast.add({
title: "Allgemeiner Chat konnte nicht erstellt werden",
color: "error"
})
} finally {
generalRoomProvisioning.value = false
}
}
const formatDateTime = (value) => { const formatDateTime = (value) => {
if (!value) return "-" if (!value) return "-"
@@ -334,6 +377,73 @@ onMounted(loadMatrixInfo)
</div> </div>
</template> </template>
</UCard> </UCard>
<UCard :ui="{ root: 'rounded-lg' }">
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 text-primary" />
<h2 class="text-base font-semibold text-highlighted">
Allgemeiner Chat
</h2>
</div>
</template>
<div class="space-y-4">
<div class="grid gap-3 sm:grid-cols-2">
<div>
<p class="text-xs font-medium uppercase text-muted">
Alias
</p>
<p class="mt-1 break-all font-mono text-sm text-highlighted">
{{ generalRoom?.alias || "-" }}
</p>
</div>
<div>
<p class="text-xs font-medium uppercase text-muted">
Status
</p>
<UBadge
class="mt-1"
:color="generalRoom?.exists ? 'success' : 'neutral'"
variant="soft"
>
{{ generalRoom?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
</UBadge>
</div>
</div>
<div>
<p class="text-xs font-medium uppercase text-muted">
Raum-ID
</p>
<p class="mt-1 break-all font-mono text-sm text-highlighted">
{{ generalRoom?.roomId || "-" }}
</p>
</div>
<UAlert
v-if="generalRoomProvisionResult"
icon="i-heroicons-check-circle"
color="success"
variant="soft"
:title="generalRoomProvisionResult.alreadyExisted ? 'Allgemeiner Chat vorhanden' : 'Allgemeiner Chat erstellt'"
:description="generalRoomProvisionResult.alias"
/>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
icon="i-heroicons-plus"
:loading="generalRoomProvisioning"
:disabled="!status?.reachable || !status?.provisioningConfigured"
@click="provisionGeneralRoom"
>
Allgemeinen Chat erstellen
</UButton>
</div>
</template>
</UCard>
</div> </div>
<UCard :ui="{ root: 'rounded-lg' }"> <UCard :ui="{ root: 'rounded-lg' }">