mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-31 08:04:54 +02:00
fix zod schemas
This commit is contained in:
parent
0e04e82b88
commit
20f659db89
7 changed files with 158 additions and 122 deletions
|
@ -41,4 +41,4 @@ flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
|
|
|
@ -13,7 +13,7 @@ experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
moduleName: "github.com/fosrl/badger"
|
moduleName: "github.com/fosrl/badger"
|
||||||
version: "v1.0.0-beta.2"
|
version: "v1.0.0-beta.3"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "INFO"
|
level: "INFO"
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { eq, and } from "drizzle-orm";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||||
|
|
||||||
const createResourceParamsSchema = z
|
const createResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -27,36 +28,43 @@ const createResourceParamsSchema = z
|
||||||
|
|
||||||
const createResourceSchema = z
|
const createResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
subdomain: z
|
subdomain: z.string().optional(),
|
||||||
.union([
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.regex(
|
|
||||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
|
||||||
"Invalid subdomain format"
|
|
||||||
)
|
|
||||||
.min(1, "Subdomain must be at least 1 character long")
|
|
||||||
.transform((val) => val.toLowerCase()),
|
|
||||||
z.string().optional()
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
proxyPort: z.number().optional()
|
||||||
}).refine(
|
})
|
||||||
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.http === true) {
|
if (!data.http) {
|
||||||
return true;
|
return z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(65535)
|
||||||
|
.safeParse(data.proxyPort).success;
|
||||||
}
|
}
|
||||||
return !!data.proxyPort;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Port number is required for non-HTTP resources",
|
message: "Invalid port number",
|
||||||
path: ["proxyPort"]
|
path: ["proxyPort"]
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.http) {
|
||||||
|
return subdomainSchema.safeParse(data.subdomain).success;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid subdomain",
|
||||||
|
path: ["subdomain"]
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CreateResourceResponse = Resource;
|
export type CreateResourceResponse = Resource;
|
||||||
|
|
||||||
export async function createResource(
|
export async function createResource(
|
||||||
|
@ -134,7 +142,6 @@ export async function createResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (proxyPort === 443 || proxyPort === 80) {
|
if (proxyPort === 443 || proxyPort === 80) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
@ -149,7 +156,7 @@ export async function createResource(
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(eq(resources.fullDomain, fullDomain));
|
.where(eq(resources.fullDomain, fullDomain));
|
||||||
|
|
||||||
if (existingResource.length > 0) {
|
if (existingResource.length > 0) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
@ -165,7 +172,7 @@ export async function createResource(
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
siteId,
|
siteId,
|
||||||
fullDomain: http? fullDomain : null,
|
fullDomain: http ? fullDomain : null,
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subdomain,
|
subdomain,
|
||||||
|
|
|
@ -156,13 +156,15 @@ export async function traefikConfigProvider(
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
|
||||||
config_output.http.routers![routerName] = {
|
config_output.http.routers![routerName] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
resource.ssl
|
resource.ssl
|
||||||
? config.getRawConfig().traefik.https_entrypoint
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
: config.getRawConfig().traefik.http_entrypoint
|
: config.getRawConfig().traefik.http_entrypoint
|
||||||
],
|
],
|
||||||
middlewares: [badgerMiddlewareName],
|
middlewares: [badgerMiddlewareName, ...additionalMiddlewares],
|
||||||
service: serviceName,
|
service: serviceName,
|
||||||
rule: `Host(\`${fullDomain}\`)`,
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
...(resource.ssl ? { tls } : {})
|
...(resource.ssl ? { tls } : {})
|
||||||
|
|
|
@ -59,38 +59,39 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
|
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||||
|
|
||||||
const createResourceFormSchema = z
|
const createResourceFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
subdomain: z
|
subdomain: z.string().optional(),
|
||||||
.union([
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.regex(
|
|
||||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
|
||||||
"Invalid subdomain format"
|
|
||||||
)
|
|
||||||
.min(1, "Subdomain must be at least 1 character long")
|
|
||||||
.transform((val) => val.toLowerCase()),
|
|
||||||
z.string().optional()
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
siteId: z.number(),
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional()
|
proxyPort: z.number().optional(),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.http === true) {
|
if (!data.http) {
|
||||||
return true;
|
return z.number().int().min(1).max(65535).safeParse(data.proxyPort).success;
|
||||||
}
|
}
|
||||||
return !!data.proxyPort;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Port number is required for non-HTTP resources",
|
message: "Invalid port number",
|
||||||
path: ["proxyPort"]
|
path: ["proxyPort"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.http) {
|
||||||
|
return subdomainSchema.safeParse(data.subdomain).success;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid subdomain",
|
||||||
|
path: ["subdomain"],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ import {
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||||
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
|
||||||
// Regular expressions for validation
|
// Regular expressions for validation
|
||||||
const DOMAIN_REGEX =
|
const DOMAIN_REGEX =
|
||||||
|
@ -458,28 +459,28 @@ export default function ReverseProxyTargets(props: {
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{resource.http && (
|
{resource.http && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
SSL Configuration
|
SSL Configuration
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
Setup SSL to secure your connections with LetsEncrypt
|
Setup SSL to secure your connections with
|
||||||
certificates
|
LetsEncrypt certificates
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SwitchInput
|
<SwitchInput
|
||||||
id="ssl-toggle"
|
id="ssl-toggle"
|
||||||
label="Enable SSL (https)"
|
label="Enable SSL (https)"
|
||||||
defaultChecked={resource.ssl}
|
defaultChecked={resource.ssl}
|
||||||
onCheckedChange={async (val) => {
|
onCheckedChange={async (val) => {
|
||||||
await saveSsl(val);
|
await saveSsl(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
{/* Targets Section */}
|
{/* Targets Section */}
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
@ -498,41 +499,45 @@ export default function ReverseProxyTargets(props: {
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{resource.http && (
|
{resource.http && (
|
||||||
|
<FormField
|
||||||
<FormField
|
control={addTargetForm.control}
|
||||||
control={addTargetForm.control}
|
name="method"
|
||||||
name="method"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem>
|
||||||
<FormItem>
|
<FormLabel>Method</FormLabel>
|
||||||
<FormLabel>Method</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Select
|
||||||
<Select
|
value={
|
||||||
value={field.value || undefined}
|
field.value ||
|
||||||
onValueChange={(value) => {
|
undefined
|
||||||
addTargetForm.setValue(
|
}
|
||||||
"method",
|
onValueChange={(
|
||||||
value
|
value
|
||||||
);
|
) => {
|
||||||
}}
|
addTargetForm.setValue(
|
||||||
>
|
"method",
|
||||||
<SelectTrigger id="method">
|
value
|
||||||
<SelectValue placeholder="Select method" />
|
);
|
||||||
</SelectTrigger>
|
}}
|
||||||
<SelectContent>
|
>
|
||||||
<SelectItem value="http">
|
<SelectTrigger id="method">
|
||||||
http
|
<SelectValue placeholder="Select method" />
|
||||||
</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="https">
|
<SelectContent>
|
||||||
https
|
<SelectItem value="http">
|
||||||
</SelectItem>
|
http
|
||||||
</SelectContent>
|
</SelectItem>
|
||||||
</Select>
|
<SelectItem value="https">
|
||||||
</FormControl>
|
https
|
||||||
<FormMessage />
|
</SelectItem>
|
||||||
</FormItem>
|
</SelectContent>
|
||||||
)}
|
</Select>
|
||||||
/>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={addTargetForm.control}
|
control={addTargetForm.control}
|
||||||
|
@ -647,9 +652,9 @@ export default function ReverseProxyTargets(props: {
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<SettingsSectionDescription>
|
<p className="text-sm text-muted-foreground">
|
||||||
Multiple targets will get load balanced by Traefik. You can use this for high availability.
|
Adding more than one target above will enable load balancing.
|
||||||
</SettingsSectionDescription>
|
</p>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -36,24 +36,44 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import CustomDomainInput from "../CustomDomainInput";
|
import CustomDomainInput from "../CustomDomainInput";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z
|
||||||
subdomain: z
|
.object({
|
||||||
.union([
|
subdomain: z.string().optional(),
|
||||||
z
|
name: z.string().min(1).max(255),
|
||||||
.string()
|
proxyPort: z.number().optional(),
|
||||||
.regex(
|
http: z.boolean()
|
||||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
})
|
||||||
"Invalid subdomain format"
|
.refine(
|
||||||
)
|
(data) => {
|
||||||
.min(1, "Subdomain must be at least 1 character long")
|
if (!data.http) {
|
||||||
.transform((val) => val.toLowerCase()),
|
return z
|
||||||
z.string().optional()
|
.number()
|
||||||
])
|
.int()
|
||||||
.optional(),
|
.min(1)
|
||||||
name: z.string().min(1).max(255),
|
.max(65535)
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional()
|
.safeParse(data.proxyPort).success;
|
||||||
});
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid port number",
|
||||||
|
path: ["proxyPort"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.http) {
|
||||||
|
return subdomainSchema.safeParse(data.subdomain).success;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid subdomain",
|
||||||
|
path: ["subdomain"]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
|
||||||
|
@ -77,7 +97,8 @@ export default function GeneralForm() {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||||
proxyPort: resource.proxyPort ? resource.proxyPort : undefined
|
proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
|
||||||
|
http: resource.http
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue