Переглянути джерело

all resources at the base domain closes #137

Milo Schwartz 5 місяців тому
батько
коміт
e475c1ea50

+ 2 - 1
server/db/schema.ts

@@ -53,7 +53,8 @@ export const resources = sqliteTable("resources", {
     proxyPort: integer("proxyPort"),
     emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
         .notNull()
-        .default(false)
+        .default(false),
+    isBaseDomain: integer("isBaseDomain", { mode: "boolean" })
 });
 
 export const targets = sqliteTable("targets", {

+ 9 - 5
server/lib/config.ts

@@ -10,7 +10,6 @@ import {
     configFilePath1,
     configFilePath2
 } from "@server/lib/consts";
-import { loadAppVersion } from "@server/lib/loadAppVersion";
 import { passwordSchema } from "@server/auth/passwordSchema";
 import stoi from "./stoi";
 
@@ -152,7 +151,8 @@ const configSchema = z.object({
             require_email_verification: z.boolean().optional(),
             disable_signup_without_invite: z.boolean().optional(),
             disable_user_create_org: z.boolean().optional(),
-            allow_raw_resources: z.boolean().optional()
+            allow_raw_resources: z.boolean().optional(),
+            allow_base_domain_resources: z.boolean().optional()
         })
         .optional()
 });
@@ -252,9 +252,9 @@ export class Config {
             ? "true"
             : "false";
         process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
-        ?.allow_raw_resources
-        ? "true"
-        : "false";
+            ?.allow_raw_resources
+            ? "true"
+            : "false";
         process.env.SESSION_COOKIE_NAME =
             parsedConfig.data.server.session_cookie_name;
         process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
@@ -270,6 +270,10 @@ export class Config {
             parsedConfig.data.server.resource_access_token_param;
         process.env.RESOURCE_SESSION_REQUEST_PARAM =
             parsedConfig.data.server.resource_session_request_param;
+        process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
+            ?.allow_base_domain_resources
+            ? "true"
+            : "false";
 
         this.rawConfig = parsedConfig.data;
     }

+ 27 - 6
server/routers/resource/createResource.ts

@@ -34,7 +34,8 @@ const createResourceSchema = z
         siteId: z.number(),
         http: z.boolean(),
         protocol: z.string(),
-        proxyPort: z.number().optional()
+        proxyPort: z.number().optional(),
+        isBaseDomain: z.boolean().optional()
     })
     .refine(
         (data) => {
@@ -55,7 +56,7 @@ const createResourceSchema = z
     )
     .refine(
         (data) => {
-            if (data.http) {
+            if (data.http && !data.isBaseDomain) {
                 return subdomainSchema.safeParse(data.subdomain).success;
             }
             return true;
@@ -75,7 +76,7 @@ const createResourceSchema = z
             return true;
         },
         {
-            message: "Cannot update proxyPort"
+            message: "Proxy port cannot be set"
         }
     )
     .refine(
@@ -88,6 +89,19 @@ const createResourceSchema = z
         {
             message: "Port 80 and 443 are reserved for http and https resources"
         }
+    )
+    .refine(
+        (data) => {
+            if (!config.getRawConfig().flags?.allow_base_domain_resources) {
+                if (data.isBaseDomain) {
+                    return false;
+                }
+            }
+            return true;
+        },
+        {
+            message: "Base domain resources are not allowed"
+        }
     );
 
 export type CreateResourceResponse = Resource;
@@ -108,7 +122,7 @@ export async function createResource(
             );
         }
 
-        let { name, subdomain, protocol, proxyPort, http } = parsedBody.data;
+        let { name, subdomain, protocol, proxyPort, http, isBaseDomain } = parsedBody.data;
 
         // Validate request params
         const parsedParams = createResourceParamsSchema.safeParse(req.params);
@@ -145,7 +159,13 @@ export async function createResource(
             );
         }
 
-        const fullDomain = `${subdomain}.${org[0].domain}`;
+        let fullDomain = "";
+        if (isBaseDomain) {
+            fullDomain = org[0].domain;
+        } else {
+            fullDomain = `${subdomain}.${org[0].domain}`;
+        }
+
         // if http is false check to see if there is already a resource with the same port and protocol
         if (!http) {
             const existingResource = await db
@@ -195,7 +215,8 @@ export async function createResource(
                     http,
                     protocol,
                     proxyPort,
-                    ssl: true
+                    ssl: true,
+                    isBaseDomain
                 })
                 .returning();
 

+ 58 - 4
server/routers/resource/updateResource.ts

@@ -28,7 +28,8 @@ const updateResourceBodySchema = z
         sso: z.boolean().optional(),
         blockAccess: z.boolean().optional(),
         proxyPort: z.number().int().min(1).max(65535).optional(),
-        emailWhitelistEnabled: z.boolean().optional()
+        emailWhitelistEnabled: z.boolean().optional(),
+        isBaseDomain: z.boolean().optional()
     })
     .strict()
     .refine((data) => Object.keys(data).length > 0, {
@@ -55,6 +56,19 @@ const updateResourceBodySchema = z
         {
             message: "Port 80 and 443 are reserved for http and https resources"
         }
+    )
+    .refine(
+        (data) => {
+            if (!config.getRawConfig().flags?.allow_base_domain_resources) {
+                if (data.isBaseDomain) {
+                    return false;
+                }
+            }
+            return true;
+        },
+        {
+            message: "Base domain resources are not allowed"
+        }
     );
 
 export async function updateResource(
@@ -104,6 +118,29 @@ export async function updateResource(
             );
         }
 
+        if (updateData.subdomain) {
+            if (!resource.http) {
+                return next(
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        "Cannot update subdomain for non-http resource"
+                    )
+                );
+            }
+
+            const valid = subdomainSchema.safeParse(
+                updateData.subdomain
+            ).success;
+            if (!valid) {
+                return next(
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        "Invalid subdomain provided"
+                    )
+                );
+            }
+        }
+
         if (updateData.proxyPort) {
             const proxyPort = updateData.proxyPort;
             const existingResource = await db
@@ -138,15 +175,32 @@ export async function updateResource(
             );
         }
 
-        const fullDomain = updateData.subdomain
-            ? `${updateData.subdomain}.${org.domain}`
-            : undefined;
+        let fullDomain = "";
+        if (updateData.isBaseDomain) {
+            fullDomain = org.domain;
+        } else {
+            fullDomain = `${updateData.subdomain}.${org.domain}`;
+        }
 
         const updatePayload = {
             ...updateData,
             ...(fullDomain && { fullDomain })
         };
 
+        const [existingDomain] = await db
+            .select()
+            .from(resources)
+            .where(eq(resources.fullDomain, fullDomain));
+
+        if (existingDomain && existingDomain.resourceId !== resourceId) {
+            return next(
+                createHttpError(
+                    HttpCode.CONFLICT,
+                    "Resource with that domain already exists"
+                )
+            );
+        }
+
         const updatedResource = await db
             .update(resources)
             .set(updatePayload)

+ 5 - 2
server/routers/traefik/getTraefikConfig.ts

