Bläddra i källkod

Merge pull request #118 from fosrl/dev

Small Bugfixes
Milo Schwartz 5 månader sedan
förälder
incheckning
5c507cc0ec

+ 0 - 1
config/config.example.yml

@@ -9,7 +9,6 @@ server:
     internal_port: 3001
     next_port: 3002
     internal_hostname: "pangolin"
-    secure_cookies: true
     session_cookie_name: "p_session_token"
     resource_access_token_param: "p_token"
     resource_session_request_param: "p_session_request"

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

@@ -4,13 +4,7 @@ api:
 
 providers:
   http:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config/http"
-    pollInterval: "5s"
-  udp:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config/udp"
-    pollInterval: "5s"
-  tcp:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp"
+    endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config"
     pollInterval: "5s"
   file:
     filename: "/etc/traefik/dynamic_config.yml"

+ 1 - 2
install/fs/config.yml

@@ -9,7 +9,6 @@ server:
     internal_port: 3001
     next_port: 3002
     internal_hostname: "pangolin"
-    secure_cookies: true
     session_cookie_name: "p_session_token"
     resource_access_token_param: "p_token"
     resource_session_request_param: "p_session_request"
@@ -40,7 +39,7 @@ rate_limits:
 {{if .EnableEmail}}
 email:
     smtp_host: "{{.EmailSMTPHost}}"
-    smtp_port: "{{.EmailSMTPPort}}"
+    smtp_port: {{.EmailSMTPPort}}
     smtp_user: "{{.EmailSMTPUser}}"
     smtp_pass: "{{.EmailSMTPPass}}"
     no_reply: "{{.EmailNoReply}}"

+ 1 - 7
install/fs/traefik/traefik_config.yml

@@ -4,13 +4,7 @@ api:
 
 providers:
   http:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config/http"
-    pollInterval: "5s"
-  udp:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config/udp"
-    pollInterval: "5s"
-  tcp:
-    endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp"
+    endpoint: "http://pangolin:3001/api/v1/traefik-config"
     pollInterval: "5s"
   file:
     filename: "/etc/traefik/dynamic_config.yml"

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
     "name": "@fosrl/pangolin",
-    "version": "1.0.0-beta.9",
+    "version": "1.0.0-beta.10",
     "private": true,
     "type": "module",
     "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",

+ 0 - 118
server/auth/index.ts

