All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 43s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-admin (push) Successful in 23s
Build and Push Docker Images / build-frontend (push) Successful in 1m12s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 22s
268 lines
10 KiB
TypeScript
268 lines
10 KiB
TypeScript
import { FastifyInstance } from "fastify"
|
|
import { and, eq, isNull } from "drizzle-orm"
|
|
import { authProfiles, authUsers, notificationMobilePushDevices } from "../../db/schema"
|
|
import { NotificationService, UserDirectory } from "../modules/notification.service"
|
|
import { pushServerClient } from "../modules/push-server.client"
|
|
|
|
const getUserDirectory: UserDirectory = async (server: FastifyInstance, userId) => {
|
|
const rows = await server.db
|
|
.select({ email: authUsers.email })
|
|
.from(authUsers)
|
|
.where(eq(authUsers.id, userId))
|
|
.limit(1)
|
|
const data = rows[0]
|
|
if (!data) return null
|
|
return { email: data.email }
|
|
}
|
|
|
|
const requireTenant = (tenantId: number | null) => {
|
|
if (!tenantId) throw new Error("Kein aktiver Mandant")
|
|
return tenantId
|
|
}
|
|
|
|
export default async function notificationsRoutes(server: FastifyInstance) {
|
|
const svc = new NotificationService(server, getUserDirectory)
|
|
|
|
server.get("/notifications", async (req) => {
|
|
const limit = Number((req.query as { limit?: string })?.limit || 50)
|
|
return await svc.listForUser(requireTenant(req.user.tenant_id), req.user.user_id, limit)
|
|
})
|
|
|
|
server.post("/notifications/:id/read", async (req, reply) => {
|
|
const params = req.params as { id: string }
|
|
const item = await svc.markRead(requireTenant(req.user.tenant_id), req.user.user_id, params.id)
|
|
if (!item) return reply.code(404).send({ error: "Benachrichtigung nicht gefunden" })
|
|
return item
|
|
})
|
|
|
|
server.get("/notifications/push/config", async () => {
|
|
return svc.getPublicPushConfig()
|
|
})
|
|
|
|
server.post("/notifications/push/subscribe", async (req) => {
|
|
const tenantId = requireTenant(req.user.tenant_id)
|
|
const userAgent = req.headers["user-agent"]
|
|
const subscription = await svc.registerPushSubscription(
|
|
tenantId,
|
|
req.user.user_id,
|
|
req.body as any,
|
|
Array.isArray(userAgent) ? userAgent.join(" ") : userAgent
|
|
)
|
|
|
|
return {
|
|
success: true,
|
|
id: subscription?.id,
|
|
}
|
|
})
|
|
|
|
server.delete("/notifications/push/subscribe", async (req) => {
|
|
const body = (req.body || {}) as { endpoint?: string }
|
|
if (!body.endpoint) throw new Error("endpoint fehlt")
|
|
return await svc.disablePushSubscription(requireTenant(req.user.tenant_id), req.user.user_id, body.endpoint)
|
|
})
|
|
|
|
server.post("/notifications/push/mobile/register", async (req) => {
|
|
const tenantId = requireTenant(req.user.tenant_id)
|
|
const body = (req.body || {}) as {
|
|
localDeviceId?: string
|
|
platform?: "ios" | "android"
|
|
providerToken?: string
|
|
deviceLabel?: string
|
|
meta?: Record<string, unknown>
|
|
}
|
|
|
|
if (!body.localDeviceId) throw new Error("localDeviceId fehlt")
|
|
if (body.platform !== "ios" && body.platform !== "android") throw new Error("platform ist ungültig")
|
|
if (!body.providerToken) throw new Error("providerToken fehlt")
|
|
|
|
const centralLocalDeviceId = `${tenantId}:${req.user.user_id}:${body.localDeviceId}`
|
|
const registered = await pushServerClient.registerDevice({
|
|
localDeviceId: centralLocalDeviceId,
|
|
platform: body.platform,
|
|
providerToken: body.providerToken,
|
|
meta: {
|
|
...(body.meta || {}),
|
|
tenantId,
|
|
userId: req.user.user_id,
|
|
source: "fedeo-mobile",
|
|
},
|
|
})
|
|
|
|
const rows = await server.db
|
|
.insert(notificationMobilePushDevices)
|
|
.values({
|
|
tenantId,
|
|
userId: req.user.user_id,
|
|
localDeviceId: body.localDeviceId,
|
|
centralDeviceId: registered.centralDeviceId,
|
|
platform: body.platform,
|
|
providerTokenPreview: previewToken(body.providerToken),
|
|
deviceLabel: body.deviceLabel,
|
|
meta: body.meta ?? null,
|
|
lastSeenAt: new Date(),
|
|
disabledAt: null,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [
|
|
notificationMobilePushDevices.tenantId,
|
|
notificationMobilePushDevices.userId,
|
|
notificationMobilePushDevices.localDeviceId,
|
|
],
|
|
set: {
|
|
centralDeviceId: registered.centralDeviceId,
|
|
platform: body.platform,
|
|
providerTokenPreview: previewToken(body.providerToken),
|
|
deviceLabel: body.deviceLabel,
|
|
meta: body.meta ?? null,
|
|
lastSeenAt: new Date(),
|
|
disabledAt: null,
|
|
},
|
|
})
|
|
.returning()
|
|
|
|
return {
|
|
success: true,
|
|
id: rows[0]?.id,
|
|
centralDeviceId: registered.centralDeviceId,
|
|
status: registered.status,
|
|
}
|
|
})
|
|
|
|
server.post("/notifications/test-mobile-push", async (req) => {
|
|
const tenantId = requireTenant(req.user.tenant_id)
|
|
const devices = await server.db
|
|
.select({ centralDeviceId: notificationMobilePushDevices.centralDeviceId })
|
|
.from(notificationMobilePushDevices)
|
|
.where(and(
|
|
eq(notificationMobilePushDevices.tenantId, tenantId),
|
|
eq(notificationMobilePushDevices.userId, req.user.user_id),
|
|
isNull(notificationMobilePushDevices.disabledAt)
|
|
))
|
|
|
|
if (!devices.length) {
|
|
throw new Error("Kein registriertes mobiles Push-Gerät gefunden")
|
|
}
|
|
|
|
return await pushServerClient.sendPush({
|
|
idempotencyKey: `mobile-test:${tenantId}:${req.user.user_id}:${Date.now()}`,
|
|
devices: devices.map((device) => device.centralDeviceId),
|
|
priority: "high",
|
|
ttlSeconds: 600,
|
|
notification: {
|
|
title: "FEDEO Mobile Push ist aktiv",
|
|
body: "Diese Testnachricht wurde über den zentralen Push-Server zugestellt.",
|
|
},
|
|
data: {
|
|
type: "system.test_mobile_push",
|
|
link: "/",
|
|
},
|
|
})
|
|
})
|
|
|
|
server.post("/notifications/test-push", async (req) => {
|
|
return await svc.trigger({
|
|
tenantId: requireTenant(req.user.tenant_id),
|
|
userId: req.user.user_id,
|
|
eventType: "system.test_push",
|
|
title: "FEDEO Desktop Push ist aktiv",
|
|
message: "Diese Testbenachrichtigung wurde von FEDEO selbst zugestellt.",
|
|
payload: {
|
|
link: "/",
|
|
icon: "/favicon.ico",
|
|
},
|
|
channels: ["inapp", "push"],
|
|
})
|
|
})
|
|
|
|
server.post("/notifications/test-push/profile/:profileId", async (req, reply) => {
|
|
const tenantId = requireTenant(req.user.tenant_id)
|
|
const { profileId } = req.params as { profileId: string }
|
|
const [profile] = await server.db
|
|
.select({
|
|
userId: authProfiles.user_id,
|
|
})
|
|
.from(authProfiles)
|
|
.where(and(
|
|
eq(authProfiles.id, profileId),
|
|
eq(authProfiles.tenant_id, tenantId)
|
|
))
|
|
.limit(1)
|
|
|
|
if (!profile) {
|
|
return reply.code(404).send({ error: "Mitarbeiter nicht gefunden" })
|
|
}
|
|
|
|
if (!profile.userId) {
|
|
return reply.code(409).send({
|
|
error: "Der Mitarbeiter ist noch nicht mit einem Benutzerkonto verknüpft",
|
|
})
|
|
}
|
|
|
|
const devices = await server.db
|
|
.select({ centralDeviceId: notificationMobilePushDevices.centralDeviceId })
|
|
.from(notificationMobilePushDevices)
|
|
.where(and(
|
|
eq(notificationMobilePushDevices.tenantId, tenantId),
|
|
eq(notificationMobilePushDevices.userId, profile.userId),
|
|
isNull(notificationMobilePushDevices.disabledAt)
|
|
))
|
|
|
|
if (!devices.length) {
|
|
return reply.code(409).send({
|
|
error: "Für diesen Mitarbeiter ist kein aktives mobiles Push-Gerät registriert",
|
|
})
|
|
}
|
|
|
|
try {
|
|
const result = await pushServerClient.sendPush({
|
|
idempotencyKey: `profile-mobile-test:${tenantId}:${profile.userId}:${Date.now()}`,
|
|
devices: devices.map((device) => device.centralDeviceId),
|
|
priority: "high",
|
|
ttlSeconds: 600,
|
|
notification: {
|
|
title: "FEDEO Push ist aktiv",
|
|
body: "Diese Testbenachrichtigung wurde einmalig über das Mitarbeiterprofil ausgelöst.",
|
|
},
|
|
data: {
|
|
type: "system.test_mobile_push",
|
|
link: "/",
|
|
},
|
|
})
|
|
|
|
if (result.accepted === 0) {
|
|
return reply.code(502).send({
|
|
error: "Der zentrale Push-Server hat kein Gerät zur Zustellung angenommen",
|
|
result,
|
|
})
|
|
}
|
|
|
|
return result
|
|
} catch (error: any) {
|
|
server.log.error({ err: error, profileId, userId: profile.userId }, "Mitarbeiter-Test-Push fehlgeschlagen")
|
|
return reply.code(502).send({
|
|
error: error?.message || "Der zentrale Push-Server konnte die Nachricht nicht annehmen",
|
|
})
|
|
}
|
|
})
|
|
|
|
server.post("/notifications/trigger", async (req, reply) => {
|
|
try {
|
|
const body = req.body as any
|
|
const tenantId = body.tenantId || req.user.tenant_id
|
|
const res = await svc.trigger({
|
|
...body,
|
|
tenantId: requireTenant(tenantId),
|
|
})
|
|
reply.send(res)
|
|
} catch (err: any) {
|
|
server.log.error(err)
|
|
reply.code(500).send({ error: err.message })
|
|
}
|
|
})
|
|
}
|
|
|
|
function previewToken(token: string) {
|
|
if (token.length <= 14) return token
|
|
return `${token.slice(0, 6)}...${token.slice(-6)}`
|
|
}
|