From 33201096b7b7c0dc2bd216a7f2b471c172f83669 Mon Sep 17 00:00:00 2001 From: "tobias.bruckert@mcon-group.com" <62531735+tb102122@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:16:08 +1100 Subject: [PATCH 1/5] add teams --- client/src/Types/Notification.ts | 1 + client/src/Validation/notifications.ts | 6 + server/src/config/services.ts | 3 + server/src/db/models/Notification.ts | 2 +- server/src/service/index.ts | 1 + .../notificationProviders/teams.ts | 221 +++++++++++++++ .../infrastructure/notificationsService.ts | 7 + server/src/types/notification.ts | 2 +- .../src/validation/notificationValidation.ts | 9 + server/test/teamsProvider.test.ts | 267 ++++++++++++++++++ 10 files changed, 517 insertions(+), 2 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/teams.ts create mode 100644 server/test/teamsProvider.test.ts diff --git a/client/src/Types/Notification.ts b/client/src/Types/Notification.ts index 5b9645c9da..610f73547d 100644 --- a/client/src/Types/Notification.ts +++ b/client/src/Types/Notification.ts @@ -5,6 +5,7 @@ export const NotificationChannels = [ "webhook", "pager_duty", "matrix", + "teams", ] as const; export type NotificationChannel = (typeof NotificationChannels)[number]; diff --git a/client/src/Validation/notifications.ts b/client/src/Validation/notifications.ts index e505c695fd..2bbb871c94 100644 --- a/client/src/Validation/notifications.ts +++ b/client/src/Validation/notifications.ts @@ -45,6 +45,11 @@ const matrixSchema = baseSchema.extend({ accessToken: z.string().min(1, "Access token is required"), }); +const teamsSchema = baseSchema.extend({ + type: z.literal("teams"), + address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"), +}); + export const notificationSchema = z.discriminatedUnion("type", [ emailSchema, slackSchema, @@ -52,6 +57,7 @@ export const notificationSchema = z.discriminatedUnion("type", [ webhookSchema, pagerDutySchema, matrixSchema, + teamsSchema, ]); export type NotificationFormData = z.infer; diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 56aef2f19c..35bf747b84 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -11,6 +11,7 @@ import { DiscordProvider, PagerDutyProvider, MatrixProvider, + TeamsProvider, INotificationsService, } from "@/service/index.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; @@ -187,6 +188,7 @@ export const initializeServices = async ({ const discordProvider = new DiscordProvider(logger); const pagerDutyProvider = new PagerDutyProvider(logger); const matrixProvider = new MatrixProvider(logger); + const teamsProvider = new TeamsProvider(logger); const notificationsService = new NotificationsService( notificationsRepository, @@ -197,6 +199,7 @@ export const initializeServices = async ({ discordProvider, pagerDutyProvider, matrixProvider, + teamsProvider, settingsService, logger, notificationMessageBuilder diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts index 97e2d43fb0..9ea3f17bdd 100755 --- a/server/src/db/models/Notification.ts +++ b/server/src/db/models/Notification.ts @@ -25,7 +25,7 @@ const NotificationSchema = new Schema( }, type: { type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[], + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[], required: true, }, notificationName: { diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 7ec49288fd..8ae76d861a 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -6,6 +6,7 @@ export * from "@/service/infrastructure/notificationProviders/INotificationProvi export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; export * from "@/service/infrastructure/notificationProviders/matrix.js"; export * from "@/service/infrastructure/notificationProviders/slack.js"; +export * from "@/service/infrastructure/notificationProviders/teams.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; diff --git a/server/src/service/infrastructure/notificationProviders/teams.ts b/server/src/service/infrastructure/notificationProviders/teams.ts new file mode 100644 index 0000000000..a10920d2a7 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/teams.ts @@ -0,0 +1,221 @@ +const SERVICE_NAME = "TeamsProvider"; +import type { Notification } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got, { HTTPError } from "got"; + +export class TeamsProvider implements INotificationProvider { + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } + + async sendTestAlert(notification: Notification): Promise { + if (!notification.address) { + return false; + } + + try { + const payload = this.wrapAdaptiveCard({ + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.4", + body: [ + { + type: "TextBlock", + text: getTestMessage(), + weight: "Bolder", + size: "Medium", + }, + ], + }); + + await got.post(notification.address, { + json: payload, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as HTTPError; + this.logger.warn({ + message: "Teams test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.address) { + return false; + } + + const payload = this.wrapAdaptiveCard(this.buildAdaptiveCard(message)); + + try { + await got.post(notification.address, { + json: payload, + headers: { + "Content-Type": "application/json", + }, + }); + this.logger.info({ + message: "Teams notification sent via sendMessage", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Teams alert failed via sendMessage", + service: SERVICE_NAME, + method: "sendMessage", + stack: err?.stack, + }); + return false; + } + } + + /** + * Wrap an Adaptive Card in the Teams webhook envelope format + */ + private wrapAdaptiveCard(card: object): object { + return { + type: "message", + attachments: [ + { + contentType: "application/vnd.microsoft.card.adaptive", + contentUrl: null, + content: card, + }, + ], + }; + } + + /** + * Build an Adaptive Card from NotificationMessage + */ + private buildAdaptiveCard(message: NotificationMessage): object { + const colorMap: Record = { + critical: "attention", + warning: "warning", + success: "good", + info: "accent", + }; + const color = colorMap[message.severity] || "default"; + + const body: any[] = []; + + // Header with colored status indicator + body.push({ + type: "TextBlock", + text: message.content.title, + weight: "Bolder", + size: "Large", + color, + wrap: true, + }); + + // Summary + body.push({ + type: "TextBlock", + text: message.content.summary, + wrap: true, + spacing: "Small", + }); + + // Separator + body.push({ + type: "ColumnSet", + separator: true, + spacing: "Medium", + columns: [], + }); + + // Monitor details as a FactSet + body.push({ + type: "FactSet", + facts: [ + { title: "Name", value: message.monitor.name }, + { title: "Type", value: message.monitor.type }, + { title: "Status", value: message.monitor.status }, + { title: "URL", value: message.monitor.url }, + ], + }); + + // Threshold breaches + if (message.content.thresholds && message.content.thresholds.length > 0) { + body.push({ + type: "TextBlock", + text: "**Threshold Breaches**", + weight: "Bolder", + spacing: "Medium", + wrap: true, + }); + + for (const breach of message.content.thresholds) { + body.push({ + type: "TextBlock", + text: `• **${breach.metric.toUpperCase()}**: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`, + wrap: true, + spacing: "Small", + }); + } + } + + // Additional details + if (message.content.details && message.content.details.length > 0) { + body.push({ + type: "TextBlock", + text: "**Additional Information**", + weight: "Bolder", + spacing: "Medium", + wrap: true, + }); + + for (const detail of message.content.details) { + body.push({ + type: "TextBlock", + text: `• ${detail}`, + wrap: true, + spacing: "Small", + }); + } + } + + // Timestamp footer + body.push({ + type: "TextBlock", + text: `Checkmate | ${new Date(message.content.timestamp).toUTCString()}`, + size: "Small", + isSubtle: true, + spacing: "Medium", + wrap: true, + }); + + // Actions (incident link) + const actions: any[] = []; + if (message.content.incident) { + actions.push({ + type: "Action.OpenUrl", + title: "View Incident", + url: `${message.clientHost}/infrastructure/${message.monitor.id}`, + }); + } + + return { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.4", + body, + ...(actions.length > 0 ? { actions } : {}), + }; + } +} diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 1da308678b..ea7b78a4ff 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -32,6 +32,7 @@ export class NotificationsService implements INotificationsService { private discordProvider: INotificationProvider; private pagerDutyProvider: INotificationProvider; private matrixProvider: INotificationProvider; + private teamsProvider: INotificationProvider; private logger: ILogger; private settingsService: ISettingsService; private notificationMessageBuilder: INotificationMessageBuilder; @@ -45,6 +46,7 @@ export class NotificationsService implements INotificationsService { discordProvider: INotificationProvider, pagerDutyProvider: INotificationProvider, matrixProvider: INotificationProvider, + teamsProvider: INotificationProvider, settingsService: ISettingsService, logger: ILogger, notificationMessageBuilder: INotificationMessageBuilder @@ -57,6 +59,7 @@ export class NotificationsService implements INotificationsService { this.discordProvider = discordProvider; this.pagerDutyProvider = pagerDutyProvider; this.matrixProvider = matrixProvider; + this.teamsProvider = teamsProvider; this.settingsService = settingsService; this.logger = logger; this.notificationMessageBuilder = notificationMessageBuilder; @@ -92,6 +95,8 @@ export class NotificationsService implements INotificationsService { return await this.discordProvider.sendMessage!(notification, notificationMessage); case "email": return await this.emailProvider.sendMessage!(notification, notificationMessage); + case "teams": + return await this.teamsProvider.sendMessage!(notification, notificationMessage); default: this.logger.warn({ message: `Unknown notification type: ${notification.type}`, @@ -150,6 +155,8 @@ export class NotificationsService implements INotificationsService { return await this.matrixProvider.sendTestAlert(notification); case "webhook": return await this.webhookProvider.sendTestAlert(notification); + case "teams": + return await this.teamsProvider.sendTestAlert(notification); default: return false; } diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts index 669c840ca1..a121cd8e0c 100644 --- a/server/src/types/notification.ts +++ b/server/src/types/notification.ts @@ -1,4 +1,4 @@ -export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as const; export type NotificationChannel = (typeof NotificationChannels)[number]; export interface Notification { diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index d974d873ff..4c66302d96 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -59,6 +59,15 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ roomId: z.string().min(1, "Room ID is required"), accessToken: z.string().min(1, "Access Token is required"), }), + // Teams notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("teams"), + address: z.string().url("Please enter a valid Webhook URL"), + homeserverUrl: z.union([z.string(), z.literal("")]).optional(), + roomId: z.union([z.string(), z.literal("")]).optional(), + accessToken: z.union([z.string(), z.literal("")]).optional(), + }), ]); export const sendTestEmailBodyValidation = z.object({ diff --git a/server/test/teamsProvider.test.ts b/server/test/teamsProvider.test.ts new file mode 100644 index 0000000000..e6104f5687 --- /dev/null +++ b/server/test/teamsProvider.test.ts @@ -0,0 +1,267 @@ +import { describe, expect, it, jest, beforeEach } from "@jest/globals"; +import { TeamsProvider } from "../src/service/infrastructure/notificationProviders/teams.ts"; +import type { Notification } from "../src/types/notification.ts"; +import type { NotificationMessage } from "../src/types/notificationMessage.ts"; + +// Mock got +jest.unstable_mockModule("got", () => ({ + default: { post: jest.fn() }, +})); + +const createLogger = () => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), +}); + +const createNotification = (overrides?: Partial): Notification => ({ + id: "notif-1", + userId: "user-1", + teamId: "team-1", + type: "teams", + notificationName: "Teams Alert", + address: "https://xxxxx.webhook.office.com/webhookb2/test", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, +}); + +const createMessage = (overrides?: Partial): NotificationMessage => ({ + type: "monitor_down", + severity: "critical", + monitor: { + id: "mon-1", + name: "API Server", + url: "https://api.example.com", + type: "http", + status: "down", + }, + content: { + title: "🔴 Monitor Down", + summary: "API Server is not responding", + details: ["Response timeout after 30s"], + thresholds: [], + incident: { + id: "inc-1", + url: "https://app.example.com/incidents/inc-1", + createdAt: new Date(), + }, + timestamp: new Date("2026-01-15T12:00:00Z"), + }, + clientHost: "https://app.example.com", + metadata: { + teamId: "team-1", + notificationReason: "monitor_down", + }, + ...overrides, +}); + +describe("TeamsProvider", () => { + let provider: TeamsProvider; + let logger: ReturnType; + let gotPost: jest.Mock; + + beforeEach(async () => { + logger = createLogger(); + provider = new TeamsProvider(logger); + const got = await import("got"); + gotPost = got.default.post as jest.Mock; + gotPost.mockReset(); + gotPost.mockResolvedValue({}); + }); + + describe("sendTestAlert", () => { + it("returns false when address is missing", async () => { + const notification = createNotification({ address: undefined }); + const result = await provider.sendTestAlert(notification); + expect(result).toBe(false); + expect(gotPost).not.toHaveBeenCalled(); + }); + + it("sends an Adaptive Card test message and returns true on success", async () => { + const notification = createNotification(); + const result = await provider.sendTestAlert(notification); + expect(result).toBe(true); + expect(gotPost).toHaveBeenCalledTimes(1); + + const [url, options] = gotPost.mock.calls[0] as [string, { json: any; headers: any }]; + expect(url).toBe(notification.address); + expect(options.headers["Content-Type"]).toBe("application/json"); + + // Verify Teams webhook envelope + const payload = options.json; + expect(payload.type).toBe("message"); + expect(payload.attachments).toHaveLength(1); + expect(payload.attachments[0].contentType).toBe("application/vnd.microsoft.card.adaptive"); + + // Verify Adaptive Card structure + const card = payload.attachments[0].content; + expect(card.type).toBe("AdaptiveCard"); + expect(card.version).toBe("1.4"); + expect(card.body[0].type).toBe("TextBlock"); + expect(card.body[0].text).toContain("test notification"); + }); + + it("returns false and logs warning on HTTP error", async () => { + gotPost.mockRejectedValue(new Error("Network error")); + const notification = createNotification(); + const result = await provider.sendTestAlert(notification); + expect(result).toBe(false); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Teams test alert failed", + service: "TeamsProvider", + }) + ); + }); + }); + + describe("sendMessage", () => { + it("returns false when address is missing", async () => { + const notification = createNotification({ address: undefined }); + const result = await provider.sendMessage(notification, createMessage()); + expect(result).toBe(false); + expect(gotPost).not.toHaveBeenCalled(); + }); + + it("sends a well-formed Adaptive Card and returns true", async () => { + const notification = createNotification(); + const message = createMessage(); + const result = await provider.sendMessage(notification, message); + + expect(result).toBe(true); + expect(gotPost).toHaveBeenCalledTimes(1); + + const [url, options] = gotPost.mock.calls[0] as [string, { json: any }]; + expect(url).toBe(notification.address); + + const payload = options.json; + expect(payload.type).toBe("message"); + + const card = payload.attachments[0].content; + expect(card.type).toBe("AdaptiveCard"); + expect(card.version).toBe("1.4"); + + // Verify body contains expected elements + const textBlocks = card.body.filter((b: any) => b.type === "TextBlock"); + const factSets = card.body.filter((b: any) => b.type === "FactSet"); + + // Title block + expect(textBlocks[0].text).toBe(message.content.title); + expect(textBlocks[0].color).toBe("attention"); // critical -> attention + + // Summary block + expect(textBlocks[1].text).toBe(message.content.summary); + + // FactSet with monitor details + expect(factSets).toHaveLength(1); + const facts = factSets[0].facts; + expect(facts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: "Name", value: "API Server" }), + expect.objectContaining({ title: "URL", value: "https://api.example.com" }), + expect.objectContaining({ title: "Status", value: "down" }), + ]) + ); + + // Incident action button + expect(card.actions).toHaveLength(1); + expect(card.actions[0].type).toBe("Action.OpenUrl"); + expect(card.actions[0].title).toBe("View Incident"); + expect(card.actions[0].url).toContain("/infrastructure/mon-1"); + }); + + it("omits actions when no incident is present", async () => { + const notification = createNotification(); + const message = createMessage({ + content: { + title: "Test", + summary: "Summary", + timestamp: new Date(), + }, + }); + const result = await provider.sendMessage(notification, message); + expect(result).toBe(true); + + const [, options] = gotPost.mock.calls[0] as [string, { json: any }]; + const card = options.json.attachments[0].content; + expect(card.actions).toBeUndefined(); + }); + + it("includes threshold breaches when present", async () => { + const notification = createNotification(); + const message = createMessage({ + type: "threshold_breach", + severity: "warning", + content: { + title: "Threshold Breach", + summary: "CPU usage exceeded", + thresholds: [ + { + metric: "cpu", + currentValue: 95, + threshold: 80, + unit: "%", + formattedValue: "95%", + }, + ], + timestamp: new Date(), + }, + }); + + await provider.sendMessage(notification, message); + + const [, options] = gotPost.mock.calls[0] as [string, { json: any }]; + const card = options.json.attachments[0].content; + + // Title should use "warning" color + expect(card.body[0].color).toBe("warning"); + + // Find threshold text blocks + const thresholdHeader = card.body.find((b: any) => b.type === "TextBlock" && b.text === "**Threshold Breaches**"); + expect(thresholdHeader).toBeDefined(); + + const cpuBlock = card.body.find((b: any) => b.type === "TextBlock" && b.text?.includes("CPU")); + expect(cpuBlock).toBeDefined(); + expect(cpuBlock.text).toContain("95%"); + expect(cpuBlock.text).toContain("threshold: 80%"); + }); + + it("maps severity to correct Adaptive Card colors", async () => { + const notification = createNotification(); + + const severityMap = [ + { severity: "critical", expected: "attention" }, + { severity: "warning", expected: "warning" }, + { severity: "success", expected: "good" }, + { severity: "info", expected: "accent" }, + ] as const; + + for (const { severity, expected } of severityMap) { + gotPost.mockReset(); + gotPost.mockResolvedValue({}); + + const message = createMessage({ severity }); + await provider.sendMessage(notification, message); + + const [, options] = gotPost.mock.calls[0] as [string, { json: any }]; + const card = options.json.attachments[0].content; + expect(card.body[0].color).toBe(expected); + } + }); + + it("returns false and logs warning on HTTP error", async () => { + gotPost.mockRejectedValue(new Error("502 Bad Gateway")); + const notification = createNotification(); + const result = await provider.sendMessage(notification, createMessage()); + expect(result).toBe(false); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Teams alert failed via sendMessage", + service: "TeamsProvider", + }) + ); + }); + }); +}); From b11ba68ff8ea7916816146941a5b8902dd0e8a77 Mon Sep 17 00:00:00 2001 From: "tobias.bruckert@mcon-group.com" <62531735+tb102122@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:47:35 +1100 Subject: [PATCH 2/5] adjust based on review --- .../service/infrastructure/notificationProviders/teams.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/teams.ts b/server/src/service/infrastructure/notificationProviders/teams.ts index a10920d2a7..dd9b217a74 100644 --- a/server/src/service/infrastructure/notificationProviders/teams.ts +++ b/server/src/service/infrastructure/notificationProviders/teams.ts @@ -3,12 +3,13 @@ import type { Notification } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import type { ILogger } from "@/utils/logger.js"; import got, { HTTPError } from "got"; export class TeamsProvider implements INotificationProvider { - private logger: any; + private logger: ILogger; - constructor(logger: any) { + constructor(logger: ILogger) { this.logger = logger; } @@ -72,7 +73,7 @@ export class TeamsProvider implements INotificationProvider { }); return true; } catch (error) { - const err = error as Error; + const err = error as HTTPError; this.logger.warn({ message: "Teams alert failed via sendMessage", service: SERVICE_NAME, From 6c421717f8b43e33a37f905cf3e626f3552e7645 Mon Sep 17 00:00:00 2001 From: "tobias.bruckert@mcon-group.com" <62531735+tb102122@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:58:04 +1100 Subject: [PATCH 3/5] run formatting --- server/src/service/business/checkService.ts | 1 - server/src/service/business/diagnosticService.ts | 1 - server/src/service/business/geoChecksService.ts | 1 - server/src/service/business/incidentService.ts | 1 - server/src/service/business/inviteService.ts | 1 - server/src/service/business/maintenanceWindowService.ts | 1 - .../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts | 1 - .../infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts | 1 - server/src/service/infrastructure/bufferService.ts | 1 - server/src/service/infrastructure/emailService.ts | 1 - server/src/service/infrastructure/globalPingService.ts | 1 - server/src/service/infrastructure/networkService.ts | 1 - server/src/service/system/settingsService.ts | 1 - 13 files changed, 13 deletions(-) diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8779b0a00d..c83a3d411d 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -194,4 +194,3 @@ export class CheckService implements ICheckService { throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); }; } - diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index f7a8b8ff95..f9c53f6333 100644 --- a/server/src/service/business/diagnosticService.ts +++ b/server/src/service/business/diagnosticService.ts @@ -107,4 +107,3 @@ export class DiagnosticService implements IDiagnosticService { return diagnostics; }; } - diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 5c5869d94c..4a31348b0f 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -188,4 +188,3 @@ export class GeoChecksService implements IGeoChecksService { return result; }; } - diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 81a98893ae..710336016d 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -266,4 +266,3 @@ export class IncidentService implements IIncidentService { } }; } - diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index 26b3b9dbeb..9d1cbf59dc 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -94,4 +94,3 @@ export class InviteService implements IInviteService { return await this.invitesRepository.findByToken(inviteToken); }; } - diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 2257718f3f..abf513eb5d 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -96,4 +96,3 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; } - diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index 216a821b1c..2857f5b825 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -289,4 +289,3 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; } - diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 11a44d1bdb..011008b322 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -412,4 +412,3 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return decision; } } - diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index ae89a1c094..229c5e7d43 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -177,4 +177,3 @@ export class BufferService implements IBufferService { } } } - diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index 180a728f3b..f536f3f188 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -178,4 +178,3 @@ export class EmailService implements IEmailService { } }; } - diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index 0a65412f49..b58486ad38 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -206,4 +206,3 @@ export class GlobalPingService implements IGlobalPingService { return new Promise((resolve) => setTimeout(resolve, ms)); } } - diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index a459c6480b..e475bc24b4 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -197,4 +197,3 @@ export class NetworkService implements INetworkService { } } } - diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 46959c5542..1d75c0a36b 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -74,4 +74,3 @@ export class SettingsService implements ISettingsService { return settings; }; } - From 08fd09f59b23cfad13442adb8af8978892db0cf5 Mon Sep 17 00:00:00 2001 From: "tobias.bruckert@mcon-group.com" <62531735+tb102122@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:48:19 +1100 Subject: [PATCH 4/5] fix build errors --- server/src/service/business/userService.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index fde9d62927..af845d6ad8 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -141,7 +141,7 @@ export class UserService implements IUserService { message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser.id, + details: { userId: newUser.id }, }); delete newUser.profileImage; @@ -205,7 +205,7 @@ export class UserService implements IUserService { message: "New user created by superadmin", service: SERVICE_NAME, method: "createUser", - details: newUser.id, + details: { userId: newUser.id }, }); newUser.profileImage = undefined; @@ -272,14 +272,17 @@ export class UserService implements IUserService { await this.recoveryTokensRepository.deleteManyByEmail(email); const recoveryToken = await this.recoveryTokensRepository.create(email); const name = user.firstName; - const { clientHost } = this.settingsService.getSettings(); - const url = `${clientHost}/set-new-password/${recoveryToken.token}`; + const settings = this.settingsService.getSettings(); + const url = `${settings.clientHost!}/set-new-password/${recoveryToken.token}`; const html = await this.emailService.buildEmail("passwordResetTemplate", { name, email, url, }); + if (!html) { + throw new Error("Failed to build password reset email HTML"); + } const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); return msgId; }; From 2cbc667f300b52450ced44e15f245334b8ef8d51 Mon Sep 17 00:00:00 2001 From: "tobias.bruckert@mcon-group.com" <62531735+tb102122@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:43:14 +1100 Subject: [PATCH 5/5] add type checks and remove unneeded vars --- .../notificationProviders/teams.ts | 64 +++++++++++++++++-- .../src/validation/notificationValidation.ts | 13 ++-- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/teams.ts b/server/src/service/infrastructure/notificationProviders/teams.ts index dd9b217a74..2a44768075 100644 --- a/server/src/service/infrastructure/notificationProviders/teams.ts +++ b/server/src/service/infrastructure/notificationProviders/teams.ts @@ -6,6 +6,58 @@ import { getTestMessage } from "@/service/infrastructure/notificationProviders/u import type { ILogger } from "@/utils/logger.js"; import got, { HTTPError } from "got"; +// Types for Adaptive Card elements +type TextBlock = { + type: "TextBlock"; + text: string; + weight?: "Bolder" | "Normal" | "Lighter"; + size?: "Small" | "Medium" | "Large" | "ExtraLarge"; + color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention"; + wrap?: boolean; + spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto"; + isSubtle?: boolean; +}; + +type ColumnSet = { + type: "ColumnSet"; + separator?: boolean; + spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto"; + columns: unknown[]; +}; + +type Fact = { + title: string; + value: string; +}; + +type FactSet = { + type: "FactSet"; + facts: Fact[]; +}; + +type Action = { + type: string; + title: string; + url?: string; +}; + +type AdaptiveCard = { + type: "AdaptiveCard"; + $schema: "http://adaptivecards.io/schemas/adaptive-card.json"; + version: "1.4"; + body: (TextBlock | ColumnSet | FactSet)[]; + actions?: Action[]; +}; + +type TeamsMessage = { + type: "message"; + attachments: Array<{ + contentType: "application/vnd.microsoft.card.adaptive"; + contentUrl: null; + content: AdaptiveCard; + }>; +}; + export class TeamsProvider implements INotificationProvider { private logger: ILogger; @@ -87,7 +139,7 @@ export class TeamsProvider implements INotificationProvider { /** * Wrap an Adaptive Card in the Teams webhook envelope format */ - private wrapAdaptiveCard(card: object): object { + private wrapAdaptiveCard(card: AdaptiveCard): TeamsMessage { return { type: "message", attachments: [ @@ -103,16 +155,16 @@ export class TeamsProvider implements INotificationProvider { /** * Build an Adaptive Card from NotificationMessage */ - private buildAdaptiveCard(message: NotificationMessage): object { + private buildAdaptiveCard(message: NotificationMessage): AdaptiveCard { const colorMap: Record = { critical: "attention", warning: "warning", success: "good", info: "accent", }; - const color = colorMap[message.severity] || "default"; + const color = (colorMap[message.severity] || "default") as "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention" | undefined; - const body: any[] = []; + const body: (TextBlock | ColumnSet | FactSet)[] = []; // Header with colored status indicator body.push({ @@ -202,12 +254,12 @@ export class TeamsProvider implements INotificationProvider { }); // Actions (incident link) - const actions: any[] = []; + const actions: Action[] = []; if (message.content.incident) { actions.push({ type: "Action.OpenUrl", title: "View Incident", - url: `${message.clientHost}/infrastructure/${message.monitor.id}`, + url: `${message.clientHost}/incidents/${message.content.incident.id}`, }); } diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index 4c66302d96..5aa0b976eb 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -18,7 +18,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("webhook"), - address: z.string().url("Please enter a valid Webhook URL"), + address: z.url({ message: "Please enter a valid Webhook URL" }), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), @@ -27,7 +27,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("slack"), - address: z.string().url("Please enter a valid Webhook URL"), + address: z.url({ message: "Please enter a valid Webhook URL" }), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), @@ -36,7 +36,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("discord"), - address: z.string().url("Please enter a valid Webhook URL"), + address: z.url({ message: "Please enter a valid Webhook URL" }), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), @@ -55,7 +55,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("matrix"), address: z.union([z.string(), z.literal("")]).optional(), - homeserverUrl: z.string().url("Please enter a valid Homeserver URL"), + homeserverUrl: z.url({ message: "Please enter a valid Homeserver URL" }), roomId: z.string().min(1, "Room ID is required"), accessToken: z.string().min(1, "Access Token is required"), }), @@ -63,10 +63,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("teams"), - address: z.string().url("Please enter a valid Webhook URL"), - homeserverUrl: z.union([z.string(), z.literal("")]).optional(), - roomId: z.union([z.string(), z.literal("")]).optional(), - accessToken: z.union([z.string(), z.literal("")]).optional(), + address: z.url({ message: "Please enter a valid Webhook URL" }), }), ]);