Merge main into feature/pushover-notifications, add Microsoft Teams provider

Both branches added a migration at index 0062. Resolved by keeping
upstream's teams migration at 0062 and promoting pushover to 0063.
Both providers are now included in the enum, types, handlers, and forms.
This commit is contained in:
Peter Crosthwaite
2026-06-19 18:27:45 +10:00
48 changed files with 4180 additions and 2854 deletions
@@ -0,0 +1 @@
ALTER TYPE "public"."provider_kind" ADD VALUE 'teams';
+2 -2
View File
@@ -1,5 +1,5 @@
{
"id": "93d81732-8e94-4c2e-be9d-79c7bacc398e",
"id": "679bcdd7-cf48-43d5-965d-0814b49196e1",
"prevId": "e6d15bce-96b6-468c-8853-08efe5a18ecd",
"version": "7",
"dialect": "postgresql",
@@ -2844,7 +2844,7 @@
"ntfy",
"webhook",
"nextcloud",
"pushover"
"teams"
]
},
"public.event_kind": {
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -439,8 +439,15 @@
{
"idx": 62,
"version": "7",
"when": 1780934519843,
"tag": "0062_futuristic_dragon_lord",
"breakpoints": true
},
{
"idx": 63,
"version": "7",
"when": 1781854323074,
"tag": "0062_open_proudstar",
"tag": "0063_open_proudstar",
"breakpoints": true
}
]
+1 -1
View File
@@ -7,7 +7,7 @@ import {z} from "zod";
import {OrganizationInvitation} from "@/db/schema/05_invitation";
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'discord', 'telegram', 'gotify', 'ntfy', 'webhook', 'nextcloud', 'pushover']);
export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'discord', 'telegram', 'gotify', 'ntfy', 'webhook', 'nextcloud', 'teams', 'pushover']);
export const notificationChannel = pgTable('notification_channel', {
id: uuid("id").defaultRandom().primaryKey(),
@@ -11,6 +11,7 @@ import {PushoverChannelConfigSchema} from "@/features/channel/notifications/push
import {S3ChannelConfigSchema} from "@/features/channel/storages/s3.schema";
import {GoogleDriveChannelConfigSchema} from "@/features/channel/storages/google-drive/google-drive.schema";
import {LocalChannelConfigSchema} from "@/features/channel/storages/local.schema";
import {TeamsChannelConfigSchema} from "@/features/channel/notifications/teams.schema";
const BaseChannelFormSchema = z.object({
@@ -54,6 +55,10 @@ export const NotificationChannelFormSchema = z.discriminatedUnion("provider", [
provider: z.literal("nextcloud"),
config: NextcloudChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("teams"),
config: TeamsChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("pushover"),
config: PushoverChannelConfigSchema,
@@ -29,6 +29,9 @@ import {
import {
notificationProviders,
} from "@/features/channel/channels-notification-helper";
import {
NotifierTeamsForm
} from "@/features/channel/notifications/teams.form";
import {storageProviders} from "@/features/channel/channels-storage-helper";
import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react";
import {LucideProps} from "lucide-react";
@@ -95,6 +98,8 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet
return <NotifierWebhookForm form={form}/>;
case "nextcloud":
return <NotifierNextcloudForm form={form}/>;
case "teams":
return <NotifierTeamsForm form={form}/>;
case "pushover":
return <NotifierPushoverForm form={form}/>;
case "s3":
@@ -14,8 +14,8 @@ export const notificationProviders: ProviderIconTypes[] = [
{value: "ntfy", label: "ntfy.sh", icon: NtfyIcon},
{value: "webhook", label: "Webhook", icon: WebhookIcon},
{value: "nextcloud", label: "Nextcloud Talk", icon: NextcloudIcon},
{value: "teams", label: "Microsoft Teams", icon: MSTeamsIcon},
{value: "pushover", label: "Pushover", icon: PushoverIcon},
{value: "microsoft-teams", label: "Microsoft Teams", icon: MSTeamsIcon, preview: true}
]
@@ -9,6 +9,7 @@ import {sendNtfy} from "@/features/channel/notifications/ntfy";
import {sendWebhook} from "@/features/channel/notifications/webhook";
import {sendNextcloud} from "@/features/channel/notifications/nextcloud";
import {sendPushover} from "@/features/channel/notifications/pushover";
import {sendTeams} from "@/features/channel/notifications/teams"
const handlers: Record<
ProviderKind,
@@ -22,6 +23,7 @@ const handlers: Record<
ntfy: sendNtfy,
webhook: sendWebhook,
nextcloud: sendNextcloud,
teams: sendTeams,
pushover: sendPushover,
};
@@ -0,0 +1,38 @@
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Separator } from "@/components/ui/separator";
import { PasswordInput } from "@/components/ui/password-input";
type NotifierTeamsFormProps = {
form: UseFormReturn<any, any, any>;
};
export const NotifierTeamsForm = ({ form }: NotifierTeamsFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.teamsWebhook"
render={({ field }) => (
<FormItem>
<FormLabel>Teams Webhook URL *</FormLabel>
<FormControl>
<PasswordInput
{...field}
placeholder="e.g. https://xxxxx.logic.azure.com:443/xxxxx"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
};
@@ -0,0 +1,5 @@
import {z} from "zod";
export const TeamsChannelConfigSchema = z.object({
teamsWebhook: z.string().url("Must be a valid URL"),
});
@@ -0,0 +1,93 @@
import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types';
export async function sendTeams(
config: { teamsWebhook: string },
payload: EventPayload
): Promise<DispatchResult> {
const { teamsWebhook: webhookUrl } = config;
const levelColor: Record<string, string> = {
critical: 'attention',
warning: 'warning',
info: 'good',
};
const accentColor = levelColor[payload.level] ?? 'default';
const bodyItems: object[] = [
{
type: 'TextBlock',
text: `**[${payload.level.toUpperCase()}] ${payload.title}**`,
wrap: true,
color: accentColor,
size: 'Medium',
weight: 'Bolder',
},
{
type: 'TextBlock',
text: payload.message,
wrap: true,
},
];
if (payload.data) {
bodyItems.push(
{
type: 'TextBlock',
text: '**Data:**',
wrap: true,
weight: 'Bolder',
},
{
type: 'TextBlock',
text: JSON.stringify(payload.data, null, 2).substring(0, 1000),
wrap: true,
fontType: 'Monospace',
}
);
}
const adaptiveCard = {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.2',
body: bodyItems,
};
const body = {
type: 'message',
attachments: [
{
contentType: 'application/vnd.microsoft.card.adaptive',
contentUrl: null,
content: adaptiveCard,
},
],
};
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
let res: Response;
try {
res = await fetch(webhookUrl, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}
if (!res.ok) {
const err = await res.text();
throw new Error(`Teams error: ${res.status} ${err}`);
}
return {
success: true,
provider: 'teams',
message: 'Sent to Microsoft Teams',
response: res.statusText,
};
}
@@ -1,4 +1,4 @@
export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook' | 'nextcloud' | 'pushover';
export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook' | 'nextcloud' | 'teams' | 'pushover';
export interface DispatchResult {
success: boolean;