refactor(webhooks): headers as array
This commit is contained in:
parent
f439fb39a1
commit
be7eef7028
15 changed files with 129 additions and 106 deletions
|
|
@ -2436,12 +2436,12 @@ export type ListBackupSchedulesResponses = {
|
|||
customResticParams: Array<string> | null;
|
||||
preBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries: number;
|
||||
|
|
@ -2724,12 +2724,12 @@ export type CreateBackupScheduleData = {
|
|||
customResticParams?: Array<string>;
|
||||
preBackupWebhook?: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook?: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries?: number;
|
||||
|
|
@ -2769,12 +2769,12 @@ export type CreateBackupScheduleResponses = {
|
|||
customResticParams: Array<string> | null;
|
||||
preBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries: number;
|
||||
|
|
@ -2848,12 +2848,12 @@ export type GetBackupScheduleResponses = {
|
|||
customResticParams: Array<string> | null;
|
||||
preBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries: number;
|
||||
|
|
@ -3135,12 +3135,12 @@ export type UpdateBackupScheduleData = {
|
|||
customResticParams?: Array<string>;
|
||||
preBackupWebhook?: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook?: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries?: number;
|
||||
|
|
@ -3182,12 +3182,12 @@ export type UpdateBackupScheduleResponses = {
|
|||
customResticParams: Array<string> | null;
|
||||
preBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries: number;
|
||||
|
|
@ -3241,12 +3241,12 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||
customResticParams: Array<string> | null;
|
||||
preBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
postBackupWebhook: {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
headers?: Array<string>;
|
||||
body?: string;
|
||||
} | null;
|
||||
maxRetries: number;
|
||||
|
|
|
|||
25
app/client/components/webhook-request-preview.tsx
Normal file
25
app/client/components/webhook-request-preview.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { CodeBlock } from "~/client/components/ui/code-block";
|
||||
import { Label } from "~/client/components/ui/label";
|
||||
|
||||
type WebhookRequestPreviewProps = {
|
||||
method: string;
|
||||
url?: string;
|
||||
contentType?: string;
|
||||
headers?: string[];
|
||||
body: string;
|
||||
};
|
||||
|
||||
export const WebhookRequestPreview = ({ method, url, contentType, headers, body }: WebhookRequestPreviewProps) => {
|
||||
const headerLines = [contentType ? `Content-Type: ${contentType}` : undefined, ...(headers ?? [])].filter(Boolean);
|
||||
const previewCode = `${method} ${url || "https://api.example.com/webhook"}${headerLines.length > 0 ? `\n${headerLines.join("\n")}` : ""}
|
||||
|
||||
${body}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-4 border-t">
|
||||
<Label>Request Preview</Label>
|
||||
<CodeBlock code={previewCode} filename="HTTP Request" />
|
||||
<p className="text-[0.8rem] text-muted-foreground">This is a preview of the HTTP request that will be sent.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Textarea } from "~/client/components/ui/textarea";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useWatch, type UseFormReturn } from "react-hook-form";
|
||||
import type { InternalFormValues } from "./types";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
|
||||
|
|
@ -9,6 +9,10 @@ type AdvancedSectionProps = {
|
|||
};
|
||||
|
||||
export const AdvancedSection = ({ form }: AdvancedSectionProps) => {
|
||||
const values = useWatch({ control: form.control });
|
||||
const preBackupWebhookBody = values.preBackupWebhookBody?.trim();
|
||||
const postBackupWebhookBody = values.postBackupWebhookBody?.trim();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
|
|
@ -74,18 +78,22 @@ export const AdvancedSection = ({ form }: AdvancedSectionProps) => {
|
|||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="preBackupWebhookHeadersText"
|
||||
name="preBackupWebhookHeaders"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Pre-backup webhook headers</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={'{\n "Authorization": "Bearer token"\n}'}
|
||||
placeholder="Authorization: Bearer token X-Custom-Header: value"
|
||||
value={Array.isArray(field.value) ? field.value.join("\n") : ""}
|
||||
onChange={(e) => field.onChange(e.target.value.split("\n"))}
|
||||
className="font-mono text-sm min-h-24"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Optional JSON object. Values are stored as plain text.</FormDescription>
|
||||
<FormDescription>
|
||||
One header per line in Key: Value format. Values are stored as plain text.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
@ -122,18 +130,22 @@ export const AdvancedSection = ({ form }: AdvancedSectionProps) => {
|
|||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="postBackupWebhookHeadersText"
|
||||
name="postBackupWebhookHeaders"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Post-backup webhook headers</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={'{\n "Authorization": "Bearer token"\n}'}
|
||||
placeholder="Authorization: Bearer token X-Custom-Header: value"
|
||||
value={Array.isArray(field.value) ? field.value.join("\n") : ""}
|
||||
onChange={(e) => field.onChange(e.target.value.split("\n"))}
|
||||
className="font-mono text-sm min-h-24"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Optional JSON object. Values are stored as plain text.</FormDescription>
|
||||
<FormDescription>
|
||||
One header per line in Key: Value format. Values are stored as plain text.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
maxRetries,
|
||||
retryDelay,
|
||||
preBackupWebhookUrl,
|
||||
preBackupWebhookHeadersText,
|
||||
preBackupWebhookHeaders,
|
||||
preBackupWebhookBody,
|
||||
postBackupWebhookUrl,
|
||||
postBackupWebhookHeadersText,
|
||||
postBackupWebhookHeaders,
|
||||
postBackupWebhookBody,
|
||||
...rest
|
||||
} = data;
|
||||
|
|
@ -74,14 +74,14 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||
preBackupWebhook: preBackupWebhookUrlValue
|
||||
? {
|
||||
url: preBackupWebhookUrlValue,
|
||||
headers: parseWebhookHeaders(preBackupWebhookHeadersText),
|
||||
headers: parseWebhookHeaders(preBackupWebhookHeaders),
|
||||
body: preBackupWebhookBodyValue || undefined,
|
||||
}
|
||||
: null,
|
||||
postBackupWebhook: postBackupWebhookUrlValue
|
||||
? {
|
||||
url: postBackupWebhookUrlValue,
|
||||
headers: parseWebhookHeaders(postBackupWebhookHeadersText),
|
||||
headers: parseWebhookHeaders(postBackupWebhookHeaders),
|
||||
body: postBackupWebhookBodyValue || undefined,
|
||||
}
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -1,29 +1,13 @@
|
|||
import { z } from "zod";
|
||||
|
||||
const webhookHeadersTextSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(value) => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
!Array.isArray(parsed) &&
|
||||
Object.values(parsed).every((headerValue) => typeof headerValue === "string")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: "Headers must be a JSON object with string values" },
|
||||
);
|
||||
const webhookHeadersSchema = z
|
||||
.array(
|
||||
z.string().refine((header) => !header.trim() || header.includes(":"), {
|
||||
message: "Headers must use Key: Value format",
|
||||
}),
|
||||
)
|
||||
.catch(() => [])
|
||||
.optional();
|
||||
|
||||
export const internalFormSchema = z.object({
|
||||
name: z.string().min(1).max(128),
|
||||
|
|
@ -46,10 +30,10 @@ export const internalFormSchema = z.object({
|
|||
oneFileSystem: z.boolean().optional(),
|
||||
customResticParamsText: z.string().optional(),
|
||||
preBackupWebhookUrl: z.union([z.string().url(), z.literal("")]).optional(),
|
||||
preBackupWebhookHeadersText: webhookHeadersTextSchema,
|
||||
preBackupWebhookHeaders: webhookHeadersSchema,
|
||||
preBackupWebhookBody: z.string().optional(),
|
||||
postBackupWebhookUrl: z.union([z.string().url(), z.literal("")]).optional(),
|
||||
postBackupWebhookHeadersText: webhookHeadersTextSchema,
|
||||
postBackupWebhookHeaders: webhookHeadersSchema,
|
||||
postBackupWebhookBody: z.string().optional(),
|
||||
maxRetries: z.number().min(0).max(32).optional(),
|
||||
retryDelay: z.number().min(1).max(1440).optional(),
|
||||
|
|
@ -74,16 +58,16 @@ export type BackupScheduleFormValues = Omit<
|
|||
| "includePatterns"
|
||||
| "customResticParamsText"
|
||||
| "preBackupWebhookUrl"
|
||||
| "preBackupWebhookHeadersText"
|
||||
| "preBackupWebhookHeaders"
|
||||
| "preBackupWebhookBody"
|
||||
| "postBackupWebhookUrl"
|
||||
| "postBackupWebhookHeadersText"
|
||||
| "postBackupWebhookHeaders"
|
||||
| "postBackupWebhookBody"
|
||||
> & {
|
||||
excludePatterns?: string[];
|
||||
excludeIfPresent?: string[];
|
||||
includePatterns?: string[];
|
||||
customResticParams?: string[];
|
||||
preBackupWebhook?: { url: string; headers?: Record<string, string>; body?: string } | null;
|
||||
postBackupWebhook?: { url: string; headers?: Record<string, string>; body?: string } | null;
|
||||
preBackupWebhook?: { url: string; headers?: string[]; body?: string } | null;
|
||||
postBackupWebhook?: { url: string; headers?: string[]; body?: string } | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,21 +10,10 @@ export const parseMultilineEntries = (value?: string) =>
|
|||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
export const parseWebhookHeaders = (value?: string) => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
export const parseWebhookHeaders = (headers?: string[]) => {
|
||||
const parsedHeaders = headers?.map((header) => header.trim()).filter(Boolean) ?? [];
|
||||
|
||||
return JSON.parse(trimmed) as Record<string, string>;
|
||||
};
|
||||
|
||||
const stringifyWebhookHeaders = (headers?: Record<string, string>) => {
|
||||
if (!headers || Object.keys(headers).length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return JSON.stringify(headers, null, 2);
|
||||
return parsedHeaders.length > 0 ? parsedHeaders : undefined;
|
||||
};
|
||||
|
||||
export const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValues | undefined => {
|
||||
|
|
@ -44,10 +33,10 @@ export const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalF
|
|||
oneFileSystem: schedule.oneFileSystem ?? false,
|
||||
customResticParamsText: schedule.customResticParams?.join("\n") ?? "",
|
||||
preBackupWebhookUrl: schedule.preBackupWebhook?.url ?? "",
|
||||
preBackupWebhookHeadersText: stringifyWebhookHeaders(schedule.preBackupWebhook?.headers),
|
||||
preBackupWebhookHeaders: schedule.preBackupWebhook?.headers ?? [],
|
||||
preBackupWebhookBody: schedule.preBackupWebhook?.body ?? "",
|
||||
postBackupWebhookUrl: schedule.postBackupWebhook?.url ?? "",
|
||||
postBackupWebhookHeadersText: stringifyWebhookHeaders(schedule.postBackupWebhook?.headers),
|
||||
postBackupWebhookHeaders: schedule.postBackupWebhook?.headers ?? [],
|
||||
postBackupWebhookBody: schedule.postBackupWebhook?.body ?? "",
|
||||
maxRetries: schedule.maxRetries,
|
||||
retryDelay: schedule.retryDelay,
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ test("submits webhook headers and body as plain config values", async () => {
|
|||
await userEvent.click(await screen.findByText("Advanced"));
|
||||
await userEvent.type(screen.getByLabelText("Pre-backup webhook"), "http://localhost:8080/stop");
|
||||
fireEvent.change(screen.getByLabelText("Pre-backup webhook headers"), {
|
||||
target: { value: '{ "Authorization": "Bearer stop-token" }' },
|
||||
target: { value: "Authorization: Bearer stop-token" },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText("Pre-backup webhook body"), {
|
||||
target: { value: '{"action":"stop"}' },
|
||||
|
|
@ -143,7 +143,7 @@ test("submits webhook headers and body as plain config values", async () => {
|
|||
await expect(submittedBody).resolves.toMatchObject({
|
||||
preBackupWebhook: {
|
||||
url: "http://localhost:8080/stop",
|
||||
headers: { Authorization: "Bearer stop-token" },
|
||||
headers: ["Authorization: Bearer stop-token"],
|
||||
body: '{"action":"stop"}',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { Input } from "~/client/components/ui/input";
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||
import { Textarea } from "~/client/components/ui/textarea";
|
||||
import { CodeBlock } from "~/client/components/ui/code-block";
|
||||
import { Label } from "~/client/components/ui/label";
|
||||
import { WebhookRequestPreview } from "~/client/components/webhook-request-preview";
|
||||
import type { NotificationFormValues } from "../create-notification-form";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -35,16 +34,14 @@ const WebhookPreview = ({ values }: { values: Partial<NotificationFormValues> })
|
|||
body = "Notification message";
|
||||
}
|
||||
|
||||
const previewCode = `${values.method} ${values.url}\nContent-Type: ${contentType}${headers.length > 0 ? `\n${headers.join("\n")}` : ""}
|
||||
|
||||
${body}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pt-4 border-t">
|
||||
<Label>Request Preview</Label>
|
||||
<CodeBlock code={previewCode} filename="HTTP Request" />
|
||||
<p className="text-[0.8rem] text-muted-foreground">This is a preview of the HTTP request that will be sent.</p>
|
||||
</div>
|
||||
<WebhookRequestPreview
|
||||
method={values.method || "POST"}
|
||||
url={values.url}
|
||||
contentType={contentType}
|
||||
headers={headers}
|
||||
body={body}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -285,12 +285,12 @@ describe("backup execution - validation failures", () => {
|
|||
repositoryId: repository.id,
|
||||
preBackupWebhook: {
|
||||
url: "http://localhost:8080/stop",
|
||||
headers: { authorization: "Bearer stop-token" },
|
||||
headers: ["authorization: Bearer stop-token"],
|
||||
body: '{"action":"stop"}',
|
||||
},
|
||||
postBackupWebhook: {
|
||||
url: "http://localhost:8080/start",
|
||||
headers: { authorization: "Bearer start-token" },
|
||||
headers: ["authorization: Bearer start-token"],
|
||||
body: '{"action":"start"}',
|
||||
},
|
||||
});
|
||||
|
|
@ -304,12 +304,12 @@ describe("backup execution - validation failures", () => {
|
|||
webhooks: {
|
||||
pre: {
|
||||
url: "http://localhost:8080/stop",
|
||||
headers: { authorization: "Bearer stop-token" },
|
||||
headers: ["authorization: Bearer stop-token"],
|
||||
body: '{"action":"stop"}',
|
||||
},
|
||||
post: {
|
||||
url: "http://localhost:8080/start",
|
||||
headers: { authorization: "Bearer start-token" },
|
||||
headers: ["authorization: Bearer start-token"],
|
||||
body: '{"action":"start"}',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -416,12 +416,12 @@ describe("schedule webhooks", () => {
|
|||
cronExpression: "0 0 * * *",
|
||||
preBackupWebhook: {
|
||||
url: "http://localhost:8080/stop",
|
||||
headers: { authorization: "Bearer stop-token" },
|
||||
headers: ["authorization: Bearer stop-token"],
|
||||
body: '{"action":"stop"}',
|
||||
},
|
||||
postBackupWebhook: {
|
||||
url: "http://localhost:8080/start",
|
||||
headers: { authorization: "Bearer start-token" },
|
||||
headers: ["authorization: Bearer start-token"],
|
||||
body: '{"action":"start"}',
|
||||
},
|
||||
retryDelay: 15 * 60 * 1000,
|
||||
|
|
@ -430,12 +430,12 @@ describe("schedule webhooks", () => {
|
|||
|
||||
expect(schedule.preBackupWebhook).toEqual({
|
||||
url: "http://localhost:8080/stop",
|
||||
headers: { authorization: "Bearer stop-token" },
|
||||
headers: ["authorization: Bearer stop-token"],
|
||||
body: '{"action":"stop"}',
|
||||
});
|
||||
expect(schedule.postBackupWebhook).toEqual({
|
||||
url: "http://localhost:8080/start",
|
||||
headers: { authorization: "Bearer start-token" },
|
||||
headers: ["authorization: Bearer start-token"],
|
||||
body: '{"action":"start"}',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import { Hono } from "hono";
|
|||
import { validator } from "hono-openapi";
|
||||
import {
|
||||
createBackupScheduleBody,
|
||||
createBackupScheduleResponse,
|
||||
createBackupScheduleDto,
|
||||
deleteBackupScheduleDto,
|
||||
getBackupScheduleDto,
|
||||
getBackupScheduleResponse,
|
||||
getBackupScheduleForVolumeDto,
|
||||
getBackupScheduleForVolumeResponse,
|
||||
listBackupSchedulesDto,
|
||||
listBackupSchedulesResponse,
|
||||
runBackupNowDto,
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
stopBackupDto,
|
||||
updateBackupScheduleDto,
|
||||
updateBackupScheduleBody,
|
||||
updateBackupScheduleResponse,
|
||||
getScheduleMirrorsDto,
|
||||
updateScheduleMirrorsDto,
|
||||
updateScheduleMirrorsBody,
|
||||
|
|
@ -73,20 +76,20 @@ export const backupScheduleController = new Hono()
|
|||
const volumeShortId = asShortId(c.req.param("volumeShortId"));
|
||||
const schedule = await backupsService.getScheduleForVolume(volumeShortId);
|
||||
|
||||
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
|
||||
return c.json<GetBackupScheduleForVolumeResponseDto>(getBackupScheduleForVolumeResponse.parse(schedule), 200);
|
||||
})
|
||||
.post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
const schedule = await backupsService.createSchedule(body);
|
||||
|
||||
return c.json<CreateBackupScheduleDto>(schedule, 201);
|
||||
return c.json<CreateBackupScheduleDto>(createBackupScheduleResponse.parse(schedule), 201);
|
||||
})
|
||||
.patch("/:shortId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
|
||||
const shortId = asShortId(c.req.param("shortId"));
|
||||
const body = c.req.valid("json");
|
||||
const schedule = await backupsService.updateSchedule(shortId, body);
|
||||
|
||||
return c.json<UpdateBackupScheduleDto>(schedule, 200);
|
||||
return c.json<UpdateBackupScheduleDto>(updateBackupScheduleResponse.parse(schedule), 200);
|
||||
})
|
||||
.delete("/:shortId", deleteBackupScheduleDto, async (c) => {
|
||||
const shortId = asShortId(c.req.param("shortId"));
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export const getBackupScheduleDto = describeRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const getBackupScheduleForVolumeResponse = backupScheduleSchema.nullable();
|
||||
export const getBackupScheduleForVolumeResponse = backupScheduleSchema.nullable();
|
||||
|
||||
export type GetBackupScheduleForVolumeResponseDto = z.infer<typeof getBackupScheduleForVolumeResponse>;
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ export const createBackupScheduleBody = z.object({
|
|||
|
||||
export type CreateBackupScheduleBody = z.infer<typeof createBackupScheduleBody>;
|
||||
|
||||
const createBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true });
|
||||
export const createBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true });
|
||||
|
||||
export type CreateBackupScheduleDto = z.infer<typeof createBackupScheduleResponse>;
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ export const updateBackupScheduleBody = z.object({
|
|||
|
||||
export type UpdateBackupScheduleBody = z.infer<typeof updateBackupScheduleBody>;
|
||||
|
||||
const updateBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true });
|
||||
export const updateBackupScheduleResponse = backupScheduleSchema.omit({ volume: true, repository: true });
|
||||
|
||||
export type UpdateBackupScheduleDto = z.infer<typeof updateBackupScheduleResponse>;
|
||||
|
||||
|
|
|
|||
|
|
@ -140,12 +140,12 @@ test("sends configured webhook headers and body without changing them", async ()
|
|||
webhooks: {
|
||||
pre: {
|
||||
url: "http://localhost:8080/pre",
|
||||
headers: { authorization: "Bearer pre-token", "content-type": "application/json" },
|
||||
headers: ["authorization: Bearer pre-token", "content-type: application/json"],
|
||||
body: '{"action":"stop"}',
|
||||
},
|
||||
post: {
|
||||
url: "http://localhost:8080/post",
|
||||
headers: { authorization: "Bearer post-token" },
|
||||
headers: ["authorization: Bearer post-token"],
|
||||
body: "start-container",
|
||||
},
|
||||
},
|
||||
|
|
@ -164,7 +164,10 @@ test("sends configured webhook headers and body without changing them", async ()
|
|||
|
||||
test("fails without running restic when the pre-backup webhook fails", async () => {
|
||||
const backupMock = vi.fn();
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response("stop failed", { status: 500 })));
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response("stop failed", { status: 500 })),
|
||||
);
|
||||
vi.spyOn(resticServer, "createRestic").mockReturnValue(
|
||||
fromPartial({
|
||||
backup: backupMock,
|
||||
|
|
@ -189,7 +192,10 @@ test("fails without running restic when the pre-backup webhook fails", async ()
|
|||
});
|
||||
|
||||
test("reports a post-backup webhook failure as completed warning details", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response("start failed", { status: 500 })));
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response("start failed", { status: 500 })),
|
||||
);
|
||||
vi.spyOn(resticServer, "createRestic").mockReturnValue(
|
||||
fromPartial({
|
||||
backup: () => Effect.succeed({ exitCode: 0, result: null, warningDetails: null }),
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ test("sends configured webhook headers and body without replacing them", async (
|
|||
pre: null,
|
||||
post: {
|
||||
url: "http://localhost:8080/post",
|
||||
headers: { authorization: "Bearer post-token" },
|
||||
headers: ["authorization: Bearer post-token"],
|
||||
body: "start-container",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ export const DEFAULT_BACKUP_WEBHOOK_TIMEOUT_MS = 60_000;
|
|||
|
||||
export const backupWebhookConfigSchema = z.object({
|
||||
url: z.url(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
headers: z
|
||||
.array(z.string())
|
||||
.catch(() => [])
|
||||
.optional(),
|
||||
body: z.string().optional(),
|
||||
});
|
||||
|
||||
|
|
@ -138,8 +141,12 @@ const createRequestInit = (config: BackupWebhookConfig, context: BackupWebhookCo
|
|||
headers.set("content-type", "application/json");
|
||||
}
|
||||
|
||||
for (const [name, value] of Object.entries(config.headers ?? {})) {
|
||||
headers.set(name, value);
|
||||
for (const header of config.headers ?? []) {
|
||||
const [name, ...valueParts] = header.split(":");
|
||||
|
||||
if (name && valueParts.length > 0) {
|
||||
headers.set(name.trim(), valueParts.join(":").trim());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue