Files
FEDEO/backend/src/routes/communication.ts
flfeders 4ffafef3f1
All checks were successful
Build and Release FEDEO / verify (push) Successful in 1m5s
Build and Release FEDEO / build-and-release (push) Successful in 6m53s
feat(admin): archive users and their profiles permanently
2026-10-15 14:22:12 +02:00

539 lines
29 KiB
TypeScript

import { createHash } from "node:crypto"
import { FastifyInstance } from "fastify"
import { and, asc, desc, eq, gt, inArray, isNull, ne, sql } from "drizzle-orm"
import {
authProfiles,
authTenantUsers,
authUsers,
communicationMessages,
communicationRoomMembers,
communicationRoomReads,
communicationRooms,
communicationHiddenRooms,
notificationsItems,
projects,
} from "../../db/schema"
import { NotificationService, UserDirectory } from "../modules/notification.service"
import { assertCustomRoomKey, chatError, projectChatAccess } from "../modules/communication-access"
import { chatContent, chatImage, chatReactions, imageMetadata, setChatReaction } from "../modules/chat-content"
type ChatUser = {
userId: string
email?: string | null
firstName?: string | null
lastName?: string | null
fullName?: string | null
}
const getUserDirectory: UserDirectory = async (server, userId) => {
const [user] = await server.db
.select({ email: authUsers.email })
.from(authUsers)
.where(eq(authUsers.id, userId))
.limit(1)
return user || null
}
const displayName = (user: Omit<ChatUser, "userId">) =>
user.fullName || [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email || "Benutzer"
const directRoomKey = (firstUserId: string, secondUserId: string) => {
const hash = createHash("sha256")
.update([firstUserId, secondUserId].sort().join(":"))
.digest("hex")
.slice(0, 16)
return `direct_${hash}`
}
const normalizeRoomKey = (value: string) => value
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9._=-]+/g, "_")
.replace(/_+/g, "_")
.replace(/^[._=-]+|[._=-]+$/g, "")
export default async function communicationRoutes(server: FastifyInstance) {
const notifications = new NotificationService(server, getUserDirectory)
const requireTenant = async (req: any) => {
const tenantId = Number(req.user.tenant_id)
if (!tenantId) throw Object.assign(new Error("Kein aktiver Mandant"), { statusCode: 400 })
const [membership] = await server.db.select({ userId: authTenantUsers.user_id }).from(authTenantUsers)
.where(and(eq(authTenantUsers.tenant_id, tenantId), eq(authTenantUsers.user_id, req.user.user_id))).limit(1)
if (!membership) chatError("Keine aktive Mandantenmitgliedschaft")
return tenantId
}
const tenantUsers = async (tenantId: number): Promise<ChatUser[]> => server.db
.select({
userId: authTenantUsers.user_id,
email: authUsers.email,
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, tenantId)
))
.where(and(eq(authTenantUsers.tenant_id, tenantId), isNull(authUsers.archived_at)))
const addMembers = async (roomId: string, userIds: string[]) => {
const uniqueUserIds = Array.from(new Set(userIds.filter(Boolean)))
if (!uniqueUserIds.length) return
await server.db.insert(communicationRoomMembers)
.values(uniqueUserIds.map((userId) => ({ roomId, userId })))
.onConflictDoNothing()
}
const ensureGeneralRoom = async (tenantId: number, creatorId: string) => server.db.transaction(async (tx) => {
const generalKey = and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, "allgemein"))
let [room] = await tx.select().from(communicationRooms)
.where(generalKey).limit(1).for("update")
if (!room) {
const [created] = await tx.insert(communicationRooms).values({
tenantId,
key: "allgemein",
name: "Allgemeiner Chat",
topic: "Mandantenweiter Austausch",
type: "general",
createdBy: creatorId,
}).onConflictDoNothing().returning()
room = created
if (!room) {
[room] = await tx.select().from(communicationRooms)
.where(generalKey).limit(1).for("update")
}
}
// Old Matrix general-room endpoints omitted the type and persisted its default 'room'.
// Only unarchived, unbound tenant rooms may be adopted; a key collision must not grant access.
if (!room || !["general", "room"].includes(room.type) || room.archived
|| room.entityType !== null || room.entityId !== null || room.entityUuid !== null) {
chatError("Allgemeiner Chat ist nicht verfügbar", 409)
}
if (room.type === "room") {
[room] = await tx.update(communicationRooms).set({ type: "general" })
.where(eq(communicationRooms.id, room.id)).returning()
}
const users = await tx.select({ userId: authTenantUsers.user_id }).from(authTenantUsers)
.innerJoin(authUsers, eq(authUsers.id, authTenantUsers.user_id))
.where(and(eq(authTenantUsers.tenant_id, tenantId), isNull(authUsers.archived_at)))
if (users.length) await tx.insert(communicationRoomMembers)
.values([...new Set(users.map(user => user.userId))].map(userId => ({ roomId: room.id, userId })))
.onConflictDoNothing()
return room
})
const requireRoom = async (req: any, tenantId: number, userId: string, roomKey: string) => {
const [row] = await server.db.select({ room: communicationRooms }).from(communicationRooms)
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
.where(and(
eq(communicationRooms.tenantId, tenantId),
eq(communicationRooms.key, roomKey),
eq(communicationRooms.archived, false),
eq(communicationRoomMembers.userId, userId)
))
.limit(1)
if (!row) throw Object.assign(new Error("Chatraum nicht gefunden oder kein Zugriff"), { statusCode: 404 })
if (row.room.type === "project" && !(await projectChatAccess(server.db, req, row.room.entityId)).length) {
chatError("Chatraum nicht gefunden oder kein Zugriff", 404)
}
return row.room
}
const roomToApi = (room: typeof communicationRooms.$inferSelect) => ({
id: room.id,
key: room.key,
name: room.name,
topic: room.topic,
type: room.type,
entityType: room.entityType,
entityId: room.entityId,
entityUuid: room.entityUuid,
exists: true,
})
const messagesForRoom = async (roomId: string, currentUserId: string, tenantId: number, afterId = 0, limit = 50) => {
const conditions = [eq(communicationMessages.roomId, roomId)]
if (afterId > 0) conditions.push(gt(communicationMessages.id, afterId))
const rows = await server.db.select({ id: communicationMessages.id, body: communicationMessages.body,
authorUserId: communicationMessages.authorUserId, createdAt: communicationMessages.createdAt,
image: sql`${communicationMessages.image} - 'dataUrl'`,
}).from(communicationMessages)
.where(and(...conditions))
.orderBy(afterId > 0 ? asc(communicationMessages.id) : desc(communicationMessages.id))
.limit(Math.min(Math.max(limit, 1), 100))
const orderedRows = afterId > 0 ? rows : rows.reverse()
const authorIds = Array.from(new Set(orderedRows.map((message) => message.authorUserId)))
const authors = authorIds.length
? await server.db.select({
userId: authUsers.id,
email: authUsers.email,
firstName: authProfiles.first_name,
lastName: authProfiles.last_name,
fullName: authProfiles.full_name,
}).from(authUsers)
.leftJoin(authProfiles, and(eq(authProfiles.user_id, authUsers.id), eq(authProfiles.tenant_id, tenantId)))
.where(inArray(authUsers.id, authorIds))
: []
const authorById = new Map(authors.map((author) => [author.userId, author]))
return orderedRows.map((message) => ({
id: message.id,
body: message.body,
image: imageMetadata(message.image),
sender: message.authorUserId,
senderDisplayName: displayName(authorById.get(message.authorUserId) || {}),
timestamp: message.createdAt.getTime(),
own: message.authorUserId === currentUserId,
}))
}
const notifyMessageRecipients = async (
tenantId: number,
room: typeof communicationRooms.$inferSelect,
senderId: string,
message: { id: number; body: string }
) => {
try {
const users = await tenantUsers(tenantId)
const sender = users.find((user) => user.userId === senderId)
const memberRows = await server.db.select({ userId: communicationRoomMembers.userId })
.from(communicationRoomMembers).where(eq(communicationRoomMembers.roomId, room.id))
const memberIds = new Set(memberRows.map((member) => member.userId))
const normalizedText = message.body.toLowerCase()
const recipients = users.filter((user) => user.userId !== senderId && memberIds.has(user.userId))
const isMentioned = (user: ChatUser) => {
const aliases = [displayName(user), user.fullName, user.firstName, user.email]
.filter(Boolean).map((value) => String(value).toLowerCase())
return aliases.some((alias) => normalizedText.includes(`@${alias}`))
}
if (!recipients.length) return
const senderName = sender ? displayName(sender) : "FEDEO"
for (const eventType of ["communication.message.direct", "communication.message.mentioned", "communication.message.room"]) {
const targets = recipients.filter((user) => (room.type === "direct" ? "communication.message.direct" : isMentioned(user) ? "communication.message.mentioned" : "communication.message.room") === eventType)
if (!targets.length) continue
await notifications.trigger({
tenantId,
userIds: targets.map((recipient) => recipient.userId),
eventType,
title: room.type === "direct" ? `Neue Direktnachricht von ${senderName}` : eventType === "communication.message.mentioned" ? `${senderName} hat dich erwähnt` : `Neue Nachricht in ${room.name}`,
message: message.body.length > 160 ? `${message.body.slice(0, 157)}...` : message.body,
payload: {
link: `/communication/chat?room=${encodeURIComponent(room.key)}`,
roomKey: room.key,
roomName: room.name,
roomType: room.type,
messageId: message.id,
mentioned: eventType === "communication.message.mentioned",
direct: room.type === "direct",
},
})
}
} catch (err) {
server.log.error({ err }, "Chat-Benachrichtigung konnte nicht ausgelöst werden")
}
}
const unreadNotifications = async (tenantId: number, userId: string) => server.db
.select({ id: notificationsItems.id, payload: notificationsItems.payload })
.from(notificationsItems)
.where(and(
eq(notificationsItems.tenantId, tenantId),
eq(notificationsItems.userId, userId),
eq(notificationsItems.eventType, "communication.message.new"),
eq(notificationsItems.channel, "inapp"),
ne(notificationsItems.status, "read")
))
server.get("/communication/chat/status", async () => ({ ready: true, provider: "fedeo" }))
server.get("/communication/chat/room-preferences", async (req: any) => {
const tenantId = await requireTenant(req)
const hidden = await server.db.select({ key: communicationHiddenRooms.roomKey }).from(communicationHiddenRooms)
.where(and(eq(communicationHiddenRooms.tenantId, tenantId), eq(communicationHiddenRooms.userId, req.user.user_id)))
return { hiddenRoomKeys: hidden.map(room => room.key) }
})
server.put("/communication/chat/room-preferences", async (req: any, reply) => {
const tenantId = await requireTenant(req)
const { roomKey, hidden } = req.body || {}
if (typeof hidden !== "boolean" || typeof roomKey !== "string" || roomKey.length > 200
|| !/^(?:[a-z0-9._=-]+|collaboration:[a-f0-9-]{36})$/.test(roomKey)) {
return reply.code(400).send({ error: "Ungültige Raumeinstellung" })
}
const value = { tenantId, userId: req.user.user_id, roomKey }
if (hidden) await server.db.insert(communicationHiddenRooms).values(value).onConflictDoNothing()
else await server.db.delete(communicationHiddenRooms).where(and(
eq(communicationHiddenRooms.tenantId, tenantId), eq(communicationHiddenRooms.userId, req.user.user_id),
eq(communicationHiddenRooms.roomKey, roomKey)))
return { roomKey, hidden }
})
server.get("/communication/chat/users", async (req: any) => {
const tenantId = await requireTenant(req)
return { users: (await tenantUsers(tenantId)).map((user) => ({
userId: user.userId,
email: user.email,
displayName: displayName(user),
})) }
})
server.get("/communication/chat/rooms", async (req: any) => {
const tenantId = await requireTenant(req)
await ensureGeneralRoom(tenantId, req.user.user_id)
const rows = await server.db.select({ room: communicationRooms }).from(communicationRooms)
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
.where(and(
eq(communicationRooms.tenantId, tenantId),
inArray(communicationRooms.type, ["general", "room"]),
eq(communicationRooms.archived, false),
eq(communicationRoomMembers.userId, req.user.user_id)
)).orderBy(asc(communicationRooms.name))
return { rooms: rows.map(({ room }) => roomToApi(room)) }
})
server.post("/communication/chat/rooms", async (req: any, reply) => {
const tenantId = await requireTenant(req)
const body = (req.body || {}) as { key?: string; name?: string; topic?: string }
const name = body.name?.trim()
const key = normalizeRoomKey(body.key?.trim() || name || "")
if (!name || !key) return reply.code(400).send({ error: "Name und gültiger Raumschlüssel sind erforderlich" })
assertCustomRoomKey(key)
const userIds = (await tenantUsers(tenantId)).map((user) => user.userId)
const created = await server.db.transaction(async (tx) => {
const [room] = await tx.insert(communicationRooms).values({
tenantId, key, name, topic: body.topic?.trim() || null, type: "room", createdBy: req.user.user_id,
}).onConflictDoNothing().returning()
if (room) await tx.insert(communicationRoomMembers)
.values([...new Set(userIds)].map((userId) => ({ roomId: room.id, userId }))).onConflictDoNothing()
return room
})
if (!created) return reply.code(409).send({ error: "Dieser Raumschlüssel ist bereits vergeben" })
const recipients = userIds.filter((id) => id !== req.user.user_id)
if (recipients.length) {
try {
await notifications.trigger({ tenantId, userIds: recipients, eventType: "communication.room.invited",
title: "Neuer Chatraum", message: `Du wurdest dem Raum „${created.name}“ hinzugefügt.`,
payload: { link: `/communication/chat?room=${encodeURIComponent(created.key)}` } })
} catch (err) { server.log.error({ err }, "Raum-Benachrichtigung fehlgeschlagen") }
}
return { ...roomToApi(created), alreadyExisted: false }
})
server.get("/communication/chat/project-rooms", async (req: any) => {
const tenantId = await requireTenant(req)
const [roomRows, projectRows] = await Promise.all([
server.db.select({ room: communicationRooms }).from(communicationRooms)
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.type, "project"), eq(communicationRoomMembers.userId, req.user.user_id))),
projectChatAccess(server.db, req),
])
const roomsByProject = new Map(roomRows.map(({ room }) => [room.entityId, room]))
return { rooms: projectRows.map((project) => {
const room = roomsByProject.get(project.id)
return room ? { ...roomToApi(room), projectId: project.id, projectNumber: project.projectNumber } : {
key: `project_${project.id}`,
name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name,
topic: `Projektkommunikation zu ${project.name}`,
type: "project", entityType: "project", entityId: project.id,
projectId: project.id, projectNumber: project.projectNumber, exists: false,
}
}) }
})
server.post("/communication/chat/project-rooms/:projectId/provision", async (req: any, reply) => {
const tenantId = await requireTenant(req)
const projectId = Number(req.params.projectId)
const [project] = await projectChatAccess(server.db, req, projectId)
if (!project) return reply.code(404).send({ error: "Projekt nicht gefunden" })
const key = `project_${project.id}`
let [room] = await server.db.select().from(communicationRooms)
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1)
if (!room) {
[room] = await server.db.insert(communicationRooms).values({
tenantId, key,
name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name,
topic: `Projektkommunikation zu ${project.name}`,
type: "project", entityType: "project", entityId: project.id, createdBy: req.user.user_id,
}).onConflictDoNothing().returning()
if (!room) {
[room] = await server.db.select().from(communicationRooms)
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1)
}
}
if (room.type !== "project" || room.entityId !== projectId || room.archived) {
chatError("Projektchat ist nicht verfügbar", 409)
}
const profileIds = (project.profiles || []) as string[]
const memberProfiles = profileIds.length
? await server.db.select({ userId: authProfiles.user_id }).from(authProfiles)
.where(and(eq(authProfiles.tenant_id, tenantId), inArray(authProfiles.id, profileIds), eq(authProfiles.active, true)))
: []
const projectMemberIds = memberProfiles.map((profile) => profile.userId).filter(Boolean) as string[]
await addMembers(room.id, [req.user.user_id, ...projectMemberIds])
return roomToApi(room)
})
server.get("/communication/chat/direct-rooms", async (req: any) => {
const tenantId = await requireTenant(req)
const users = (await tenantUsers(tenantId)).filter((user) => user.userId !== req.user.user_id)
const existingRows = await server.db.select({ room: communicationRooms }).from(communicationRooms)
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.type, "direct"), eq(communicationRoomMembers.userId, req.user.user_id)))
const roomsByKey = new Map(existingRows.map(({ room }) => [room.key, room]))
return { rooms: users.map((user) => {
const key = directRoomKey(req.user.user_id, user.userId)
const room = roomsByKey.get(key)
return room ? {
...roomToApi(room),
name: displayName(user),
topic: `Direktnachricht mit ${displayName(user)}`,
entityUuid: user.userId,
userId: user.userId,
email: user.email,
} : {
key, name: displayName(user), topic: `Direktnachricht mit ${displayName(user)}`,
type: "direct", entityType: "user", entityUuid: user.userId,
userId: user.userId, email: user.email, exists: false,
}
}) }
})
server.post("/communication/chat/direct-rooms/:userId/provision", async (req: any, reply) => {
const tenantId = await requireTenant(req)
const targetId = String(req.params.userId)
const target = (await tenantUsers(tenantId)).find((user) => user.userId === targetId)
if (!target || targetId === req.user.user_id) return reply.code(404).send({ error: "Benutzer nicht gefunden" })
const key = directRoomKey(req.user.user_id, targetId)
let [room] = await server.db.select().from(communicationRooms)
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1)
if (!room) {
[room] = await server.db.insert(communicationRooms).values({
tenantId, key, name: displayName(target), topic: `Direktnachricht mit ${displayName(target)}`,
type: "direct", entityType: "user", entityUuid: targetId, createdBy: req.user.user_id,
}).onConflictDoNothing().returning()
if (!room) {
[room] = await server.db.select().from(communicationRooms)
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.key, key))).limit(1)
}
}
if (room.type !== "direct" || room.archived) chatError("Direktchat ist nicht verfügbar", 409)
await addMembers(room.id, [req.user.user_id, targetId])
return {
...roomToApi(room),
name: displayName(target),
topic: `Direktnachricht mit ${displayName(target)}`,
entityUuid: targetId,
userId: targetId,
email: target.email,
}
})
server.get("/communication/chat/unread", async (req: any) => {
const tenantId = await requireTenant(req)
const memberships = await server.db.select({
roomId: communicationRoomMembers.roomId,
roomKey: communicationRooms.key,
roomType: communicationRooms.type,
projectId: communicationRooms.entityId,
lastReadMessageId: communicationRoomReads.lastReadMessageId,
}).from(communicationRoomMembers)
.innerJoin(communicationRooms, eq(communicationRooms.id, communicationRoomMembers.roomId))
.leftJoin(communicationRoomReads, and(eq(communicationRoomReads.roomId, communicationRoomMembers.roomId), eq(communicationRoomReads.userId, req.user.user_id)))
.where(and(eq(communicationRoomMembers.userId, req.user.user_id), eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.archived, false)))
const allowedProjects = new Set((await projectChatAccess(server.db, req)).map((project: any) => project.id))
const rooms: Record<string, { count: number; mentions: number }> = {}
for (const membership of memberships) {
if (membership.roomType === "project" && !allowedProjects.has(membership.projectId)) continue
const [result] = await server.db.select({ count: sql<number>`count(*)::int` }).from(communicationMessages)
.where(and(eq(communicationMessages.roomId, membership.roomId), gt(communicationMessages.id, membership.lastReadMessageId || 0), ne(communicationMessages.authorUserId, req.user.user_id)))
rooms[membership.roomKey] = { count: result?.count || 0, mentions: 0 }
}
return { rooms }
})
server.get("/communication/chat/rooms/:roomKey/messages", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return { ...roomToApi(room), messages: await messagesForRoom(room.id, req.user.user_id, tenantId) }
})
server.get("/communication/chat/rooms/:roomKey/sync", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
const afterId = Math.max(Number(req.query?.afterId || 0), 0)
const messages = await messagesForRoom(room.id, req.user.user_id, tenantId, afterId, 100)
return { ...roomToApi(room), messages, nextId: messages[messages.length - 1]?.id || afterId }
})
server.post("/communication/chat/rooms/:roomKey/messages", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
const { body, image } = await chatContent(req.body)
const [created] = await server.db.insert(communicationMessages).values({
tenantId, roomId: room.id, authorUserId: req.user.user_id, body, image,
}).returning()
await notifyMessageRecipients(tenantId, room, req.user.user_id, { id: created.id, body: body || "📷 Bild" })
return { id: created.id, body: created.body, image: imageMetadata(created.image), sender: created.authorUserId, senderDisplayName: "Du", timestamp: created.createdAt.getTime(), own: true }
})
server.get("/communication/chat/rooms/:roomKey/image", async (req: any, reply) => {
reply.header("Cache-Control", "no-store")
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return chatImage(server.db, "communication", room.id, req.query.messageId)
})
server.get("/communication/chat/rooms/:roomKey/reactions", async (req: any, reply) => {
reply.header("Cache-Control", "no-store")
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return chatReactions(server.db, "communication", room.id, req.user.user_id, req.query.ids)
})
server.put("/communication/chat/rooms/:roomKey/reactions", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return setChatReaction(server.db, "communication", room.id, req.user.user_id, req.body)
})
server.post("/communication/chat/rooms/:roomKey/read", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
const requestedId = Math.max(Number(req.body?.messageId || 0), 0)
const [latest] = await server.db.select({ id: communicationMessages.id }).from(communicationMessages)
.where(eq(communicationMessages.roomId, room.id)).orderBy(desc(communicationMessages.id)).limit(1)
const latestId = latest?.id || 0
const messageId = requestedId ? Math.min(requestedId, latestId) : latestId
await server.db.insert(communicationRoomReads).values({
roomId: room.id, userId: req.user.user_id, lastReadMessageId: messageId || null, readAt: new Date(),
}).onConflictDoUpdate({
target: [communicationRoomReads.roomId, communicationRoomReads.userId],
set: { lastReadMessageId: messageId || null, readAt: new Date() },
})
const notificationRows = await unreadNotifications(tenantId, req.user.user_id)
const ids = notificationRows.filter((item) => (item.payload as any)?.roomKey === room.key).map((item) => item.id)
if (ids.length) await server.db.update(notificationsItems).set({ readAt: new Date(), status: "read" }).where(inArray(notificationsItems.id, ids))
return { read: true, messageId }
})
server.get("/communication/chat/rooms/:roomKey/members", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
const members = await server.db.select({
userId: authUsers.id, email: authUsers.email,
firstName: authProfiles.first_name, lastName: authProfiles.last_name, fullName: authProfiles.full_name,
}).from(communicationRoomMembers)
.innerJoin(authUsers, eq(authUsers.id, communicationRoomMembers.userId))
.leftJoin(authProfiles, and(eq(authProfiles.user_id, authUsers.id), eq(authProfiles.tenant_id, tenantId)))
.where(eq(communicationRoomMembers.roomId, room.id))
return { members: members.map((member) => ({
userId: member.userId, displayName: displayName(member), email: member.email, own: member.userId === req.user.user_id,
})) }
})
}