feat: added additional validation, moved field location, made skip tls and ca certificate mutually exclusive with tooltips
This commit is contained in:
parent
07e68b9fe7
commit
e78af76beb
3 changed files with 190 additions and 38 deletions
|
|
@ -72,6 +72,7 @@ export const CreateRepositoryForm = ({
|
||||||
const form = useForm<RepositoryFormValues>({
|
const form = useForm<RepositoryFormValues>({
|
||||||
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
|
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
|
||||||
defaultValues: initialValues,
|
defaultValues: initialValues,
|
||||||
|
mode: "onTouched",
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDefaultValues: true,
|
keepDefaultValues: true,
|
||||||
keepDirtyValues: false,
|
keepDirtyValues: false,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState, useRef } from "react";
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
|
|
@ -11,6 +12,7 @@ import { Input } from "../../../../components/ui/input";
|
||||||
import { SecretInput } from "../../../../components/ui/secret-input";
|
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||||
import { Textarea } from "../../../../components/ui/textarea";
|
import { Textarea } from "../../../../components/ui/textarea";
|
||||||
import { Checkbox } from "../../../../components/ui/checkbox";
|
import { Checkbox } from "../../../../components/ui/checkbox";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip";
|
||||||
import type { RepositoryFormValues } from "../create-repository-form";
|
import type { RepositoryFormValues } from "../create-repository-form";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -18,6 +20,13 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RestRepositoryForm = ({ form }: Props) => {
|
export const RestRepositoryForm = ({ form }: Props) => {
|
||||||
|
const insecureTls = form.watch("insecureTls");
|
||||||
|
const cacert = form.watch("cacert");
|
||||||
|
const [showCertTooltip, setShowCertTooltip] = useState(false);
|
||||||
|
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
|
||||||
|
const tooltipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const tooltipHideTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -34,6 +43,145 @@ export const RestRepositoryForm = ({ form }: Props) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="insecureTls"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||||
|
<FormControl>
|
||||||
|
<Tooltip delayDuration={500}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value ?? false}
|
||||||
|
disabled={!!cacert}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked);
|
||||||
|
if (checked) {
|
||||||
|
form.setValue("cacert", "");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{cacert && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="max-w-xs">
|
||||||
|
This option is disabled because a CA certificate is provided. Remove the CA certificate to skip
|
||||||
|
TLS validation instead.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Skip TLS Certificate Verification</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Disable TLS certificate verification if rest server is https and uses a self-signed certificate. This is
|
||||||
|
insecure and should only be used for testing.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cacert"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>CA Certificate (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
|
rows={6}
|
||||||
|
disabled={insecureTls}
|
||||||
|
{...field}
|
||||||
|
value={insecureTls ? "" : field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e);
|
||||||
|
if (e.target.value) {
|
||||||
|
form.setValue("insecureTls", false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerEnter={(e) => {
|
||||||
|
if (insecureTls && !showCertTooltip) {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
setTooltipPos({
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (tooltipTimeoutRef.current) {
|
||||||
|
clearTimeout(tooltipTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout to show tooltip after 500ms
|
||||||
|
tooltipTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowCertTooltip(true);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerMove={(e) => {
|
||||||
|
if (insecureTls && !showCertTooltip) {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
setTooltipPos({
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerLeave={() => {
|
||||||
|
// Clear show timeout if still waiting
|
||||||
|
if (tooltipTimeoutRef.current) {
|
||||||
|
clearTimeout(tooltipTimeoutRef.current);
|
||||||
|
tooltipTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing hide timeout
|
||||||
|
if (tooltipHideTimeoutRef.current) {
|
||||||
|
clearTimeout(tooltipHideTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout to hide tooltip after 500ms
|
||||||
|
tooltipHideTimeoutRef.current = setTimeout(() => {
|
||||||
|
setShowCertTooltip(false);
|
||||||
|
}, 500);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{insecureTls && showCertTooltip && (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute z-50 rounded-md bg-foreground px-3 py-1.5 text-xs text-background shadow-md"
|
||||||
|
style={{
|
||||||
|
left: `${tooltipPos.x}px`,
|
||||||
|
top: `${tooltipPos.y - 40}px`,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="max-w-xs text-balance">
|
||||||
|
CA certificate is disabled because TLS validation is being skipped. Uncheck "Skip TLS Certificate
|
||||||
|
Verification" to provide a custom CA certificate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom CA certificate for self-signed certificates (PEM format).{" "}
|
||||||
|
<a
|
||||||
|
href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#rest-server"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="path"
|
name="path"
|
||||||
|
|
@ -76,43 +224,6 @@ export const RestRepositoryForm = ({ form }: Props) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cacert"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>CA Certificate (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
|
||||||
rows={6}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Custom CA certificate for self-signed certificates (PEM format). https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#rest-server
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="insecureTls"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox checked={field.value ?? false} onCheckedChange={field.onChange} />
|
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Skip TLS Certificate Verification</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Disable TLS certificate verification. This is insecure and should only be used for testing. To trust a self-signed certificate without skipping verification, paste the CA certificate in the CA Certificate field.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,53 @@ export const rcloneRepositoryConfigSchema = type({
|
||||||
path: "string",
|
path: "string",
|
||||||
}).and(baseRepositoryConfigSchema);
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
|
const pemCertificateType = type("string").narrow((value, ctx) => {
|
||||||
|
if (!value || value.trim() === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
|
||||||
|
// Check for BEGIN and END markers
|
||||||
|
if (!trimmed.includes("-----BEGIN CERTIFICATE-----") || !trimmed.includes("-----END CERTIFICATE-----")) {
|
||||||
|
return ctx.error("Certificate must be in PEM format with BEGIN and END markers");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract content between markers
|
||||||
|
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
||||||
|
const endMarker = "-----END CERTIFICATE-----";
|
||||||
|
const beginIndex = trimmed.indexOf(beginMarker);
|
||||||
|
const endIndex = trimmed.indexOf(endMarker);
|
||||||
|
|
||||||
|
if (beginIndex === -1 || endIndex === -1 || endIndex <= beginIndex) {
|
||||||
|
return ctx.error("Invalid PEM certificate structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract base64 content
|
||||||
|
const base64Content = trimmed.substring(beginIndex + beginMarker.length, endIndex).replace(/\s/g, "");
|
||||||
|
|
||||||
|
// Validate base64 format
|
||||||
|
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
||||||
|
if (!base64Regex.test(base64Content)) {
|
||||||
|
return ctx.error("Certificate contains invalid base64 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base64Content.length === 0) {
|
||||||
|
return ctx.error("Certificate content is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const optionalPemCertificate = pemCertificateType.optional();
|
||||||
|
|
||||||
export const restRepositoryConfigSchema = type({
|
export const restRepositoryConfigSchema = type({
|
||||||
backend: "'rest'",
|
backend: "'rest'",
|
||||||
url: "string",
|
url: "string",
|
||||||
username: "string?",
|
username: "string?",
|
||||||
password: "string?",
|
password: "string?",
|
||||||
path: "string?",
|
path: "string?",
|
||||||
cacert: "string?",
|
cacert: optionalPemCertificate as any,
|
||||||
insecureTls: "boolean?",
|
insecureTls: "boolean?",
|
||||||
}).and(baseRepositoryConfigSchema);
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue