mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #267 from lorenzomigliorero/feat/condition-certificate
feat: condition domain certificate field
This commit is contained in:
@@ -28,84 +28,103 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { useEffect, useState } from "react";
|
||||||
import { PlusIcon } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
// const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
|
import { domain } from "@/server/db/validations";
|
||||||
// .regex(hostnameRegex
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
const addDomain = z.object({
|
import type z from "zod";
|
||||||
host: z.string().min(1, "Hostname is required"),
|
|
||||||
path: z.string().min(1),
|
|
||||||
port: z.number(),
|
|
||||||
https: z.boolean(),
|
|
||||||
certificateType: z.enum(["letsencrypt", "none"]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddDomain = z.infer<typeof addDomain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
children?: React.ReactNode;
|
domainId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDomain = ({
|
export const AddDomain = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
domainId = "",
|
||||||
|
children,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
const { mutateAsync, isError, error } = api.domain.create.useMutation();
|
{
|
||||||
|
domainId,
|
||||||
const form = useForm<AddDomain>({
|
|
||||||
defaultValues: {
|
|
||||||
host: "",
|
|
||||||
https: false,
|
|
||||||
path: "/",
|
|
||||||
port: 3000,
|
|
||||||
certificateType: "none",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addDomain),
|
{
|
||||||
|
enabled: !!domainId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
|
? api.domain.update.useMutation()
|
||||||
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<Domain>({
|
||||||
|
resolver: zodResolver(domain),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
if (data) {
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
path: data?.path || undefined,
|
||||||
|
port: data?.port || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const onSubmit = async (data: AddDomain) => {
|
if (!domainId) {
|
||||||
|
form.reset({});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data, isLoading]);
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
|
error: domainId
|
||||||
|
? "Error to update the domain"
|
||||||
|
: "Error to create the domain",
|
||||||
|
submit: domainId ? "Update" : "Create",
|
||||||
|
dialogDescription: domainId
|
||||||
|
? "In this section you can edit a domain"
|
||||||
|
: "In this section you can add domains",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Domain) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
applicationId,
|
applicationId,
|
||||||
host: data.host,
|
...data,
|
||||||
https: data.https,
|
|
||||||
path: data.path,
|
|
||||||
port: data.port,
|
|
||||||
certificateType: data.certificateType,
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Domain Created");
|
toast.success(dictionary.success);
|
||||||
await utils.domain.byApplicationId.invalidate({
|
await utils.domain.byApplicationId.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
||||||
|
|
||||||
|
if (domainId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the domain");
|
toast.error(dictionary.error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button>{children}</Button>
|
{children}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Domain</DialogTitle>
|
<DialogTitle>Domain</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
In this section you can add custom domains
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
@@ -169,33 +188,36 @@ export const AddDomain = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{form.getValues().https && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="certificateType"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="certificateType"
|
||||||
<FormItem className="col-span-2">
|
render={({ field }) => (
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormItem className="col-span-2">
|
||||||
<Select
|
<FormLabel>Certificate</FormLabel>
|
||||||
onValueChange={field.onChange}
|
<Select
|
||||||
defaultValue={field.value || ""}
|
onValueChange={field.onChange}
|
||||||
>
|
defaultValue={field.value || ""}
|
||||||
<FormControl>
|
>
|
||||||
<SelectTrigger>
|
<FormControl>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select a certificate" />
|
||||||
</FormControl>
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Letsencrypt (Default)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">None</SelectItem>
|
|
||||||
<SelectItem value={"letsencrypt"}>
|
|
||||||
Letsencrypt (Default)
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="https"
|
name="https"
|
||||||
@@ -206,6 +228,7 @@ export const AddDomain = ({
|
|||||||
<FormDescription>
|
<FormDescription>
|
||||||
Automatically provision SSL Certificate.
|
Automatically provision SSL Certificate.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -226,7 +249,7 @@ export const AddDomain = ({
|
|||||||
form="hook-form"
|
form="hook-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{dictionary.submit}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react";
|
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
import { DeleteDomain } from "./delete-domain";
|
import { DeleteDomain } from "./delete-domain";
|
||||||
import { GenerateDomain } from "./generate-domain";
|
import { GenerateDomain } from "./generate-domain";
|
||||||
import { UpdateDomain } from "./update-domain";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -43,7 +41,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain applicationId={applicationId}>
|
<AddDomain applicationId={applicationId}>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<Button>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</Button>
|
||||||
</AddDomain>
|
</AddDomain>
|
||||||
)}
|
)}
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
@@ -61,7 +61,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<AddDomain applicationId={applicationId}>
|
<AddDomain applicationId={applicationId}>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<Button>
|
||||||
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
|
</Button>
|
||||||
</AddDomain>
|
</AddDomain>
|
||||||
|
|
||||||
<GenerateDomain applicationId={applicationId} />
|
<GenerateDomain applicationId={applicationId} />
|
||||||
@@ -90,7 +92,14 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
{item.https ? "HTTPS" : "HTTP"}
|
{item.https ? "HTTPS" : "HTTP"}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateDomain domainId={item.domainId} />
|
<AddDomain
|
||||||
|
applicationId={applicationId}
|
||||||
|
domainId={item.domainId}
|
||||||
|
>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</AddDomain>
|
||||||
<DeleteDomain domainId={item.domainId} />
|
<DeleteDomain domainId={item.domainId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
|
|
||||||
|
|
||||||
const updateDomain = z.object({
|
|
||||||
host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }),
|
|
||||||
path: z.string().min(1),
|
|
||||||
port: z
|
|
||||||
.number()
|
|
||||||
.min(1, { message: "Port must be at least 1" })
|
|
||||||
.max(65535, { message: "Port must be 65535 or below" }),
|
|
||||||
https: z.boolean(),
|
|
||||||
certificateType: z.enum(["letsencrypt", "none"]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateDomain = z.infer<typeof updateDomain>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
domainId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdateDomain = ({ domainId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { data, refetch } = api.domain.one.useQuery(
|
|
||||||
{
|
|
||||||
domainId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!domainId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { mutateAsync, isError, error } = api.domain.update.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdateDomain>({
|
|
||||||
defaultValues: {
|
|
||||||
host: "",
|
|
||||||
https: true,
|
|
||||||
path: "/",
|
|
||||||
port: 3000,
|
|
||||||
certificateType: "none",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(updateDomain),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
host: data.host || "",
|
|
||||||
port: data.port || 3000,
|
|
||||||
path: data.path || "/",
|
|
||||||
https: data.https,
|
|
||||||
certificateType: data.certificateType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateDomain) => {
|
|
||||||
await mutateAsync({
|
|
||||||
domainId,
|
|
||||||
host: data.host,
|
|
||||||
https: data.https,
|
|
||||||
path: data.path,
|
|
||||||
port: data.port,
|
|
||||||
certificateType: data.certificateType,
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
toast.success("Domain Updated");
|
|
||||||
await refetch();
|
|
||||||
await utils.domain.byApplicationId.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
await utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to update the domain");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger className="" asChild>
|
|
||||||
<Button variant="ghost">
|
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Domain</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
In this section you can add custom domains
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-8 "
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="host"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Host</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="api.dokploy.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="path"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Path</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"/"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="port"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Container Port</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"3000"}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(Number.parseInt(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certificateType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="col-span-2">
|
|
||||||
<FormLabel>Certificate</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a certificate" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={"none"}>None</SelectItem>
|
|
||||||
<SelectItem value={"letsencrypt"}>
|
|
||||||
Letsencrypt
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { domain } from "@/server/db/validations";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
|
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
|
||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { certificateType } from "./shared";
|
import { certificateType } from "./shared";
|
||||||
|
|
||||||
@@ -31,27 +31,17 @@ export const domainsRelations = relations(domains, ({ one }) => ({
|
|||||||
references: [applications.applicationId],
|
references: [applications.applicationId],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
|
|
||||||
const createSchema = createInsertSchema(domains, {
|
|
||||||
domainId: z.string().min(1),
|
|
||||||
host: z.string().min(1),
|
|
||||||
path: z.string().min(1),
|
|
||||||
port: z.number(),
|
|
||||||
https: z.boolean(),
|
|
||||||
applicationId: z.string(),
|
|
||||||
certificateType: z.enum(["letsencrypt", "none"]),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiCreateDomain = createSchema
|
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
||||||
.pick({
|
|
||||||
host: true,
|
export const apiCreateDomain = createSchema.pick({
|
||||||
path: true,
|
host: true,
|
||||||
port: true,
|
path: true,
|
||||||
https: true,
|
port: true,
|
||||||
applicationId: true,
|
https: true,
|
||||||
certificateType: true,
|
applicationId: true,
|
||||||
})
|
certificateType: true,
|
||||||
.required();
|
});
|
||||||
|
|
||||||
export const apiFindDomain = createSchema
|
export const apiFindDomain = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
@@ -59,19 +49,16 @@ export const apiFindDomain = createSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
export const apiFindDomainByApplication = createSchema
|
export const apiFindDomainByApplication = createSchema.pick({
|
||||||
.pick({
|
applicationId: true,
|
||||||
applicationId: true,
|
});
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export const apiUpdateDomain = createSchema
|
export const apiUpdateDomain = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
domainId: true,
|
|
||||||
host: true,
|
host: true,
|
||||||
path: true,
|
path: true,
|
||||||
port: true,
|
port: true,
|
||||||
https: true,
|
https: true,
|
||||||
certificateType: true,
|
certificateType: true,
|
||||||
})
|
})
|
||||||
.required();
|
.merge(createSchema.pick({ domainId: true }).required());
|
||||||
|
|||||||
25
server/db/validations/index.ts
Normal file
25
server/db/validations/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const domain = z
|
||||||
|
.object({
|
||||||
|
host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, {
|
||||||
|
message: "Invalid hostname",
|
||||||
|
}),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none"]).optional(),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user