Milo Schwartz 6 месяцев назад
Родитель
Сommit
20f659db89

+ 1 - 1
config/config.example.yml

@@ -41,4 +41,4 @@ flags:
     require_email_verification: false
     disable_signup_without_invite: true
     disable_user_create_org: true
-    allow_raw_resources: true
+    allow_raw_resources: true

+ 1 - 1
config/traefik/traefik_config.example.yml

@@ -13,7 +13,7 @@ experimental:
   plugins:
     badger:
       moduleName: "github.com/fosrl/badger"
-      version: "v1.0.0-beta.2"
+      version: "v1.0.0-beta.3"
 
 log:
   level: "INFO"

+ 30 - 23
server/routers/resource/createResource.ts

@@ -17,6 +17,7 @@ import { eq, and } from "drizzle-orm";
 import stoi from "@server/lib/stoi";
 import { fromError } from "zod-validation-error";
 import logger from "@server/logger";
+import { subdomainSchema } from "@server/schemas/subdomainSchema";
 
 const createResourceParamsSchema = z
     .object({
@@ -27,36 +28,43 @@ const createResourceParamsSchema = z
 
 const createResourceSchema = z
     .object({
-        subdomain: z
-            .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(),
+        subdomain: z.string().optional(),
         name: z.string().min(1).max(255),
+        siteId: z.number(),
         http: z.boolean(),
         protocol: z.string(),
-        proxyPort: z.number().int().min(1).max(65535).optional(),
-    }).refine(
+        proxyPort: z.number().optional()
+    })
+    .refine(
         (data) => {
-            if (data.http === true) {
-                return true;
+            if (!data.http) {
+                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"]
         }
+    )
+    .refine(
+        (data) => {
+            if (data.http) {
+                return subdomainSchema.safeParse(data.subdomain).success;
+            }
+            return true;
+        },
+        {
+            message: "Invalid subdomain",
+            path: ["subdomain"]
+        }
     );
-    
+
 export type CreateResourceResponse = Resource;
 
 export async function createResource(
@@ -134,7 +142,6 @@ export async function createResource(
                 );
             }
         } else {
-
             if (proxyPort === 443 || proxyPort === 80) {
                 return next(
                     createHttpError(
@@ -149,7 +156,7 @@ export async function createResource(
                 .select()
                 .from(resources)
                 .where(eq(resources.fullDomain, fullDomain));
-            
+
             if (existingResource.length > 0) {
                 return next(
                     createHttpError(
@@ -165,7 +172,7 @@ export async function createResource(
                 .insert(resources)
                 .values({
                     siteId,
-                    fullDomain: http? fullDomain : null,
+                    fullDomain: http ? fullDomain : null,
                     orgId,
                     name,
                     subdomain,

+ 3 - 1
server/routers/traefik/getTraefikConfig.ts

@@ -156,13 +156,15 @@ export async function traefikConfigProvider(
                         : {})
                 };
 
+                const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || [];
+
                 config_output.http.routers![routerName] = {
                     entryPoints: [
                         resource.ssl
                             ? config.getRawConfig().traefik.https_entrypoint
                             : config.getRawConfig().traefik.http_entrypoint
                     ],
-                    middlewares: [badgerMiddlewareName],
+                    middlewares: [badgerMiddlewareName, ...additionalMiddlewares],
                     service: serviceName,
                     rule: `Host(\`${fullDomain}\`)`,
                     ...(resource.ssl ? { tls } : {})

+ 20 - 19
src/app/[orgId]/settings/resources/CreateResourceForm.tsx

@@ -59,38 +59,39 @@ import {
     SelectTrigger,
     SelectValue
 } from "@app/components/ui/select";
+import { subdomainSchema } from "@server/schemas/subdomainSchema";
 
 const createResourceFormSchema = z
     .object({
-        subdomain: z
-            .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(),
+        subdomain: z.string().optional(),
         name: z.string().min(1).max(255),
         siteId: z.number(),
         http: z.boolean(),
         protocol: z.string(),
-        proxyPort: z.number().int().min(1).max(65535).optional()
+        proxyPort: z.number().optional(),
     })
     .refine(
         (data) => {
-            if (data.http === true) {
-                return true;
+            if (!data.http) {
+                return z.number().int().min(1).max(65535).safeParse(data.proxyPort).success;
+            }
+            return true;
+        },
+        {
+            message: "Invalid port number",
+            path: ["proxyPort"],
+        }
+    )
+    .refine(
+        (data) => {
+            if (data.http) {
+                return subdomainSchema.safeParse(data.subdomain).success;
             }
-            return !!data.proxyPort;
+            return true;
         },
         {
-            message: "Port number is required for non-HTTP resources",
-            path: ["proxyPort"]
+            message: "Invalid subdomain",
+            path: ["subdomain"],
         }
     );
 

+ 64 - 59
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -63,6 +63,7 @@ import {
 } from "@app/components/Settings";
 import { SwitchInput } from "@app/components/SwitchInput";
 import { useSiteContext } from "@app/hooks/useSiteContext";
+import { InfoPopup } from "@app/components/ui/info-popup";
 
 // Regular expressions for validation
 const DOMAIN_REGEX =
@@ -458,28 +459,28 @@ export default function ReverseProxyTargets(props: {
     return (
         <SettingsContainer>
             {resource.http && (
-            <SettingsSection>
-                <SettingsSectionHeader>
-                    <SettingsSectionTitle>
-                        SSL Configuration
-                    </SettingsSectionTitle>
-                    <SettingsSectionDescription>
-                        Setup SSL to secure your connections with LetsEncrypt
-                        certificates
-                    </SettingsSectionDescription>
-                </SettingsSectionHeader>
-                <SettingsSectionBody>
-                    <SwitchInput
-                        id="ssl-toggle"
-                        label="Enable SSL (https)"
-                        defaultChecked={resource.ssl}
-                        onCheckedChange={async (val) => {
-                            await saveSsl(val);
-                        }}
-                    />
-                </SettingsSectionBody>
-            </SettingsSection>
-)}
+                <SettingsSection>
+                    <SettingsSectionHeader>
+                        <SettingsSectionTitle>
+                            SSL Configuration
+                        </SettingsSectionTitle>
+                        <SettingsSectionDescription>
+                            Setup SSL to secure your connections with
+                            LetsEncrypt certificates
+                        </SettingsSectionDescription>
+                    </SettingsSectionHeader>
+                    <SettingsSectionBody>
+                        <SwitchInput
+                            id="ssl-toggle"
+                            label="Enable SSL (https)"
+                            defaultChecked={resource.ssl}
+                            onCheckedChange={async (val) => {
+                                await saveSsl(val);
+                            }}
+                        />
+                    </SettingsSectionBody>
+                </SettingsSection>
+            )}
             {/* Targets Section */}
             <SettingsSection>
                 <SettingsSectionHeader>
@@ -498,41 +499,45 @@ export default function ReverseProxyTargets(props: {
                         >
                             <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
                                 {resource.http && (
-
-                                <FormField
-                                    control={addTargetForm.control}
-                                    name="method"
-                                    render={({ field }) => (
-                                        <FormItem>
-                                            <FormLabel>Method</FormLabel>
-                                            <FormControl>
-                                                <Select
-                                                    value={field.value || undefined}    
-                                                    onValueChange={(value) => {
-                                                        addTargetForm.setValue(
-                                                            "method",
+                                    <FormField
+                                        control={addTargetForm.control}
+                                        name="method"
+                                        render={({ field }) => (
+                                            <FormItem>
+                                                <FormLabel>Method</FormLabel>
+                                                <FormControl>
+                                                    <Select
+                                                        value={
+                                                            field.value ||
+                                                            undefined
+                                                        }
+                                                        onValueChange={(
                                                             value
-                                                        );
-                                                    }}
-                                                >
-                                                    <SelectTrigger id="method">
-                                                        <SelectValue placeholder="Select method" />
-                                                    </SelectTrigger>
-                                                    <SelectContent>
-                                                        <SelectItem value="http">
-                                                            http
-                                                        </SelectItem>
-                                                        <SelectItem value="https">
-                                                            https
-                                                        </SelectItem>
-                                                    </SelectContent>
-                                                </Select>
-                                            </FormControl>
-                                            <FormMessage />
-                                        </FormItem>
-                                    )}
-                                />
-                            )}
+                                                        ) => {
+                                                            addTargetForm.setValue(
+                                                                "method",
+                                                                value
+                                                            );
+                                                        }}
+                                                    >
+                                                        <SelectTrigger id="method">
+                                                            <SelectValue placeholder="Select method" />
+                                                        </SelectTrigger>
+                                                        <SelectContent>
+                                                            <SelectItem value="http">
+                                                                http
+                                                            </SelectItem>
+                                                            <SelectItem value="https">
+                                                                https
+                                                            </SelectItem>
+                                                        </SelectContent>
+                                                    </Select>
+                                                </FormControl>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                )}
 
                                 <FormField
                                     control={addTargetForm.control}
@@ -647,9 +652,9 @@ export default function ReverseProxyTargets(props: {
                             </TableBody>
                         </Table>
                     </TableContainer>
-                    <SettingsSectionDescription>
-                        Multiple targets will get load balanced by Traefik. You can use this for high availability.
-                    </SettingsSectionDescription>
+                    <p className="text-sm text-muted-foreground">
+                        Adding more than one target above will enable load balancing.
+                    </p>
                 </SettingsSectionBody>
                 <SettingsSectionFooter>
                     <Button

+ 39 - 18
src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx

@@ -36,24 +36,44 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
 import CustomDomainInput from "../CustomDomainInput";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
+import { subdomainSchema } from "@server/schemas/subdomainSchema";
 
-const GeneralFormSchema = z.object({
-    subdomain: z
-        .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),
-    proxyPort: z.number().int().min(1).max(65535).optional()
-});
+const GeneralFormSchema = z
+    .object({
+        subdomain: z.string().optional(),
+        name: z.string().min(1).max(255),
+        proxyPort: z.number().optional(),
+        http: z.boolean()
+    })
+    .refine(
+        (data) => {
+            if (!data.http) {
+                return z
+                    .number()
+                    .int()
+                    .min(1)
+                    .max(65535)
+                    .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>;
 
@@ -77,7 +97,8 @@ export default function GeneralForm() {
         defaultValues: {
             name: resource.name,
             subdomain: resource.subdomain ? resource.subdomain : undefined,
-            proxyPort: resource.proxyPort ? resource.proxyPort : undefined
+            proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
+            http: resource.http
         },
         mode: "onChange"
     });