diff --git a/backend/src/modules/matrix.service.ts b/backend/src/modules/matrix.service.ts index b9ac2b2..ab4ff70 100644 --- a/backend/src/modules/matrix.service.ts +++ b/backend/src/modules/matrix.service.ts @@ -2,7 +2,7 @@ import { createHash, createHmac, randomBytes } from "node:crypto" import { existsSync, readFileSync } from "node:fs" import { resolve } from "node:path" import { FastifyInstance } from "fastify" -import { authProfiles, authUsers, communicationRooms, tenants } from "../../db/schema" +import { authProfiles, authTenantUsers, authUsers, communicationRooms, tenants } from "../../db/schema" import { and, eq } from "drizzle-orm" import { secrets } from "../utils/secrets" @@ -944,21 +944,15 @@ export function matrixService(server: FastifyInstance) { return session } - try { - await requestMatrixJson( - `/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`, - session.accessToken, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - } - ) - } catch (err: any) { - if (err.errcode !== "M_FORBIDDEN" && err.errcode !== "M_UNKNOWN") { - throw err + await requestMatrixJson( + `/_matrix/client/v3/join/${encodeURIComponent(room.roomId || room.alias)}`, + session.accessToken, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), } - } + ) matrixJoinedRoomCache.set(joinCacheKey, Date.now() + 30 * 60 * 1000) return session @@ -1112,6 +1106,125 @@ export function matrixService(server: FastifyInstance) { } } + const listTenantCommunicationUsers = async (tenantId: number | null) => { + const tenant = await getCurrentTenant(tenantId) + const rows = await server.db + .select({ + userId: authTenantUsers.user_id, + email: authUsers.email, + profileActive: authProfiles.active, + firstName: authProfiles.first_name, + lastName: authProfiles.last_name, + fullName: authProfiles.full_name, + }) + .from(authTenantUsers) + .innerJoin(authUsers, eq(authUsers.id, authTenantUsers.user_id)) + .leftJoin(authProfiles, and( + eq(authProfiles.user_id, authTenantUsers.user_id), + eq(authProfiles.tenant_id, tenant.id) + )) + .where(eq(authTenantUsers.tenant_id, tenant.id)) + + return rows + .filter((row) => row.profileActive !== false) + .map((row) => ({ + userId: row.userId, + email: row.email, + displayName: row.fullName || `${row.firstName || ""} ${row.lastName || ""}`.trim() || row.email, + })) + } + + const inviteMatrixUserToRoom = async ( + room: { roomId: string }, + matrixUserId: string, + reason?: string + ) => { + const serviceLogin = await ensureServiceAccessToken() + + try { + await requestMatrixJson( + `/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/invite`, + serviceLogin.accessToken, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_id: matrixUserId, + reason, + }), + } + ) + + return "invited" + } catch (err: any) { + if ( + err.errcode === "M_FORBIDDEN" || + err.errcode === "M_BAD_STATE" || + err.errcode === "M_UNKNOWN" + ) { + return "already_available" + } + + throw err + } + } + + const syncTenantRoomMembers = async ( + requestingUserId: string, + tenantId: number | null, + options: MatrixTenantRoomOptions = {} + ) => { + const room = await provisionTenantRoom(requestingUserId, tenantId, options) + const users = await listTenantCommunicationUsers(tenantId) + const results = [] + + for (const user of users) { + try { + const account = await provisionCurrentUser(user.userId, tenantId) + await inviteMatrixUserToRoom( + { roomId: room.roomId }, + account.matrixUserId, + `FEDEO-Raumsynchronisation: ${room.name}` + ) + await ensureCurrentUserJoinedRoom(user.userId, tenantId, { + roomId: room.roomId, + alias: room.alias, + }) + + results.push({ + userId: user.userId, + email: user.email, + displayName: user.displayName, + matrixUserId: account.matrixUserId, + status: "joined", + ok: true, + }) + } catch (err: any) { + results.push({ + userId: user.userId, + email: user.email, + displayName: user.displayName, + status: "failed", + ok: false, + error: err.message || "Synchronisation fehlgeschlagen", + }) + } + } + + return { + roomId: room.roomId, + alias: room.alias, + key: room.key, + name: room.name, + total: results.length, + joined: results.filter((item) => item.status === "joined").length, + invited: results.filter((item) => item.status === "invited").length, + alreadyAvailable: results.filter((item) => item.status === "already_available").length, + failed: results.filter((item) => !item.ok).length, + results, + } + } + const getGeneralRoomMessages = (userId: string, tenantId: number | null) => getTenantRoomMessages(userId, tenantId, { key: "allgemein", @@ -1149,6 +1262,7 @@ export function matrixService(server: FastifyInstance) { getTenantRoomMessages, getTenantRoomMembers, sendTenantRoomMessage, + syncTenantRoomMembers, getGeneralRoomMessages, getGeneralRoomMembers, sendGeneralRoomMessage, diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index a47afcd..768c28c 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -125,6 +125,17 @@ export default async function communicationRoutes(server: FastifyInstance) { } }) + server.post("/communication/matrix/rooms/general/members/sync", async (req, reply) => { + try { + return await matrix.syncTenantRoomMembers(req.user.user_id, req.user.tenant_id, { + key: "allgemein", + name: "Allgemeiner Chat", + }) + } catch (err: any) { + return handleMatrixError(req, reply, err, "Matrix member sync failed") + } + }) + server.post("/communication/matrix/rooms/general/messages", async (req, reply) => { try { const body = req.body as { text?: string } @@ -179,6 +190,18 @@ export default async function communicationRoutes(server: FastifyInstance) { } }) + server.post("/communication/matrix/rooms/:roomKey/members/sync", async (req, reply) => { + try { + return await matrix.syncTenantRoomMembers( + req.user.user_id, + req.user.tenant_id, + roomOptionsFromRequest(req) + ) + } catch (err: any) { + return handleMatrixError(req, reply, err, "Matrix member sync failed") + } + }) + server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => { try { const body = req.body as { text?: string } diff --git a/frontend/pages/communication/chat.vue b/frontend/pages/communication/chat.vue index 682089c..b338a42 100644 --- a/frontend/pages/communication/chat.vue +++ b/frontend/pages/communication/chat.vue @@ -19,6 +19,7 @@ const roomCreateForm = ref({ const loading = ref(false) const roomProvisioning = ref(false) const roomCreating = ref(false) +const roomMembersSyncing = ref(false) const matrixMessagesLoading = ref(false) const matrixMembersLoading = ref(false) const matrixMessageSending = ref(false) @@ -288,6 +289,32 @@ const loadRoomMembers = async ({ silent = false } = {}) => { } } +const syncRoomMembers = async () => { + if (!canUseMatrixChat.value || !activeRoom.value?.exists) return + + roomMembersSyncing.value = true + try { + const res = await $api(`${activeRoomEndpoint.value}/members/sync`, { + method: "POST" + }) + + toast.add({ + title: "Teilnehmer synchronisiert", + description: `${res.joined || 0} synchronisiert, ${res.failed || 0} fehlgeschlagen`, + color: res.failed ? "warning" : "success" + }) + + await loadRoomMembers() + } catch (error) { + toast.add({ + title: "Teilnehmer konnten nicht synchronisiert werden", + color: "error" + }) + } finally { + roomMembersSyncing.value = false + } +} + const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => { await loadRoomMessages({ silent }) @@ -752,15 +779,26 @@ onBeforeUnmount(stopMatrixAutoRefresh)

Teilnehmer

- +
+ + +