@@ -25,6 +25,7 @@ export async function traefikConfigProvider(
                 http: resources.http,
                 proxyPort: resources.proxyPort,
                 protocol: resources.protocol,
+                isBaseDomain: resources.isBaseDomain,
                 // Site fields
                 site: {
                     siteId: sites.siteId,
@@ -110,11 +111,11 @@ export async function traefikConfigProvider(
 
             const routerName = `${resource.resourceId}-router`;
             const serviceName = `${resource.resourceId}-service`;
-            const fullDomain = `${resource.subdomain}.${org.domain}`;
+            const fullDomain = `${resource.fullDomain}`;
 
             if (resource.http) {
                 // HTTP configuration remains the same
-                if (!resource.subdomain) {
+                if (!resource.subdomain && !resource.isBaseDomain) {
                     continue;
                 }
 
@@ -148,6 +149,8 @@ export async function traefikConfigProvider(
                         : {})
                 };
 
+                logger.debug(config.getRawConfig().traefik.prefer_wildcard_cert)
+
                 const additionalMiddlewares =
                     config.getRawConfig().traefik.additional_middlewares || [];
 

+ 6 - 1
server/setup/copyInConfig.ts

@@ -23,7 +23,12 @@ export async function copyInConfig() {
         const allResources = await trx.select().from(resources);
 
         for (const resource of allResources) {
-            const fullDomain = `${resource.subdomain}.${domain}`;
+            let fullDomain = "";
+            if (resource.isBaseDomain) {
+                fullDomain = domain;
+            } else {
+                fullDomain = `${resource.subdomain}.${domain}`;
+            }
             await trx
                 .update(resources)
                 .set({ fullDomain })

+ 39 - 5
server/setup/migrations.ts

@@ -3,8 +3,9 @@ import db, { exists } from "@server/db";
 import path from "path";
 import semver from "semver";
 import { versionMigrations } from "@server/db/schema";
-import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
+import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
 import { SqliteError } from "better-sqlite3";
+import fs from "fs";
 import m1 from "./scripts/1.0.0-beta1";
 import m2 from "./scripts/1.0.0-beta2";
 import m3 from "./scripts/1.0.0-beta3";
@@ -12,6 +13,7 @@ import m4 from "./scripts/1.0.0-beta5";
 import m5 from "./scripts/1.0.0-beta6";
 import m6 from "./scripts/1.0.0-beta9";
 import m7 from "./scripts/1.0.0-beta10";
+import m8 from "./scripts/1.0.0-beta12";
 
 // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
 // EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -24,12 +26,41 @@ const migrations = [
     { version: "1.0.0-beta.5", run: m4 },
     { version: "1.0.0-beta.6", run: m5 },
     { version: "1.0.0-beta.9", run: m6 },
-    { version: "1.0.0-beta.10", run: m7 }
+    { version: "1.0.0-beta.10", run: m7 },
+    { version: "1.0.0-beta.12", run: m8 }
     // Add new migrations here as they are created
 ] as const;
 
-// Run the migrations
-await runMigrations();
+await run();
+
+async function run() {
+    // backup the database
+    backupDb();
+
+    // run the migrations
+    await runMigrations();
+}
+
+function backupDb() {
+    // make dir config/db/backups
+    const appPath = APP_PATH;
+    const dbDir = path.join(appPath, "db");
+
+    const backupsDir = path.join(dbDir, "backups");
+
+    // check if the backups directory exists and create it if it doesn't
+    if (!fs.existsSync(backupsDir)) {
+        fs.mkdirSync(backupsDir, { recursive: true });
+    }
+
+    // copy the db.sqlite file to backups
+    // add the date to the filename
+    const date = new Date();
+    const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
+    const dbPath = path.join(dbDir, "db.sqlite");
+    const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
+    fs.copyFileSync(dbPath, backupPath);
+}
 
 export async function runMigrations() {
     try {
@@ -105,7 +136,10 @@ async function executeScripts() {
                     `Successfully completed migration ${migration.version}`
                 );
             } catch (e) {
-                if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
+                if (
+                    e instanceof SqliteError &&
+                    e.code === "SQLITE_CONSTRAINT_UNIQUE"
+                ) {
                     console.error("Migration has already run! Skipping...");
                     continue;
                 }

+ 62 - 0
server/setup/scripts/1.0.0-beta12.ts

@@ -0,0 +1,62 @@
+import db from "@server/db";
+import { configFilePath1, configFilePath2 } from "@server/lib/consts";
+import { sql } from "drizzle-orm";
+import fs from "fs";
+import yaml from "js-yaml";
+
+export default async function migration() {
+    console.log("Running setup script 1.0.0-beta.12...");
+
+    try {
+        // Determine which config file exists
+        const filePaths = [configFilePath1, configFilePath2];
+        let filePath = "";
+        for (const path of filePaths) {
+            if (fs.existsSync(path)) {
+                filePath = path;
+                break;
+            }
+        }
+
+        if (!filePath) {
+            throw new Error(
+                `No config file found (expected config.yml or config.yaml).`
+            );
+        }
+
+        // Read and parse the YAML file
+        let rawConfig: any;
+        const fileContents = fs.readFileSync(filePath, "utf8");
+        rawConfig = yaml.load(fileContents);
+
+        if (!rawConfig.flags) {
+            rawConfig.flags = {};
+        }
+
+        rawConfig.flags.allow_base_domain_resources = true;
+
+        // Write the updated YAML back to the file
+        const updatedYaml = yaml.dump(rawConfig);
+        fs.writeFileSync(filePath, updatedYaml, "utf8");
+
+        console.log(`Added new config option: allow_base_domain_resources`);
+    } catch (e) {
+        console.log(
+            `Unable to add new config option: allow_base_domain_resources. This is not critical.`
+        );
+        console.error(e);
+    }
+
+    try {
+        db.transaction((trx) => {
+            trx.run(sql`ALTER TABLE 'resources' ADD 'isBaseDomain' integer;`);
+        });
+
+        console.log(`Added new column: isBaseDomain`);
+    } catch (e) {
+        console.log("Unable to add new column: isBaseDomain");
+        throw e;
+    }
+
+    console.log("Done.");
+}

+ 108 - 43
src/app/[orgId]/settings/resources/CreateResourceForm.tsx

@@ -63,6 +63,8 @@ import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import Link from "next/link";
 import { SquareArrowOutUpRight } from "lucide-react";
 import CopyTextBox from "@app/components/CopyTextBox";
+import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
+import { Label } from "@app/components/ui/label";
 
 const createResourceFormSchema = z
     .object({
@@ -71,7 +73,8 @@ const createResourceFormSchema = z
         siteId: z.number(),
         http: z.boolean(),
         protocol: z.string(),
-        proxyPort: z.number().optional()
+        proxyPort: z.number().optional(),
+        isBaseDomain: z.boolean().optional()
     })
     .refine(
         (data) => {
@@ -92,7 +95,7 @@ const createResourceFormSchema = z
     )
     .refine(
         (data) => {
-            if (data.http) {
+            if (data.http && !data.isBaseDomain) {
                 return subdomainSchema.safeParse(data.subdomain).success;
             }
             return true;
@@ -131,12 +134,15 @@ export default function CreateResourceForm({
     const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
     const [showSnippets, setShowSnippets] = useState(false);
     const [resourceId, setResourceId] = useState<number | null>(null);
+    const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
+        "subdomain"
+    );
 
     const form = useForm<CreateResourceFormValues>({
         resolver: zodResolver(createResourceFormSchema),
         defaultValues: {
             subdomain: "",
-            name: "My Resource",
+            name: "",
             http: true,
             protocol: "tcp"
         }
@@ -180,7 +186,8 @@ export default function CreateResourceForm({
                     http: data.http,
                     protocol: data.protocol,
                     proxyPort: data.http ? undefined : data.proxyPort,
-                    siteId: data.siteId
+                    siteId: data.siteId,
+                    isBaseDomain: data.isBaseDomain
                 }
             )
             .catch((e) => {
@@ -246,7 +253,7 @@ export default function CreateResourceForm({
                                                 <FormLabel>Name</FormLabel>
                                                 <FormControl>
                                                     <Input
-                                                        placeholder="Your name"
+                                                        placeholder="Resource name"
                                                         {...field}
                                                     />
                                                 </FormControl>
@@ -291,33 +298,89 @@ export default function CreateResourceForm({
                                         />
                                     )}
 
+                                    {form.watch("http") &&
+                                        env.flags.allowBaseDomainResources && (
+                                            <div>
+                                                <RadioGroup
+                                                    className="flex space-x-4"
+                                                    defaultValue={domainType}
+                                                    onValueChange={(val) => {
+                                                        setDomainType(
+                                                            val as any
+                                                        );
+                                                        form.setValue(
+                                                            "isBaseDomain",
+                                                            val === "basedomain"
+                                                        );
+                                                    }}
+                                                >
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="subdomain"
+                                                            id="r1"
+                                                        />
+                                                        <Label htmlFor="r1">
+                                                            Subdomain
+                                                        </Label>
+                                                    </div>
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="basedomain"
+                                                            id="r2"
+                                                        />
+                                                        <Label htmlFor="r2">
+                                                            Base Domain
+                                                        </Label>
+                                                    </div>
+                                                </RadioGroup>
+                                            </div>
+                                        )}
+
                                     {form.watch("http") && (
                                         <FormField
                                             control={form.control}
                                             name="subdomain"
                                             render={({ field }) => (
                                                 <FormItem>
-                                                    <FormLabel>
-                                                        Subdomain
-                                                    </FormLabel>
-                                                    <FormControl>
-                                                        <CustomDomainInput
-                                                            value={
-                                                                field.value ??
-                                                                ""
-                                                            }
-                                                            domainSuffix={
-                                                                domainSuffix
-                                                            }
-                                                            placeholder="Enter subdomain"
-                                                            onChange={(value) =>
-                                                                form.setValue(
-                                                                    "subdomain",
+                                                    {!env.flags
+                                                        .allowBaseDomainResources && (
+                                                        <FormLabel>
+                                                            Subdomain
+                                                        </FormLabel>
+                                                    )}
+                                                    {domainType ===
+                                                    "subdomain" ? (
+                                                        <FormControl>
+                                                            <CustomDomainInput
+                                                                value={
+                                                                    field.value ??
+                                                                    ""
+                                                                }
+                                                                domainSuffix={
+                                                                    domainSuffix
+                                                                }
+                                                                placeholder="Subdomain"
+                                                                onChange={(
                                                                     value
-                                                                )
-                                                            }
-                                                        />
-                                                    </FormControl>
+                                                                ) =>
+                                                                    form.setValue(
+                                                                        "subdomain",
+                                                                        value
+                                                                    )
+                                                                }
+                                                            />
+                                                        </FormControl>
+                                                    ) : (
+                                                        <FormControl>
+                                                            <Input
+                                                                value={
+                                                                    domainSuffix
+                                                                }
+                                                                readOnly
+                                                                disabled
+                                                            />
+                                                        </FormControl>
+                                                    )}
                                                     <FormDescription>
                                                         This is the fully
                                                         qualified domain name
@@ -471,9 +534,7 @@ export default function CreateResourceForm({
                                                                             site
                                                                         ) => (
                                                                             <CommandItem
-                                                                                value={
-                                                                                    `${site.siteId}:${site.name}:${site.niceId}`
-                                                                                }
+                                                                                value={`${site.siteId}:${site.name}:${site.niceId}`}
                                                                                 key={
                                                                                     site.siteId
                                                                                 }
@@ -567,21 +628,25 @@ export default function CreateResourceForm({
                         )}
                     </CredenzaBody>
                     <CredenzaFooter>
-                        {!showSnippets && <Button
-                            type="submit"
-                            form="create-resource-form"
-                            loading={loading}
-                            disabled={loading}
-                        >
-                            Create Resource
-                        </Button>}
-
-                        {showSnippets && <Button
-                            loading={loading}
-                            onClick={() => goToResource()}
-                        >
-                            Go to Resource
-                        </Button>}
+                        {!showSnippets && (
+                            <Button
+                                type="submit"
+                                form="create-resource-form"
+                                loading={loading}
+                                disabled={loading}
+                            >
+                                Create Resource
+                            </Button>
+                        )}
+
+                        {showSnippets && (
+                            <Button
+                                loading={loading}
+                                onClick={() => goToResource()}
+                            >
+                                Go to Resource
+                            </Button>
+                        )}
 
                         <CredenzaClose asChild>
                             <Button variant="outline">Close</Button>

+ 10 - 9
src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx

@@ -2,11 +2,7 @@
 
 import { useState } from "react";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import {
-    InfoIcon,
-    ShieldCheck,
-    ShieldOff
-} from "lucide-react";
+import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
 import { useOrgContext } from "@app/hooks/useOrgContext";
 import { useResourceContext } from "@app/hooks/useResourceContext";
 import { Separator } from "@app/components/ui/separator";
@@ -26,9 +22,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
     const { org } = useOrgContext();
     const { resource, authInfo } = useResourceContext();
 
-    const fullUrl = `${resource.ssl ? "https" : "http"}://${
-        resource.subdomain
-    }.${org.org.domain}`;
+    let fullUrl = `${resource.ssl ? "https" : "http"}://`;
+    if (resource.isBaseDomain) {
+        fullUrl = fullUrl + org.org.domain;
+    } else {
+        fullUrl = fullUrl + `${resource.subdomain}.${org.org.domain}`;
+    }
 
     return (
         <Alert>
@@ -82,7 +81,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
                             <InfoSection>
                                 <InfoSectionTitle>Protocol</InfoSectionTitle>
                                 <InfoSectionContent>
-                                    <span>{resource.protocol.toUpperCase()}</span>
+                                    <span>
+                                        {resource.protocol.toUpperCase()}
+                                    </span>
                                 </InfoSectionContent>
                             </InfoSection>
                             <Separator orientation="vertical" />

+ 1 - 2
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -132,9 +132,8 @@ export default function ReverseProxyTargets(props: {
         defaultValues: {
             ip: "",
             method: resource.http ? "http" : null,
-            port: ""
             // protocol: "TCP",
-        }
+        } as z.infer<typeof addTargetSchema>
     });
 
     useEffect(() => {

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

@@ -51,13 +51,17 @@ import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
+import { pullEnv } from "@app/lib/pullEnv";
+import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
+import { Label } from "@app/components/ui/label";
 
 const GeneralFormSchema = z
     .object({
         subdomain: z.string().optional(),
         name: z.string().min(1).max(255),
         proxyPort: z.number().optional(),
-        http: z.boolean()
+        http: z.boolean(),
+        isBaseDomain: z.boolean().optional()
     })
     .refine(
         (data) => {
@@ -78,7 +82,7 @@ const GeneralFormSchema = z
     )
     .refine(
         (data) => {
-            if (data.http) {
+            if (data.http && !data.isBaseDomain) {
                 return subdomainSchema.safeParse(data.subdomain).success;
             }
             return true;
@@ -103,9 +107,11 @@ export default function GeneralForm() {
     const { org } = useOrgContext();
     const router = useRouter();
 
+    const { env } = useEnvContext();
+
     const orgId = params.orgId;
 
-    const api = createApiClient(useEnvContext());
+    const api = createApiClient({ env });
 
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
     const [saveLoading, setSaveLoading] = useState(false);
@@ -113,13 +119,18 @@ export default function GeneralForm() {
     const [transferLoading, setTransferLoading] = useState(false);
     const [open, setOpen] = useState(false);
 
+    const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
+        resource.isBaseDomain ? "basedomain" : "subdomain"
+    );
+
     const form = useForm<GeneralFormValues>({
         resolver: zodResolver(GeneralFormSchema),
         defaultValues: {
             name: resource.name,
             subdomain: resource.subdomain ? resource.subdomain : undefined,
             proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
-            http: resource.http
+            http: resource.http,
+            isBaseDomain: resource.isBaseDomain ? true : false
         },
         mode: "onChange"
     });
@@ -148,7 +159,8 @@ export default function GeneralForm() {
             .post(`resource/${resource?.resourceId}`, {
                 name: data.name,
                 subdomain: data.subdomain,
-                proxyPort: data.proxyPort
+                proxyPort: data.proxyPort,
+                isBaseDomain: data.isBaseDomain
             })
             .catch((e) => {
                 toast({
@@ -170,7 +182,8 @@ export default function GeneralForm() {
             updateResource({
                 name: data.name,
                 subdomain: data.subdomain,
-                proxyPort: data.proxyPort
+                proxyPort: data.proxyPort,
+                isBaseDomain: data.isBaseDomain
             });
         }
         setSaveLoading(false);
@@ -242,40 +255,103 @@ export default function GeneralForm() {
                                     )}
                                 />
 
-                                {resource.http ? (
-                                    <FormField
-                                        control={form.control}
-                                        name="subdomain"
-                                        render={({ field }) => (
-                                            <FormItem>
-                                                <FormLabel>Subdomain</FormLabel>
-                                                <FormControl>
-                                                    <CustomDomainInput
-                                                        value={
-                                                            field.value || ""
-                                                        }
-                                                        domainSuffix={
-                                                            domainSuffix
-                                                        }
-                                                        placeholder="Enter subdomain"
-                                                        onChange={(value) =>
-                                                            form.setValue(
-                                                                "subdomain",
-                                                                value
-                                                            )
-                                                        }
-                                                    />
-                                                </FormControl>
-                                                <FormDescription>
-                                                    This is the subdomain that
-                                                    will be used to access the
-                                                    resource.
-                                                </FormDescription>
-                                                <FormMessage />
-                                            </FormItem>
+                                {resource.http && (
+                                    <>
+                                        {env.flags.allowBaseDomainResources && (
+                                            <div>
+                                                <RadioGroup
+                                                    className="flex space-x-4"
+                                                    defaultValue={domainType}
+                                                    onValueChange={(val) => {
+                                                        setDomainType(
+                                                            val as any
+                                                        );
+                                                        form.setValue(
+                                                            "isBaseDomain",
+                                                            val === "basedomain"
+                                                        );
+                                                    }}
+                                                >
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="subdomain"
+                                                            id="r1"
+                                                        />
+                                                        <Label htmlFor="r1">
+                                                            Subdomain
+                                                        </Label>
+                                                    </div>
+                                                    <div className="flex items-center space-x-2">
+                                                        <RadioGroupItem
+                                                            value="basedomain"
+                                                            id="r2"
+                                                        />
+                                                        <Label htmlFor="r2">
+                                                            Base Domain
+                                                        </Label>
+                                                    </div>
+                                                </RadioGroup>
+                                            </div>
                                         )}
-                                    />
-                                ) : (
+
+                                        <FormField
+                                            control={form.control}
+                                            name="subdomain"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    {!env.flags
+                                                        .allowBaseDomainResources && (
+                                                        <FormLabel>
+                                                            Subdomain
+                                                        </FormLabel>
+                                                    )}
+
+                                                    {domainType ===
+                                                    "subdomain" ? (
+                                                        <FormControl>
+                                                            <CustomDomainInput
+                                                                value={
+                                                                    field.value ||
+                                                                    ""
+                                                                }
+                                                                domainSuffix={
+                                                                    domainSuffix
+                                                                }
+                                                                placeholder="Enter subdomain"
+                                                                onChange={(
+                                                                    value
+                                                                ) =>
+                                                                    form.setValue(
+                                                                        "subdomain",
+                                                                        value
+                                                                    )
+                                                                }
+                                                            />
+                                                        </FormControl>
+                                                    ) : (
+                                                        <FormControl>
+                                                            <Input
+                                                                value={
+                                                                    domainSuffix
+                                                                }
+                                                                readOnly
+                                                                disabled
+                                                            />
+                                                        </FormControl>
+                                                    )}
+                                                    <FormDescription>
+                                                        This is the subdomain
+                                                        that will be used to
+                                                        access the resource.
+                                                    </FormDescription>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+                                    </>
+                                )}
+
+                                {!resource.http && (
                                     <FormField
                                         control={form.control}
                                         name="proxyPort"

+ 46 - 23
src/components/ui/checkbox.tsx

@@ -1,30 +1,53 @@
-"use client"
+"use client";
 
-import * as React from "react"
-import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
-import { Check } from "lucide-react"
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { Check } from "lucide-react";
 
-import { cn } from "@app/lib/cn"
+import { cn } from "@app/lib/cn";
 
 const Checkbox = React.forwardRef<
-  React.ElementRef<typeof CheckboxPrimitive.Root>,
-  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
+    React.ElementRef<typeof CheckboxPrimitive.Root>,
+    React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
 >(({ className, ...props }, ref) => (
-  <CheckboxPrimitive.Root
-    ref={ref}
-    className={cn(
-      "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
-      className
-    )}
-    {...props}
-  >
-    <CheckboxPrimitive.Indicator
-      className={cn("flex items-center justify-center text-current")}
+    <CheckboxPrimitive.Root
+        ref={ref}
+        className={cn(
+            "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
+            className
+        )}
+        {...props}
     >
-      <Check className="h-4 w-4" />
-    </CheckboxPrimitive.Indicator>
-  </CheckboxPrimitive.Root>
-))
-Checkbox.displayName = CheckboxPrimitive.Root.displayName
+        <CheckboxPrimitive.Indicator
+            className={cn("flex items-center justify-center text-current")}
+        >
+            <Check className="h-4 w-4" />
+        </CheckboxPrimitive.Indicator>
+    </CheckboxPrimitive.Root>
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
 
-export { Checkbox }
+interface CheckboxWithLabelProps
+    extends React.ComponentPropsWithoutRef<typeof Checkbox> {
+    label: string;
+}
+
+const CheckboxWithLabel = React.forwardRef<
+    React.ElementRef<typeof Checkbox>,
+    CheckboxWithLabelProps
+>(({ className, label, id, ...props }, ref) => {
+    return (
+        <div className={cn("flex items-center space-x-2", className)}>
+            <Checkbox id={id} ref={ref} {...props} />
+            <label
+                htmlFor={id}
+                className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+            >
+                {label}
+            </label>
+        </div>
+    );
+});
+CheckboxWithLabel.displayName = "CheckboxWithLabel";
+
+export { Checkbox, CheckboxWithLabel };

+ 8 - 2
src/lib/pullEnv.ts

@@ -6,8 +6,10 @@ export function pullEnv(): Env {
             nextPort: process.env.NEXT_PORT as string,
             externalPort: process.env.SERVER_EXTERNAL_PORT as string,
             sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
-            resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
-            resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
+            resourceAccessTokenParam: process.env
+                .RESOURCE_ACCESS_TOKEN_PARAM as string,
+            resourceSessionRequestParam: process.env
+                .RESOURCE_SESSION_REQUEST_PARAM as string
         },
         app: {
             environment: process.env.ENVIRONMENT as string,
@@ -29,6 +31,10 @@ export function pullEnv(): Env {
                     : false,
             allowRawResources:
                 process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
+            allowBaseDomainResources:
+                process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES === "true"
+                    ? true
+                    : false
         }
     };
 }

+ 1 - 0
src/lib/types/env.ts

@@ -18,5 +18,6 @@ export type Env = {
         disableUserCreateOrg: boolean;
         emailVerificationRequired: boolean;
         allowRawResources: boolean;
+        allowBaseDomainResources: boolean;
     }
 };