@@ -1,118 +0,0 @@
-import {
-    encodeBase32LowerCaseNoPadding,
-    encodeHexLowerCase,
-} from "@oslojs/encoding";
-import { sha256 } from "@oslojs/crypto/sha2";
-import { Session, sessions, User, users } from "@server/db/schema";
-import db from "@server/db";
-import { eq } from "drizzle-orm";
-import config from "@server/lib/config";
-import type { RandomReader } from "@oslojs/crypto/random";
-import { generateRandomString } from "@oslojs/crypto/random";
-
-export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
-export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
-export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
-export const COOKIE_DOMAIN = "." + config.getBaseDomain();
-
-export function generateSessionToken(): string {
-    const bytes = new Uint8Array(20);
-    crypto.getRandomValues(bytes);
-    const token = encodeBase32LowerCaseNoPadding(bytes);
-    return token;
-}
-
-export async function createSession(
-    token: string,
-    userId: string,
-): Promise<Session> {
-    const sessionId = encodeHexLowerCase(
-        sha256(new TextEncoder().encode(token)),
-    );
-    const session: Session = {
-        sessionId: sessionId,
-        userId,
-        expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
-    };
-    await db.insert(sessions).values(session);
-    return session;
-}
-
-export async function validateSessionToken(
-    token: string,
-): Promise<SessionValidationResult> {
-    const sessionId = encodeHexLowerCase(
-        sha256(new TextEncoder().encode(token)),
-    );
-    const result = await db
-        .select({ user: users, session: sessions })
-        .from(sessions)
-        .innerJoin(users, eq(sessions.userId, users.userId))
-        .where(eq(sessions.sessionId, sessionId));
-    if (result.length < 1) {
-        return { session: null, user: null };
-    }
-    const { user, session } = result[0];
-    if (Date.now() >= session.expiresAt) {
-        await db
-            .delete(sessions)
-            .where(eq(sessions.sessionId, session.sessionId));
-        return { session: null, user: null };
-    }
-    if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
-        session.expiresAt = new Date(
-            Date.now() + SESSION_COOKIE_EXPIRES,
-        ).getTime();
-        await db
-            .update(sessions)
-            .set({
-                expiresAt: session.expiresAt,
-            })
-            .where(eq(sessions.sessionId, session.sessionId));
-    }
-    return { session, user };
-}
-
-export async function invalidateSession(sessionId: string): Promise<void> {
-    await db.delete(sessions).where(eq(sessions.sessionId, sessionId));
-}
-
-export async function invalidateAllSessions(userId: string): Promise<void> {
-    await db.delete(sessions).where(eq(sessions.userId, userId));
-}
-
-export function serializeSessionCookie(token: string): string {
-    if (SECURE_COOKIES) {
-        return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
-    } else {
-        return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
-    }
-}
-
-export function createBlankSessionTokenCookie(): string {
-    if (SECURE_COOKIES) {
-        return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
-    } else {
-        return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
-    }
-}
-
-const random: RandomReader = {
-    read(bytes: Uint8Array): void {
-        crypto.getRandomValues(bytes);
-    },
-};
-
-export function generateId(length: number): string {
-    const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
-    return generateRandomString(random, alphabet, length);
-}
-
-export function generateIdFromEntropySize(size: number): string {
-    const buffer = crypto.getRandomValues(new Uint8Array(size));
-    return encodeBase32LowerCaseNoPadding(buffer);
-}
-
-export type SessionValidationResult =
-    | { session: Session; user: User }
-    | { session: null; user: null };

+ 2 - 12
server/auth/sessions/app.ts

@@ -24,7 +24,6 @@ export const SESSION_COOKIE_EXPIRES =
     60 *
     60 *
     config.getRawConfig().server.dashboard_session_length_hours;
-export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
 export const COOKIE_DOMAIN =
     "." + new URL(config.getRawConfig().app.dashboard_url).hostname;
 
@@ -108,12 +107,7 @@ export function serializeSessionCookie(
     isSecure: boolean
 ): string {
     if (isSecure) {
-        logger.debug("Setting cookie for secure origin");
-        if (SECURE_COOKIES) {
-            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
-        } else {
-            return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${COOKIE_DOMAIN}`;
-        }
+        return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
     } else {
         return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
     }
@@ -121,11 +115,7 @@ export function serializeSessionCookie(
 
 export function createBlankSessionTokenCookie(isSecure: boolean): string {
     if (isSecure) {
-        if (SECURE_COOKIES) {
-            return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
-        } else {
-            return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
-        }
+        return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
     } else {
         return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
     }

+ 4 - 4
server/auth/sessions/resource.ts

@@ -9,7 +9,6 @@ export const SESSION_COOKIE_NAME =
     config.getRawConfig().server.session_cookie_name;
 export const SESSION_COOKIE_EXPIRES =
     1000 * 60 * 60 * config.getRawConfig().server.resource_session_length_hours;
-export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
 
 export async function createResourceSession(opts: {
     token: string;
@@ -170,7 +169,7 @@ export function serializeResourceSessionCookie(
     token: string,
     isHttp: boolean = false
 ): string {
-    if (SECURE_COOKIES && !isHttp) {
+    if (!isHttp) {
         return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
     } else {
         return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
@@ -179,9 +178,10 @@ export function serializeResourceSessionCookie(
 
 export function createBlankResourceSessionTokenCookie(
     cookieName: string,
-    domain: string
+    domain: string,
+    isHttp: boolean = false
 ): string {
-    if (SECURE_COOKIES) {
+    if (!isHttp) {
         return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
     } else {
         return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`;

+ 0 - 1
server/lib/config.ts

@@ -60,7 +60,6 @@ const configSchema = z.object({
             .transform(stoi)
             .pipe(portSchema),
         internal_hostname: z.string().transform((url) => url.toLowerCase()),
-        secure_cookies: z.boolean(),
         session_cookie_name: z.string(),
         resource_access_token_param: z.string(),
         resource_session_request_param: z.string(),

+ 1 - 2
server/routers/badger/exchangeSession.ts

@@ -12,8 +12,7 @@ import {
     serializeResourceSessionCookie,
     validateResourceSessionToken
 } from "@server/auth/sessions/resource";
-import { generateSessionToken } from "@server/auth";
-import { SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app";
+import { generateSessionToken, SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app";
 import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
 import config from "@server/lib/config";
 import { response } from "@server/lib";

+ 1 - 1
server/routers/badger/verifySession.ts

@@ -26,8 +26,8 @@ import {
 import { Resource, roleResources, userResources } from "@server/db/schema";
 import logger from "@server/logger";
 import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
-import { generateSessionToken } from "@server/auth";
 import NodeCache from "node-cache";
+import { generateSessionToken } from "@server/auth/sessions/app";
 
 // We'll see if this speeds anything up
 const cache = new NodeCache({

+ 5 - 6
server/routers/internal.ts

@@ -1,12 +1,11 @@
 import { Router } from "express";
 import * as gerbil from "@server/routers/gerbil";
 import * as traefik from "@server/routers/traefik";
+import * as resource from "./resource";
+import * as badger from "./badger";
 import * as auth from "@server/routers/auth";
 import HttpCode from "@server/types/HttpCode";
 import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
-import { getExchangeToken } from "./resource/getExchangeToken";
-import { verifyResourceSession } from "./badger";
-import { exchangeSession } from "./badger/exchangeSession";
 
 // Root routes
 const internalRouter = Router();
@@ -26,7 +25,7 @@ internalRouter.post(
     `/resource/:resourceId/get-exchange-token`,
     verifySessionUserMiddleware,
     verifyResourceAccess,
-    getExchangeToken
+    resource.getExchangeToken
 );
 
 // Gerbil routes
@@ -40,7 +39,7 @@ gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
 const badgerRouter = Router();
 internalRouter.use("/badger", badgerRouter);
 
-badgerRouter.post("/verify-session", verifyResourceSession);
-badgerRouter.post("/exchange-session", exchangeSession);
+badgerRouter.post("/verify-session", badger.verifyResourceSession);
+badgerRouter.post("/exchange-session", badger.exchangeSession);
 
 export default internalRouter;

+ 1 - 0
server/routers/newt/handleRegisterMessage.ts

@@ -106,6 +106,7 @@ export const handleRegisterMessage: MessageHandler = async (context) => {
                 eq(targets.enabled, true)
             )
         )
+        .where(eq(resources.siteId, siteId))
         .groupBy(resources.resourceId);
 
     let tcpTargets: string[] = [];

+ 4 - 2
server/setup/migrations.ts

@@ -4,7 +4,7 @@ import path from "path";
 import semver from "semver";
 import { versionMigrations } from "@server/db/schema";
 import { desc } from "drizzle-orm";
-import { __DIRNAME, APP_PATH } from "@server/lib/consts";
+import { __DIRNAME } from "@server/lib/consts";
 import { loadAppVersion } from "@server/lib/loadAppVersion";
 import m1 from "./scripts/1.0.0-beta1";
 import m2 from "./scripts/1.0.0-beta2";
@@ -12,6 +12,7 @@ import m3 from "./scripts/1.0.0-beta3";
 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";
 
 // THIS CANNOT IMPORT ANYTHING FROM THE SERVER
 // EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -23,7 +24,8 @@ const migrations = [
     { version: "1.0.0-beta.3", run: m3 },
     { 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.9", run: m6 },
+    { version: "1.0.0-beta.10", run: m7 }
     // Add new migrations here as they are created
 ] as const;
 

+ 45 - 0
server/setup/scripts/1.0.0-beta10.ts

@@ -0,0 +1,45 @@
+import { configFilePath1, configFilePath2 } from "@server/lib/consts";
+import fs from "fs";
+import yaml from "js-yaml";
+
+export default async function migration() {
+    console.log("Running setup script 1.0.0-beta.10...");
+
+    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);
+
+        delete rawConfig.server.secure_cookies;
+
+        // Write the updated YAML back to the file
+        const updatedYaml = yaml.dump(rawConfig);
+        fs.writeFileSync(filePath, updatedYaml, "utf8");
+
+        console.log(`Removed deprecated config option: secure_cookies.`);
+    } catch (e) {
+        console.log(
+            `Was unable to remove deprecated config option: secure_cookies. Error: ${e}`
+        );
+        return;
+    }
+
+    console.log("Done.");
+}

+ 318 - 229
src/app/[orgId]/settings/resources/CreateResourceForm.tsx

@@ -62,6 +62,7 @@ import {
 import { subdomainSchema } from "@server/schemas/subdomainSchema";
 import Link from "next/link";
 import { SquareArrowOutUpRight } from "lucide-react";
+import CopyTextBox from "@app/components/CopyTextBox";
 
 const createResourceFormSchema = z
     .object({
@@ -129,6 +130,10 @@ export default function CreateResourceForm({
     const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
     const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
 
+    const [showSnippets, setShowSnippets] = useState(false);
+
+    const [resourceId, setResourceId] = useState<number | null>(null);
+
     const form = useForm<CreateResourceFormValues>({
         resolver: zodResolver(createResourceFormSchema),
         defaultValues: {
@@ -186,11 +191,21 @@ export default function CreateResourceForm({
 
         if (res && res.status === 201) {
             const id = res.data.data.resourceId;
-            // navigate to the resource page
-            router.push(`/${orgId}/settings/resources/${id}`);
+            setResourceId(id);
+
+            if (data.http) {
+                goToResource();
+            } else {
+                setShowSnippets(true);
+            }
         }
     }
 
+    function goToResource() {
+        // navigate to the resource page
+        router.push(`/${orgId}/settings/resources/${resourceId}`);
+    }
+
     return (
         <>
             <Credenza
@@ -211,284 +226,358 @@ export default function CreateResourceForm({
                         </CredenzaDescription>
                     </CredenzaHeader>
                     <CredenzaBody>
-                        <Form {...form}>
-                            <form
-                                onSubmit={form.handleSubmit(onSubmit)}
-                                className="space-y-4"
-                                id="create-resource-form"
-                            >
-                                <FormField
-                                    control={form.control}
-                                    name="name"
-                                    render={({ field }) => (
-                                        <FormItem>
-                                            <FormLabel>Name</FormLabel>
-                                            <FormControl>
-                                                <Input
-                                                    placeholder="Your name"
-                                                    {...field}
-                                                />
-                                            </FormControl>
-                                            <FormDescription>
-                                                This is the name that will be
-                                                displayed for this resource.
-                                            </FormDescription>
-                                            <FormMessage />
-                                        </FormItem>
-                                    )}
-                                />
-
-                                {!env.flags.allowRawResources || (
+                        {!showSnippets && (
+                            <Form {...form}>
+                                <form
+                                    onSubmit={form.handleSubmit(onSubmit)}
+                                    className="space-y-4"
+                                    id="create-resource-form"
+                                >
                                     <FormField
                                         control={form.control}
-                                        name="http"
-                                        render={({ field }) => (
-                                            <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                                                <div className="space-y-0.5">
-                                                    <FormLabel className="text-base">
-                                                        HTTP Resource
-                                                    </FormLabel>
-                                                    <FormDescription>
-                                                        Toggle if this is an
-                                                        HTTP resource or a raw
-                                                        TCP/UDP resource
-                                                    </FormDescription>
-                                                </div>
-                                                <FormControl>
-                                                    <Switch
-                                                        checked={field.value}
-                                                        onCheckedChange={
-                                                            field.onChange
-                                                        }
-                                                    />
-                                                </FormControl>
-                                            </FormItem>
-                                        )}
-                                    />
-                                )}
-
-                                {form.watch("http") && (
-                                    <FormField
-                                        control={form.control}
-                                        name="subdomain"
+                                        name="name"
                                         render={({ field }) => (
                                             <FormItem>
-                                                <FormLabel>Subdomain</FormLabel>
+                                                <FormLabel>Name</FormLabel>
                                                 <FormControl>
-                                                    <CustomDomainInput
-                                                        value={
-                                                            field.value ?? ""
-                                                        }
-                                                        domainSuffix={
-                                                            domainSuffix
-                                                        }
-                                                        placeholder="Enter subdomain"
-                                                        onChange={(value) =>
-                                                            form.setValue(
-                                                                "subdomain",
-                                                                value
-                                                            )
-                                                        }
+                                                    <Input
+                                                        placeholder="Your name"
+                                                        {...field}
                                                     />
                                                 </FormControl>
                                                 <FormDescription>
-                                                    This is the fully qualified
-                                                    domain name that will be
-                                                    used to access the resource.
+                                                    This is the name that will
+                                                    be displayed for this
+                                                    resource.
                                                 </FormDescription>
                                                 <FormMessage />
                                             </FormItem>
                                         )}
                                     />
-                                )}
-
-                                {!form.watch("http") && (
-                                    <Link
-                                        className="text-sm text-primary flex items-center gap-1"
-                                        href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
-                                        target="_blank"
-                                        rel="noopener noreferrer"
-                                    >
-                                        <span>
-                                            Learn how to configure TCP/UDP resources
-                                        </span>
-                                        <SquareArrowOutUpRight size={14} />
-                                    </Link>
-                                )}
-
-                                {!form.watch("http") && (
-                                    <>
+
+                                    {!env.flags.allowRawResources || (
                                         <FormField
                                             control={form.control}
-                                            name="protocol"
+                                            name="http"
                                             render={({ field }) => (
-                                                <FormItem>
-                                                    <FormLabel>
-                                                        Protocol
-                                                    </FormLabel>
-                                                    <Select
-                                                        value={field.value}
-                                                        onValueChange={
-                                                            field.onChange
-                                                        }
-                                                    >
-                                                        <FormControl>
-                                                            <SelectTrigger>
-                                                                <SelectValue placeholder="Select a protocol" />
-                                                            </SelectTrigger>
-                                                        </FormControl>
-                                                        <SelectContent>
-                                                            <SelectItem value="tcp">
-                                                                TCP
-                                                            </SelectItem>
-                                                            <SelectItem value="udp">
-                                                                UDP
-                                                            </SelectItem>
-                                                        </SelectContent>
-                                                    </Select>
-                                                    <FormDescription>
-                                                        The protocol to use for
-                                                        the resource
-                                                    </FormDescription>
-                                                    <FormMessage />
+                                                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                                                    <div className="space-y-0.5">
+                                                        <FormLabel className="text-base">
+                                                            HTTP Resource
+                                                        </FormLabel>
+                                                        <FormDescription>
+                                                            Toggle if this is an
+                                                            HTTP resource or a
+                                                            raw TCP/UDP resource
+                                                        </FormDescription>
+                                                    </div>
+                                                    <FormControl>
+                                                        <Switch
+                                                            checked={
+                                                                field.value
+                                                            }
+                                                            onCheckedChange={
+                                                                field.onChange
+                                                            }
+                                                        />
+                                                    </FormControl>
                                                 </FormItem>
                                             )}
                                         />
+                                    )}
+
+                                    {form.watch("http") && (
                                         <FormField
                                             control={form.control}
-                                            name="proxyPort"
+                                            name="subdomain"
                                             render={({ field }) => (
                                                 <FormItem>
                                                     <FormLabel>
-                                                        Port Number
+                                                        Subdomain
                                                     </FormLabel>
                                                     <FormControl>
-                                                        <Input
-                                                            type="number"
-                                                            placeholder="Enter port number"
+                                                        <CustomDomainInput
                                                             value={
                                                                 field.value ??
                                                                 ""
                                                             }
-                                                            onChange={(e) =>
-                                                                field.onChange(
-                                                                    e.target
-                                                                        .value
-                                                                        ? parseInt(
-                                                                              e
-                                                                                  .target
-                                                                                  .value
-                                                                          )
-                                                                        : null
+                                                            domainSuffix={
+                                                                domainSuffix
+                                                            }
+                                                            placeholder="Enter subdomain"
+                                                            onChange={(value) =>
+                                                                form.setValue(
+                                                                    "subdomain",
+                                                                    value
                                                                 )
                                                             }
                                                         />
                                                     </FormControl>
                                                     <FormDescription>
-                                                        The port number to proxy
-                                                        requests to (required
-                                                        for non-HTTP resources)
+                                                        This is the fully
+                                                        qualified domain name
+                                                        that will be used to
+                                                        access the resource.
                                                     </FormDescription>
                                                     <FormMessage />
                                                 </FormItem>
                                             )}
                                         />
-                                    </>
-                                )}
-
-                                <FormField
-                                    control={form.control}
-                                    name="siteId"
-                                    render={({ field }) => (
-                                        <FormItem className="flex flex-col">
-                                            <FormLabel>Site</FormLabel>
-                                            <Popover>
-                                                <PopoverTrigger asChild>
-                                                    <FormControl>
-                                                        <Button
-                                                            variant="outline"
-                                                            role="combobox"
-                                                            className={cn(
-                                                                "justify-between",
-                                                                !field.value &&
-                                                                    "text-muted-foreground"
-                                                            )}
+                                    )}
+
+                                    {!form.watch("http") && (
+                                        <Link
+                                            className="text-sm text-primary flex items-center gap-1"
+                                            href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
+                                            target="_blank"
+                                            rel="noopener noreferrer"
+                                        >
+                                            <span>
+                                                Learn how to configure TCP/UDP
+                                                resources
+                                            </span>
+                                            <SquareArrowOutUpRight size={14} />
+                                        </Link>
+                                    )}
+
+                                    {!form.watch("http") && (
+                                        <>
+                                            <FormField
+                                                control={form.control}
+                                                name="protocol"
+                                                render={({ field }) => (
+                                                    <FormItem>
+                                                        <FormLabel>
+                                                            Protocol
+                                                        </FormLabel>
+                                                        <Select
+                                                            value={field.value}
+                                                            onValueChange={
+                                                                field.onChange
+                                                            }
                                                         >
-                                                            {field.value
-                                                                ? sites.find(
-                                                                      (site) =>
-                                                                          site.siteId ===
-                                                                          field.value
-                                                                  )?.name
-                                                                : "Select site"}
-                                                            <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                                                        </Button>
-                                                    </FormControl>
-                                                </PopoverTrigger>
-                                                <PopoverContent className="p-0">
-                                                    <Command>
-                                                        <CommandInput placeholder="Search site..." />
-                                                        <CommandList>
-                                                            <CommandEmpty>
-                                                                No site found.
-                                                            </CommandEmpty>
-                                                            <CommandGroup>
-                                                                {sites.map(
-                                                                    (site) => (
-                                                                        <CommandItem
-                                                                            value={
-                                                                                site.niceId
-                                                                            }
-                                                                            key={
-                                                                                site.siteId
-                                                                            }
-                                                                            onSelect={() => {
-                                                                                form.setValue(
-                                                                                    "siteId",
-                                                                                    site.siteId
-                                                                                );
-                                                                            }}
-                                                                        >
-                                                                            <CheckIcon
-                                                                                className={cn(
-                                                                                    "mr-2 h-4 w-4",
-                                                                                    site.siteId ===
-                                                                                        field.value
-                                                                                        ? "opacity-100"
-                                                                                        : "opacity-0"
-                                                                                )}
-                                                                            />
-                                                                            {
-                                                                                site.name
-                                                                            }
-                                                                        </CommandItem>
+                                                            <FormControl>
+                                                                <SelectTrigger>
+                                                                    <SelectValue placeholder="Select a protocol" />
+                                                                </SelectTrigger>
+                                                            </FormControl>
+                                                            <SelectContent>
+                                                                <SelectItem value="tcp">
+                                                                    TCP
+                                                                </SelectItem>
+                                                                <SelectItem value="udp">
+                                                                    UDP
+                                                                </SelectItem>
+                                                            </SelectContent>
+                                                        </Select>
+                                                        <FormDescription>
+                                                            The protocol to use
+                                                            for the resource
+                                                        </FormDescription>
+                                                        <FormMessage />
+                                                    </FormItem>
+                                                )}
+                                            />
+                                            <FormField
+                                                control={form.control}
+                                                name="proxyPort"
+                                                render={({ field }) => (
+                                                    <FormItem>
+                                                        <FormLabel>
+                                                            Port Number
+                                                        </FormLabel>
+                                                        <FormControl>
+                                                            <Input
+                                                                type="number"
+                                                                placeholder="Enter port number"
+                                                                value={
+                                                                    field.value ??
+                                                                    ""
+                                                                }
+                                                                onChange={(e) =>
+                                                                    field.onChange(
+                                                                        e.target
+                                                                            .value
+                                                                            ? parseInt(
+                                                                                  e
+                                                                                      .target
+                                                                                      .value
+                                                                              )
+                                                                            : null
                                                                     )
-                                                                )}
-                                                            </CommandGroup>
-                                                        </CommandList>
-                                                    </Command>
-                                                </PopoverContent>
-                                            </Popover>
-                                            <FormDescription>
-                                                This is the site that will be
-                                                used in the dashboard.
-                                            </FormDescription>
-                                            <FormMessage />
-                                        </FormItem>
+                                                                }
+                                                            />
+                                                        </FormControl>
+                                                        <FormDescription>
+                                                            The port number to
+                                                            proxy requests to
+                                                            (required for
+                                                            non-HTTP resources)
+                                                        </FormDescription>
+                                                        <FormMessage />
+                                                    </FormItem>
+                                                )}
+                                            />
+                                        </>
                                     )}
-                                />
-                            </form>
-                        </Form>
+
+                                    <FormField
+                                        control={form.control}
+                                        name="siteId"
+                                        render={({ field }) => (
+                                            <FormItem className="flex flex-col">
+                                                <FormLabel>Site</FormLabel>
+                                                <Popover>
+                                                    <PopoverTrigger asChild>
+                                                        <FormControl>
+                                                            <Button
+                                                                variant="outline"
+                                                                role="combobox"
+                                                                className={cn(
+                                                                    "justify-between",
+                                                                    !field.value &&
+                                                                        "text-muted-foreground"
+                                                                )}
+                                                            >
+                                                                {field.value
+                                                                    ? sites.find(
+                                                                          (
+                                                                              site
+                                                                          ) =>
+                                                                              site.siteId ===
+                                                                              field.value
+                                                                      )?.name
+                                                                    : "Select site"}
+                                                                <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                                                            </Button>
+                                                        </FormControl>
+                                                    </PopoverTrigger>
+                                                    <PopoverContent className="p-0">
+                                                        <Command>
+                                                            <CommandInput placeholder="Search site..." />
+                                                            <CommandList>
+                                                                <CommandEmpty>
+                                                                    No site
+                                                                    found.
+                                                                </CommandEmpty>
+                                                                <CommandGroup>
+                                                                    {sites.map(
+                                                                        (
+                                                                            site
+                                                                        ) => (
+                                                                            <CommandItem
+                                                                                value={
+                                                                                    site.niceId
+                                                                                }
+                                                                                key={
+                                                                                    site.siteId
+                                                                                }
+                                                                                onSelect={() => {
+                                                                                    form.setValue(
+                                                                                        "siteId",
+                                                                                        site.siteId
+                                                                                    );
+                                                                                }}
+                                                                            >
+                                                                                <CheckIcon
+                                                                                    className={cn(
+                                                                                        "mr-2 h-4 w-4",
+                                                                                        site.siteId ===
+                                                                                            field.value
+                                                                                            ? "opacity-100"
+                                                                                            : "opacity-0"
+                                                                                    )}
+                                                                                />
+                                                                                {
+                                                                                    site.name
+                                                                                }
+                                                                            </CommandItem>
+                                                                        )
+                                                                    )}
+                                                                </CommandGroup>
+                                                            </CommandList>
+                                                        </Command>
+                                                    </PopoverContent>
+                                                </Popover>
+                                                <FormDescription>
+                                                    This is the site that will
+                                                    be used in the dashboard.
+                                                </FormDescription>
+                                                <FormMessage />
+                                            </FormItem>
+                                        )}
+                                    />
+                                </form>
+                            </Form>
+                        )}
+
+                        {showSnippets && (
+                            <div>
+                                <div className="flex items-start space-x-4 mb-6 last:mb-0">
+                                    <div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
+                                        1
+                                    </div>
+                                    <div className="flex-grow">
+                                        <h3 className="text-lg font-semibold mb-3">
+                                            Traefik: Add Entrypoints
+                                        </h3>
+                                        <CopyTextBox
+                                            text={`entryPoints:
+  ${form.getValues("protocol")}-${form.getValues("proxyPort")}:
+    address: ":${form.getValues("proxyPort")}/${form.getValues("protocol")}"`}
+                                            wrapText={false}
+                                        />
+                                    </div>
+                                </div>
+
+                                <div className="flex items-start space-x-4 mb-6 last:mb-0">
+                                    <div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
+                                        2
+                                    </div>
+                                    <div className="flex-grow">
+                                        <h3 className="text-lg font-semibold mb-3">
+                                            Gerbil: Expose Ports in Docker
+                                            Compose
+                                        </h3>
+                                        <CopyTextBox
+                                            text={`ports:
+  - ${form.getValues("proxyPort")}:${form.getValues("proxyPort")}${form.getValues("protocol") === "tcp" ? "" : "/" + form.getValues("protocol")}`}
+                                            wrapText={false}
+                                        />
+                                    </div>
+                                </div>
+
+                                <Link
+                                    className="text-sm text-primary flex items-center gap-1"
+                                    href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
+                                    target="_blank"
+                                    rel="noopener noreferrer"
+                                >
+                                    <span>
+                                        Make sure to follow the full guide
+                                    </span>
+                                    <SquareArrowOutUpRight size={14} />
+                                </Link>
+                            </div>
+                        )}
                     </CredenzaBody>
                     <CredenzaFooter>
-                        <Button
+                        {!showSnippets && <Button
                             type="submit"
                             form="create-resource-form"
                             loading={loading}
                             disabled={loading}
                         >
                             Create Resource
-                        </Button>
+                        </Button>}
+
+                        {showSnippets && <Button
+                            loading={loading}
+                            onClick={() => goToResource()}
+                        >
+                            Go to Resource
+                        </Button>}
+
                         <CredenzaClose asChild>
                             <Button variant="outline">Close</Button>
                         </CredenzaClose>

+ 1 - 1
src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx

@@ -37,7 +37,7 @@ export default function CustomDomainInput({
                     className="rounded-r-none flex-grow"
                 />
                 <div className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-muted-foreground">
-                    <span className="text-sm">{domainSuffix}</span>
+                    <span className="text-sm">.{domainSuffix}</span>
                 </div>
             </div>
         </div>

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

@@ -130,7 +130,7 @@ export default function ReverseProxyTargets(props: {
     const addTargetForm = useForm({
         resolver: zodResolver(addTargetSchema),
         defaultValues: {
-            ip: "localhost",
+            ip: "",
             method: resource.http ? "http" : null,
             port: resource.http ? 80 : resource.proxyPort || 1234
             // protocol: "TCP",