KI-AGENT: Matrix-Mandanten-Space provisionieren
This commit is contained in:
@@ -32,3 +32,4 @@ MATRIX_DEV_TURN_MAX_PORT=49200
|
|||||||
# Backend-Integration gegen den lokalen Matrix-Stack
|
# Backend-Integration gegen den lokalen Matrix-Stack
|
||||||
MATRIX_HOMESERVER_URL=http://localhost:8008
|
MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
|
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
|
||||||
|
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createHash, createHmac, randomBytes } from "node:crypto"
|
|||||||
import { existsSync, readFileSync } from "node:fs"
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
import { resolve } from "node:path"
|
import { resolve } from "node:path"
|
||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { authProfiles, authUsers } from "../../db/schema"
|
import { authProfiles, authUsers, tenants } from "../../db/schema"
|
||||||
import { and, eq } from "drizzle-orm"
|
import { and, eq } from "drizzle-orm"
|
||||||
import { secrets } from "../utils/secrets"
|
import { secrets } from "../utils/secrets"
|
||||||
|
|
||||||
@@ -50,6 +50,11 @@ const normalizeMatrixLocalpartSeed = (value: string) => {
|
|||||||
return normalized || "user"
|
return normalized || "user"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeMatrixAliasSeed = (value: string) =>
|
||||||
|
normalizeMatrixLocalpartSeed(value)
|
||||||
|
.replace(/[.=]/g, "_")
|
||||||
|
.replace(/_+/g, "_")
|
||||||
|
|
||||||
export function matrixService(server: FastifyInstance) {
|
export function matrixService(server: FastifyInstance) {
|
||||||
const homeserverUrl = () =>
|
const homeserverUrl = () =>
|
||||||
trimTrailingSlash(
|
trimTrailingSlash(
|
||||||
@@ -69,6 +74,16 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
readLocalDevRegistrationSharedSecret() ||
|
readLocalDevRegistrationSharedSecret() ||
|
||||||
""
|
""
|
||||||
|
|
||||||
|
const serviceUserLocalpart = () =>
|
||||||
|
process.env.MATRIX_SERVICE_USER_LOCALPART ||
|
||||||
|
secrets.MATRIX_SERVICE_USER_LOCALPART ||
|
||||||
|
"fedeo_service"
|
||||||
|
|
||||||
|
const serviceUserPassword = () =>
|
||||||
|
createHmac("sha256", registrationSharedSecret())
|
||||||
|
.update(`${serverName()}:fedeo-service-user`)
|
||||||
|
.digest("base64url")
|
||||||
|
|
||||||
const getUserIdentitySeed = async (userId: string, tenantId: number | null) => {
|
const getUserIdentitySeed = async (userId: string, tenantId: number | null) => {
|
||||||
const [user] = await server.db
|
const [user] = await server.db
|
||||||
.select({ email: authUsers.email })
|
.select({ email: authUsers.email })
|
||||||
@@ -99,6 +114,14 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
const matrixUserIdForUser = async (userId: string, tenantId: number | null) =>
|
const matrixUserIdForUser = async (userId: string, tenantId: number | null) =>
|
||||||
`@${await matrixLocalpartForUser(userId, tenantId)}:${serverName()}`
|
`@${await matrixLocalpartForUser(userId, tenantId)}:${serverName()}`
|
||||||
|
|
||||||
|
const tenantSpaceAliasLocalpart = (tenant: { id: number, short?: string | null, name?: string | null }) => {
|
||||||
|
const seed = normalizeMatrixAliasSeed(tenant.short || tenant.name || `tenant_${tenant.id}`)
|
||||||
|
return `fedeo_${seed}_${tenant.id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantSpaceAlias = (tenant: { id: number, short?: string | null, name?: string | null }) =>
|
||||||
|
`#${tenantSpaceAliasLocalpart(tenant)}:${serverName()}`
|
||||||
|
|
||||||
const buildSharedSecretMac = (
|
const buildSharedSecretMac = (
|
||||||
nonce: string,
|
nonce: string,
|
||||||
username: string,
|
username: string,
|
||||||
@@ -116,6 +139,69 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
return hmac.digest("hex")
|
return hmac.digest("hex")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerWithSharedSecret = async (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
admin: boolean
|
||||||
|
) => {
|
||||||
|
const nonceResponse = await requestJson<{ nonce: string }>(
|
||||||
|
`${homeserverUrl()}/_synapse/admin/v1/register`
|
||||||
|
)
|
||||||
|
const mac = buildSharedSecretMac(nonceResponse.nonce, username, password, admin)
|
||||||
|
|
||||||
|
return requestJson(`${homeserverUrl()}/_synapse/admin/v1/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
nonce: nonceResponse.nonce,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
admin,
|
||||||
|
mac,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginMatrixUser = async (username: string, password: string) => {
|
||||||
|
return requestJson<{
|
||||||
|
access_token: string
|
||||||
|
user_id: string
|
||||||
|
}>(`${homeserverUrl()}/_matrix/client/v3/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: "m.login.password",
|
||||||
|
identifier: {
|
||||||
|
type: "m.id.user",
|
||||||
|
user: username,
|
||||||
|
},
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureServiceAccessToken = async () => {
|
||||||
|
if (!registrationSharedSecret()) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("MATRIX_REGISTRATION_SHARED_SECRET is not configured"),
|
||||||
|
{ statusCode: 503 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = serviceUserLocalpart()
|
||||||
|
const password = serviceUserPassword()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await registerWithSharedSecret(username, password, true)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.errcode !== "M_USER_IN_USE") {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginMatrixUser(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
const getCurrentUserDisplayName = async (userId: string, tenantId: number | null) => {
|
const getCurrentUserDisplayName = async (userId: string, tenantId: number | null) => {
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
const [profile] = await server.db
|
const [profile] = await server.db
|
||||||
@@ -167,6 +253,16 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
return body as T
|
return body as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestMatrixJson = async <T>(path: string, accessToken: string, init?: RequestInit): Promise<T> => {
|
||||||
|
return requestJson<T>(`${homeserverUrl()}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
...(init?.headers || {}),
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getStatus = async () => {
|
const getStatus = async () => {
|
||||||
const configured = Boolean(homeserverUrl() && serverName())
|
const configured = Boolean(homeserverUrl() && serverName())
|
||||||
|
|
||||||
@@ -218,24 +314,8 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
const password = randomBytes(32).toString("base64url")
|
const password = randomBytes(32).toString("base64url")
|
||||||
const displayName = await getCurrentUserDisplayName(userId, tenantId)
|
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 {
|
try {
|
||||||
await requestJson(`${homeserverUrl()}/_synapse/admin/v1/register`, {
|
await registerWithSharedSecret(username, password, false)
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
nonce: nonceResponse.nonce,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
admin: false,
|
|
||||||
mac,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
matrixUserId,
|
matrixUserId,
|
||||||
@@ -259,10 +339,132 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCurrentTenant = async (tenantId: number | null) => {
|
||||||
|
if (!tenantId) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("No active tenant selected"),
|
||||||
|
{ statusCode: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tenant] = await server.db
|
||||||
|
.select({
|
||||||
|
id: tenants.id,
|
||||||
|
name: tenants.name,
|
||||||
|
short: tenants.short,
|
||||||
|
})
|
||||||
|
.from(tenants)
|
||||||
|
.where(eq(tenants.id, tenantId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!tenant) {
|
||||||
|
throw Object.assign(
|
||||||
|
new Error("Tenant not found"),
|
||||||
|
{ statusCode: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tenant
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTenantSpaceStatus = async (tenantId: number | null) => {
|
||||||
|
const tenant = await getCurrentTenant(tenantId)
|
||||||
|
const alias = tenantSpaceAlias(tenant)
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
alias,
|
||||||
|
exists: false,
|
||||||
|
roomId: null,
|
||||||
|
servers: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provisionCurrentTenantSpace = async (userId: string, tenantId: number | null) => {
|
||||||
|
const tenant = await getCurrentTenant(tenantId)
|
||||||
|
const existing = await getTenantSpaceStatus(tenant.id)
|
||||||
|
const userAccount = await provisionCurrentUser(userId, tenant.id)
|
||||||
|
|
||||||
|
if (existing.exists) {
|
||||||
|
return {
|
||||||
|
...existing,
|
||||||
|
created: false,
|
||||||
|
alreadyExisted: true,
|
||||||
|
invitedUserId: userAccount.matrixUserId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceLogin = await ensureServiceAccessToken()
|
||||||
|
const aliasLocalpart = tenantSpaceAliasLocalpart(tenant)
|
||||||
|
const createdRoom = await requestMatrixJson<{ room_id: string }>(
|
||||||
|
"/_matrix/client/v3/createRoom",
|
||||||
|
serviceLogin.access_token,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
creation_content: {
|
||||||
|
type: "m.space",
|
||||||
|
},
|
||||||
|
name: `FEDEO · ${tenant.name}`,
|
||||||
|
topic: `Kommunikationsbereich für ${tenant.name}`,
|
||||||
|
preset: "private_chat",
|
||||||
|
visibility: "private",
|
||||||
|
room_alias_name: aliasLocalpart,
|
||||||
|
invite: [userAccount.matrixUserId],
|
||||||
|
initial_state: [
|
||||||
|
{
|
||||||
|
type: "m.room.history_visibility",
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
history_visibility: "invited",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenantId: tenant.id,
|
||||||
|
tenantName: tenant.name,
|
||||||
|
alias: tenantSpaceAlias(tenant),
|
||||||
|
exists: true,
|
||||||
|
created: true,
|
||||||
|
alreadyExisted: false,
|
||||||
|
roomId: createdRoom.room_id,
|
||||||
|
invitedUserId: userAccount.matrixUserId,
|
||||||
|
serviceUserId: serviceLogin.user_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStatus,
|
getStatus,
|
||||||
matrixUserIdForUser,
|
matrixUserIdForUser,
|
||||||
getCurrentUserDisplayName,
|
getCurrentUserDisplayName,
|
||||||
provisionCurrentUser,
|
provisionCurrentUser,
|
||||||
|
getTenantSpaceStatus,
|
||||||
|
provisionCurrentTenantSpace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,26 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
.send({ error: err.message || "Matrix provisioning failed" })
|
.send({ error: err.message || "Matrix provisioning failed" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/tenant-space", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
return await matrix.getTenantSpaceStatus(req.user.tenant_id)
|
||||||
|
} catch (err: any) {
|
||||||
|
req.log.error(err)
|
||||||
|
return reply
|
||||||
|
.code(err.statusCode || 500)
|
||||||
|
.send({ error: err.message || "Matrix tenant space status failed" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.post("/communication/matrix/tenant-space/provision", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
return await matrix.provisionCurrentTenantSpace(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 tenant space provisioning failed" })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export let secrets = {
|
|||||||
MATRIX_HOMESERVER_URL?: string
|
MATRIX_HOMESERVER_URL?: string
|
||||||
MATRIX_SERVER_NAME?: string
|
MATRIX_SERVER_NAME?: string
|
||||||
MATRIX_REGISTRATION_SHARED_SECRET?: string
|
MATRIX_REGISTRATION_SHARED_SECRET?: string
|
||||||
|
MATRIX_SERVICE_USER_LOCALPART?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSecrets () {
|
export async function loadSecrets () {
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ const { $api } = useNuxtApp()
|
|||||||
|
|
||||||
const status = ref(null)
|
const status = ref(null)
|
||||||
const identity = ref(null)
|
const identity = ref(null)
|
||||||
|
const tenantSpace = ref(null)
|
||||||
const provisionResult = ref(null)
|
const provisionResult = ref(null)
|
||||||
|
const tenantSpaceProvisionResult = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const provisioning = ref(false)
|
const provisioning = ref(false)
|
||||||
|
const tenantSpaceProvisioning = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
|
|
||||||
const statusItems = computed(() => [
|
const statusItems = computed(() => [
|
||||||
@@ -45,13 +48,15 @@ const statusItems = computed(() => [
|
|||||||
const loadMatrixInfo = async () => {
|
const loadMatrixInfo = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [statusRes, identityRes] = await Promise.all([
|
const [statusRes, identityRes, tenantSpaceRes] = 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")
|
||||||
])
|
])
|
||||||
|
|
||||||
status.value = statusRes
|
status.value = statusRes
|
||||||
identity.value = identityRes
|
identity.value = identityRes
|
||||||
|
tenantSpace.value = tenantSpaceRes
|
||||||
lastUpdated.value = new Date()
|
lastUpdated.value = new Date()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -91,6 +96,37 @@ const provisionMatrixAccount = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provisionTenantSpace = async () => {
|
||||||
|
tenantSpaceProvisioning.value = true
|
||||||
|
try {
|
||||||
|
const res = await $api("/api/communication/matrix/tenant-space/provision", {
|
||||||
|
method: "POST"
|
||||||
|
})
|
||||||
|
|
||||||
|
tenantSpaceProvisionResult.value = res
|
||||||
|
tenantSpace.value = {
|
||||||
|
tenantId: res.tenantId,
|
||||||
|
tenantName: res.tenantName,
|
||||||
|
alias: res.alias,
|
||||||
|
exists: true,
|
||||||
|
roomId: res.roomId,
|
||||||
|
servers: res.servers || []
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: res.alreadyExisted ? "Mandanten-Space ist bereits vorhanden" : "Mandanten-Space erstellt",
|
||||||
|
color: "success"
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
title: "Mandanten-Space konnte nicht erstellt werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
tenantSpaceProvisioning.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatDateTime = (value) => {
|
const formatDateTime = (value) => {
|
||||||
if (!value) return "-"
|
if (!value) return "-"
|
||||||
|
|
||||||
@@ -156,80 +192,149 @@ onMounted(loadMatrixInfo)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">
|
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
<div class="space-y-4">
|
||||||
<template #header>
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
<div class="flex items-center gap-2">
|
<template #header>
|
||||||
<UIcon name="i-heroicons-user-circle" class="size-5 text-primary" />
|
<div class="flex items-center gap-2">
|
||||||
<h2 class="text-base font-semibold text-highlighted">
|
<UIcon name="i-heroicons-user-circle" class="size-5 text-primary" />
|
||||||
Eigene Matrix-Identität
|
<h2 class="text-base font-semibold text-highlighted">
|
||||||
</h2>
|
Eigene Matrix-Identität
|
||||||
</div>
|
</h2>
|
||||||
</template>
|
</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">
|
||||||
|
Matrix-ID
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
||||||
|
{{ identity?.matrixUserId || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium uppercase text-muted">
|
||||||
|
Anzeigename
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-sm text-highlighted">
|
||||||
|
{{ identity?.displayName || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="provisionResult"
|
||||||
|
icon="i-heroicons-check-circle"
|
||||||
|
color="success"
|
||||||
|
variant="soft"
|
||||||
|
:title="provisionResult.alreadyExisted ? 'Matrix-Konto vorhanden' : 'Matrix-Konto erstellt'"
|
||||||
|
:description="provisionResult.matrixUserId"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="status && !status.reachable"
|
||||||
|
icon="i-heroicons-exclamation-triangle"
|
||||||
|
color="error"
|
||||||
|
variant="soft"
|
||||||
|
title="Matrix-Homeserver nicht erreichbar"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p class="text-xs text-muted">
|
||||||
|
Aktualisiert: {{ formatDateTime(lastUpdated) }}
|
||||||
|
</p>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-user-plus"
|
||||||
|
:loading="provisioning"
|
||||||
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
||||||
|
@click="provisionMatrixAccount"
|
||||||
|
>
|
||||||
|
Matrix-Konto erstellen
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-building-office-2" class="size-5 text-primary" />
|
||||||
|
<h2 class="text-base font-semibold text-highlighted">
|
||||||
|
Mandanten-Space
|
||||||
|
</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">
|
||||||
|
{{ tenantSpace?.alias || "-" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium uppercase text-muted">
|
||||||
|
Status
|
||||||
|
</p>
|
||||||
|
<UBadge
|
||||||
|
class="mt-1"
|
||||||
|
:color="tenantSpace?.exists ? 'success' : 'neutral'"
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
|
{{ tenantSpace?.exists ? "Vorhanden" : "Noch nicht erstellt" }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
<p class="text-xs font-medium uppercase text-muted">
|
||||||
Matrix-ID
|
Raum-ID
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
<p class="mt-1 break-all font-mono text-sm text-highlighted">
|
||||||
{{ identity?.matrixUserId || "-" }}
|
{{ tenantSpace?.roomId || "-" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p class="text-xs font-medium uppercase text-muted">
|
<UAlert
|
||||||
Anzeigename
|
v-if="tenantSpaceProvisionResult"
|
||||||
</p>
|
icon="i-heroicons-check-circle"
|
||||||
<p class="mt-1 text-sm text-highlighted">
|
color="success"
|
||||||
{{ identity?.displayName || "-" }}
|
variant="soft"
|
||||||
</p>
|
:title="tenantSpaceProvisionResult.alreadyExisted ? 'Mandanten-Space vorhanden' : 'Mandanten-Space erstellt'"
|
||||||
|
:description="tenantSpaceProvisionResult.alias"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
:loading="tenantSpaceProvisioning"
|
||||||
|
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
||||||
|
@click="provisionTenantSpace"
|
||||||
|
>
|
||||||
|
Mandanten-Space erstellen
|
||||||
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
</UCard>
|
||||||
<UAlert
|
</div>
|
||||||
v-if="provisionResult"
|
|
||||||
icon="i-heroicons-check-circle"
|
|
||||||
color="success"
|
|
||||||
variant="soft"
|
|
||||||
:title="provisionResult.alreadyExisted ? 'Matrix-Konto vorhanden' : 'Matrix-Konto erstellt'"
|
|
||||||
:description="provisionResult.matrixUserId"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="status && !status.reachable"
|
|
||||||
icon="i-heroicons-exclamation-triangle"
|
|
||||||
color="error"
|
|
||||||
variant="soft"
|
|
||||||
title="Matrix-Homeserver nicht erreichbar"
|
|
||||||
: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>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<p class="text-xs text-muted">
|
|
||||||
Aktualisiert: {{ formatDateTime(lastUpdated) }}
|
|
||||||
</p>
|
|
||||||
<UButton
|
|
||||||
icon="i-heroicons-user-plus"
|
|
||||||
:loading="provisioning"
|
|
||||||
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
|
||||||
@click="provisionMatrixAccount"
|
|
||||||
>
|
|
||||||
Matrix-Konto erstellen
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<UCard :ui="{ root: 'rounded-lg' }">
|
<UCard :ui="{ root: 'rounded-lg' }">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -244,7 +349,7 @@ onMounted(loadMatrixInfo)
|
|||||||
<div class="space-y-3 text-sm text-muted">
|
<div class="space-y-3 text-sm text-muted">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<UIcon name="i-heroicons-building-office-2" class="mt-0.5 size-4 shrink-0" />
|
<UIcon name="i-heroicons-building-office-2" class="mt-0.5 size-4 shrink-0" />
|
||||||
<span>Mandanten-Space automatisch anlegen.</span>
|
<span>Team- und Projekt-Räume im Mandanten-Space anlegen.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<UIcon name="i-heroicons-users" class="mt-0.5 size-4 shrink-0" />
|
<UIcon name="i-heroicons-users" class="mt-0.5 size-4 shrink-0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user