mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge pull request #105 from Portabase/feat/provider-validation
Feat/provider validation
This commit is contained in:
-36
@@ -1,36 +0,0 @@
|
||||
import { PageParams } from "@/types/next";
|
||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
||||
import { notFound } from "next/navigation";
|
||||
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
|
||||
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
||||
const { databaseId } = await props.params;
|
||||
|
||||
const dbItem = await db.query.database.findFirst({
|
||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
||||
});
|
||||
|
||||
if (!dbItem) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<PageTitle>Edit {dbItem.name}</PageTitle>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<DatabaseForm
|
||||
databaseId={databaseId}
|
||||
// @ts-ignore
|
||||
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
||||
/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -1,161 +1,198 @@
|
||||
"use client"
|
||||
import {Button, ButtonVariantsProps} from "@/components/ui/button";
|
||||
import {ReactNode, useState} from "react";
|
||||
import {Loader2} from "lucide-react";
|
||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
"use client";
|
||||
import { Button, ButtonVariantsProps } from "@/components/ui/button";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
export type ButtonWithConfirmProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
button?: {
|
||||
main: {
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
text?: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
size?: ButtonVariantsProps["size"];
|
||||
disabled?: boolean;
|
||||
tooltipText?: string;
|
||||
};
|
||||
confirm: {
|
||||
className?: string;
|
||||
text: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
size?: ButtonVariantsProps["size"];
|
||||
onClick?: () => void;
|
||||
};
|
||||
cancel: {
|
||||
className?: string;
|
||||
text: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
size?: ButtonVariantsProps["size"];
|
||||
onClick?: () => void;
|
||||
};
|
||||
title: string;
|
||||
description: string;
|
||||
button?: {
|
||||
main: {
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
text?: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
size?: ButtonVariantsProps["size"];
|
||||
disabled?: boolean;
|
||||
tooltipText?: string;
|
||||
};
|
||||
children?: ReactNode;
|
||||
onConfirm?: (e: React.MouseEvent) => void;
|
||||
onCancel?: (e: React.MouseEvent) => void;
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
isPending?: boolean;
|
||||
confirm: {
|
||||
className?: string;
|
||||
text: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
size?: ButtonVariantsProps["size"];
|
||||
onClick?: () => void;
|
||||
};
|
||||
cancel: {
|
||||
className?: string;
|
||||
text: string;
|
||||
icon?: any;
|
||||
variant?: ButtonVariantsProps["variant"];
|
||||
size?: ButtonVariantsProps["size"];
|
||||
onClick?: () => void;
|
||||
};
|
||||
};
|
||||
children?: ReactNode;
|
||||
onConfirm?: (e: React.MouseEvent) => void;
|
||||
onCancel?: (e: React.MouseEvent) => void;
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
isPending?: boolean;
|
||||
};
|
||||
|
||||
|
||||
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
const isLegacy = !!props.button;
|
||||
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
|
||||
const isLegacy = !!props.button;
|
||||
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
|
||||
|
||||
const handleConfirm = (e: React.MouseEvent) => {
|
||||
const handleConfirm = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isLegacy) {
|
||||
props.button?.confirm.onClick?.();
|
||||
} else {
|
||||
props.onConfirm?.(e);
|
||||
}
|
||||
setIsConfirming(false);
|
||||
};
|
||||
|
||||
const handleCancel = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isLegacy) {
|
||||
props.button?.cancel.onClick?.();
|
||||
} else {
|
||||
props.onCancel?.(e);
|
||||
}
|
||||
setIsConfirming(false);
|
||||
};
|
||||
|
||||
const triggerContent = isLegacy ? (
|
||||
<Button
|
||||
type={props.button?.main.type}
|
||||
disabled={isDisabled}
|
||||
variant={props.button?.main.variant ?? "default"}
|
||||
size={props.button?.main.size ?? "default"}
|
||||
className={props.button?.main.className}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isLegacy) {
|
||||
props.button?.confirm.onClick?.();
|
||||
} else {
|
||||
props.onConfirm?.(e);
|
||||
}
|
||||
setIsConfirming(false);
|
||||
};
|
||||
|
||||
const handleCancel = (e: React.MouseEvent) => {
|
||||
if (!isDisabled) setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||
{props.button?.main.icon}
|
||||
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isLegacy) {
|
||||
props.button?.cancel.onClick?.();
|
||||
} else {
|
||||
props.onCancel?.(e);
|
||||
}
|
||||
setIsConfirming(false);
|
||||
};
|
||||
setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const triggerContent = isLegacy ? (
|
||||
<Button
|
||||
type={props.button?.main.type}
|
||||
disabled={isDisabled}
|
||||
variant={props.button?.main.variant ?? "default"}
|
||||
size={props.button?.main.size ?? "default"}
|
||||
className={props.button?.main.className}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDisabled) setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{props.button?.main.icon}
|
||||
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
||||
</Button>
|
||||
const withTooltip =
|
||||
isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>{triggerContent}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{props.button?.main.tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsConfirming(true);
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
triggerContent
|
||||
);
|
||||
|
||||
const withTooltip = isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>{triggerContent}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{props.button?.main.tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : triggerContent;
|
||||
|
||||
return (
|
||||
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
||||
<PopoverTrigger asChild>
|
||||
{withTooltip}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80"
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseMove={(e) => e.stopPropagation()}
|
||||
onMouseEnter={(e) => e.stopPropagation()}
|
||||
onMouseLeave={(e) => e.stopPropagation()}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
return (
|
||||
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
||||
<PopoverTrigger asChild>{withTooltip}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80"
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseMove={(e) => e.stopPropagation()}
|
||||
onMouseEnter={(e) => e.stopPropagation()}
|
||||
onMouseLeave={(e) => e.stopPropagation()}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">{props.title}</h4>
|
||||
<p className="text-sm text-muted-foreground">{props.description}</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant={
|
||||
isLegacy
|
||||
? (props.button?.confirm.variant ?? "default")
|
||||
: "default"
|
||||
}
|
||||
size={
|
||||
isLegacy ? (props.button?.confirm.size ?? "default") : "default"
|
||||
}
|
||||
className={cn(
|
||||
isLegacy ? props.button?.confirm.className : "",
|
||||
"w-full",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium leading-none">{props.title}</h4>
|
||||
<p className="text-sm text-muted-foreground">{props.description}</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant={isLegacy ? (props.button?.confirm.variant ?? "default") : "default"}
|
||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{isLegacy && props.button?.confirm.icon}
|
||||
<span>{isLegacy ? props.button?.confirm.text : (props.confirmButtonText ?? "Confirm")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={isLegacy ? (props.button?.cancel.variant ?? "outline") : "outline"}
|
||||
onClick={handleCancel}
|
||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
||||
>
|
||||
{isLegacy ? (props.button?.cancel.text ?? "Cancel") : (props.cancelButtonText ?? "Cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
{props.isPending && (
|
||||
<Loader2 className="animate-spin mr-4" size={16} />
|
||||
)}
|
||||
{isLegacy && props.button?.confirm.icon}
|
||||
<span>
|
||||
{isLegacy
|
||||
? props.button?.confirm.text
|
||||
: (props.confirmButtonText ?? "Confirm")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
isLegacy
|
||||
? (props.button?.cancel.variant ?? "outline")
|
||||
: "outline"
|
||||
}
|
||||
onClick={handleCancel}
|
||||
className={cn(
|
||||
isLegacy ? props.button?.cancel.className : "",
|
||||
"w-full",
|
||||
)}
|
||||
size={
|
||||
isLegacy ? (props.button?.cancel.size ?? "default") : "default"
|
||||
}
|
||||
>
|
||||
{isLegacy
|
||||
? (props.button?.cancel.text ?? "Cancel")
|
||||
: (props.cancelButtonText ?? "Cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
+55
-12
@@ -1,26 +1,69 @@
|
||||
import {z} from "zod";
|
||||
import {SlackChannelConfigSchema} from "./providers/notifications/forms/slack.schema";
|
||||
import {SmtpChannelConfigSchema} from "./providers/notifications/forms/smtp.schema";
|
||||
import {DiscordChannelConfigSchema} from "./providers/notifications/forms/discord.schema";
|
||||
import {TelegramChannelConfigSchema} from "./providers/notifications/forms/telegram.schema";
|
||||
import {GotifyChannelConfigSchema} from "./providers/notifications/forms/gotify.schema";
|
||||
import {NtfyChannelConfigSchema} from "./providers/notifications/forms/ntfy.schema";
|
||||
import {WebhookChannelConfigSchema} from "./providers/notifications/forms/webhook.schema";
|
||||
import {S3ChannelConfigSchema} from "./providers/storages/forms/s3.schema";
|
||||
import {GoogleDriveChannelConfigSchema} from "./providers/storages/forms/google-drive.schema";
|
||||
import {LocalChannelConfigSchema} from "./providers/storages/forms/local.schema";
|
||||
|
||||
|
||||
export const ChannelFormSchema = z.object({
|
||||
const BaseChannelFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(5, "Name must be at least 5 characters long")
|
||||
.max(40, "Name must be at most 40 characters long"),
|
||||
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const NotificationChannelFormSchema = z.discriminatedUnion("provider", [
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("slack"),
|
||||
config: SlackChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("smtp"),
|
||||
config: SmtpChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("discord"),
|
||||
config: DiscordChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("telegram"),
|
||||
config: TelegramChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("gotify"),
|
||||
config: GotifyChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("ntfy"),
|
||||
config: NtfyChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("webhook"),
|
||||
config: WebhookChannelConfigSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const NotificationChannelFormSchema = ChannelFormSchema.extend({
|
||||
provider: z.enum(
|
||||
["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"],
|
||||
{required_error: "Provider is required"}
|
||||
),
|
||||
});
|
||||
|
||||
export const StorageChannelFormSchema = ChannelFormSchema.extend({
|
||||
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
|
||||
});
|
||||
export const StorageChannelFormSchema = z.discriminatedUnion("provider", [
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("s3"),
|
||||
config: S3ChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("google-drive"),
|
||||
config: GoogleDriveChannelConfigSchema,
|
||||
}),
|
||||
BaseChannelFormSchema.extend({
|
||||
provider: z.literal("local"),
|
||||
config: LocalChannelConfigSchema
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const DiscordChannelConfigSchema = z.object({
|
||||
discordWebhook: z.string().url("Must be a valid URL"),
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const GotifyChannelConfigSchema = z.object({
|
||||
gotifyServerUrl: z.string().url("Must be a valid URL"),
|
||||
gotifyAppToken: z.string().min(1, "App token is required"),
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const NtfyChannelConfigSchema = z.object({
|
||||
ntfyTopic: z.string().min(1, "Topic is required"),
|
||||
ntfyServerUrl: z.string().url("Must be a valid URL").optional().or(z.literal('')),
|
||||
ntfyToken: z.string().optional(),
|
||||
ntfyUsername: z.string().optional(),
|
||||
ntfyPassword: z.string().optional(),
|
||||
});
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const SlackChannelConfigSchema = z.object({
|
||||
slackWebhook: z.string().url("Must be a valid URL"),
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const SmtpChannelConfigSchema = z.object({
|
||||
host: z.string().min(1, "Host is required"),
|
||||
port: z.coerce.number().min(1, "Port is required"),
|
||||
user: z.string().min(1, "User is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
from: z.string().min(1, "From is required"),
|
||||
to: z.string().min(1, "To is required"),
|
||||
});
|
||||
+16
@@ -43,6 +43,22 @@ export const NotifierTelegramForm = ({form}: NotifierTelegramFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.telegramTopicId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telegram Topic ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="123456"/>
|
||||
</FormControl>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Unique identifier for the target message thread (topic) of the forum; for forum supergroups only
|
||||
</p>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const TelegramChannelConfigSchema = z
|
||||
.object({
|
||||
telegramBotToken: z.string().min(1, "Bot token is required"),
|
||||
telegramChatId: z.string().min(1, "Chat ID is required"),
|
||||
telegramTopicId: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.telegramTopicId && !data.telegramChatId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Chat ID is required when a Topic ID is provided",
|
||||
path: ["telegramChatId"],
|
||||
},
|
||||
);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const WebhookChannelConfigSchema = z.object({
|
||||
webhookUrl: z.string().url("Must be a valid URL"),
|
||||
webhookSecretHeader: z.string().optional(),
|
||||
webhookSecret: z.string().optional(),
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const GoogleDriveChannelConfigSchema = z.object({
|
||||
clientId: z.string().min(1, "Client ID is required"),
|
||||
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||
folderId: z.string().min(1, "Folder ID is required"),
|
||||
refreshToken: z.string().optional(),
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const LocalChannelConfigSchema = z.object({});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const S3ChannelConfigSchema = z.object({
|
||||
endPointUrl: z.string().min(1, "Endpoint URL is required"),
|
||||
region: z.string().min(1, "Region is required"),
|
||||
accessKey: z.string().min(1, "Access Key is required"),
|
||||
secretKey: z.string().min(1, "Secret Key is required"),
|
||||
bucketName: z.string().min(1, "Bucket name is required"),
|
||||
port: z.coerce.number().optional(),
|
||||
ssl: z.boolean().optional().default(true),
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
"use client";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { GearIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export type EditButtonProps = {};
|
||||
|
||||
export const EditButton = (props: EditButtonProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Link className={buttonVariants({ variant: "outline" })} href={`${pathname}/edit`}>
|
||||
<GearIcon className="w-7 h-7" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +1,62 @@
|
||||
import type {EventPayload, DispatchResult} from '../types';
|
||||
import type { EventPayload, DispatchResult } from "../types";
|
||||
|
||||
export async function sendTelegram(
|
||||
config: { telegramBotToken: string; telegramChatId: string },
|
||||
payload: EventPayload
|
||||
config: {
|
||||
telegramBotToken: string;
|
||||
telegramChatId: string;
|
||||
telegramTopicId?: string;
|
||||
},
|
||||
payload: EventPayload,
|
||||
): Promise<DispatchResult> {
|
||||
const {telegramBotToken, telegramChatId} = config;
|
||||
const { telegramBotToken, telegramChatId, telegramTopicId } = config;
|
||||
|
||||
// Helper to escape HTML characters
|
||||
const escapeHtml = (unsafe: string) => {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
// Helper to escape HTML characters
|
||||
const escapeHtml = (unsafe: string) => {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
const title = escapeHtml(payload.title);
|
||||
const message = escapeHtml(payload.message);
|
||||
const level = escapeHtml(payload.level.toUpperCase());
|
||||
const dataString = payload.data ? escapeHtml(JSON.stringify(payload.data, null, 2).substring(0, 1000)) : '';
|
||||
const title = escapeHtml(payload.title);
|
||||
const message = escapeHtml(payload.message);
|
||||
const level = escapeHtml(payload.level.toUpperCase());
|
||||
const dataString = payload.data
|
||||
? escapeHtml(JSON.stringify(payload.data, null, 2).substring(0, 1000))
|
||||
: "";
|
||||
|
||||
const text = `<b>${title}</b>\n\n${message}\n\nLevel: <code>${level}</code>${payload.data ? `\n\nData:\n<pre>${dataString}</pre>` : ''}`;
|
||||
const text = `<b>${title}</b>\n\n${message}\n\nLevel: <code>${level}</code>${payload.data ? `\n\nData:\n<pre>${dataString}</pre>` : ""}`;
|
||||
|
||||
const body = {
|
||||
chat_id: telegramChatId,
|
||||
text,
|
||||
parse_mode: 'HTML',
|
||||
};
|
||||
const body: Record<string, any> = {
|
||||
chat_id: telegramChatId,
|
||||
text,
|
||||
parse_mode: "HTML",
|
||||
};
|
||||
|
||||
const res = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
if (telegramTopicId) {
|
||||
body.message_thread_id = Number(telegramTopicId);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Telegram error: ${res.status} ${err}`);
|
||||
}
|
||||
const res = await fetch(
|
||||
`https://api.telegram.org/bot${telegramBotToken}/sendMessage`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'telegram',
|
||||
message: 'Sent to Telegram',
|
||||
response: await res.text(),
|
||||
};
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Telegram error: ${res.status} ${err}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "telegram",
|
||||
message: "Sent to Telegram",
|
||||
response: await res.text(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user