Basic clients working

This commit is contained in:
Owen
2025-07-27 10:21:27 -07:00
parent 15adfcca8c
commit 28f8b05dbc
21 changed files with 387 additions and 87 deletions

View File

@@ -48,7 +48,7 @@ export default async function ClientsPage(props: ClientsPageProps) {
return (
<>
<SettingsSectionTitle
title="Manage Clients"
title="Manage Clients (beta)"
description="Clients are devices that can connect to your sites"
/>

View File

@@ -18,6 +18,7 @@ import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { RotateCw } from "lucide-react";
import { createApiClient } from "@app/lib/api";
import { build } from "@server/build";
type ResourceInfoBoxType = {};
@@ -34,7 +35,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t('resourceInfo')}
{t("resourceInfo")}
</AlertTitle>
<AlertDescription className="mt-4">
<InfoSections cols={4}>
@@ -42,7 +43,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<>
<InfoSection>
<InfoSectionTitle>
{t('authentication')}
{t("authentication")}
</InfoSectionTitle>
<InfoSectionContent>
{authInfo.password ||
@@ -51,12 +52,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" />
<span>{t('protected')}</span>
<span>{t("protected")}</span>
</div>
) : (
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff className="w-4 h-4" />
<span>{t('notProtected')}</span>
<span>{t("notProtected")}</span>
</div>
)}
</InfoSectionContent>
@@ -71,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>{t('site')}</InfoSectionTitle>
<InfoSectionTitle>{t("site")}</InfoSectionTitle>
<InfoSectionContent>
{resource.siteName}
</InfoSectionContent>
@@ -98,7 +99,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
) : (
<>
<InfoSection>
<InfoSectionTitle>{t('protocol')}</InfoSectionTitle>
<InfoSectionTitle>
{t("protocol")}
</InfoSectionTitle>
<InfoSectionContent>
<span>
{resource.protocol.toUpperCase()}
@@ -106,7 +109,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>{t('port')}</InfoSectionTitle>
<InfoSectionTitle>{t("port")}</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={resource.proxyPort!.toString()}
@@ -114,13 +117,29 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
/>
</InfoSectionContent>
</InfoSection>
{build == "oss" && (
<InfoSection>
<InfoSectionTitle>
{t("externalProxyEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
<span>
{resource.enableProxy
? t("enabled")
: t("disabled")}
</span>
</InfoSectionContent>
</InfoSection>
)}
</>
)}
<InfoSection>
<InfoSectionTitle>{t('visibility')}</InfoSectionTitle>
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
<InfoSectionContent>
<span>
{resource.enabled ? t('enabled') : t('disabled')}
{resource.enabled
? t("enabled")
: t("disabled")}
</span>
</InfoSectionContent>
</InfoSection>

View File

@@ -66,6 +66,7 @@ import {
} from "@server/routers/resource";
import { SwitchInput } from "@app/components/SwitchInput";
import { useTranslations } from "next-intl";
import { Checkbox } from "@app/components/ui/checkbox";
import {
Credenza,
CredenzaBody,
@@ -78,6 +79,7 @@ import {
} from "@app/components/Credenza";
import DomainPicker from "@app/components/DomainPicker";
import { Globe } from "lucide-react";
import { build } from "@server/build";
const TransferFormSchema = z.object({
siteId: z.number()
@@ -118,25 +120,31 @@ export default function GeneralForm() {
fullDomain: string;
} | null>(null);
const GeneralFormSchema = z.object({
enabled: z.boolean(),
subdomain: z.string().optional(),
name: z.string().min(1).max(255),
domainId: z.string().optional(),
proxyPort: z.number().int().min(1).max(65535).optional()
}).refine((data) => {
// For non-HTTP resources, proxyPort should be defined
if (!resource.http) {
return data.proxyPort !== undefined;
}
// For HTTP resources, proxyPort should be undefined
return data.proxyPort === undefined;
}, {
message: !resource.http
? "Port number is required for non-HTTP resources"
: "Port number should not be set for HTTP resources",
path: ["proxyPort"]
});
const GeneralFormSchema = z
.object({
enabled: z.boolean(),
subdomain: z.string().optional(),
name: z.string().min(1).max(255),
domainId: z.string().optional(),
proxyPort: z.number().int().min(1).max(65535).optional(),
enableProxy: z.boolean().optional()
})
.refine(
(data) => {
// For non-HTTP resources, proxyPort should be defined
if (!resource.http) {
return data.proxyPort !== undefined;
}
// For HTTP resources, proxyPort should be undefined
return data.proxyPort === undefined;
},
{
message: !resource.http
? "Port number is required for non-HTTP resources"
: "Port number should not be set for HTTP resources",
path: ["proxyPort"]
}
);
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
@@ -147,7 +155,8 @@ export default function GeneralForm() {
name: resource.name,
subdomain: resource.subdomain ? resource.subdomain : undefined,
domainId: resource.domainId || undefined,
proxyPort: resource.proxyPort || undefined
proxyPort: resource.proxyPort || undefined,
enableProxy: resource.enableProxy || false
},
mode: "onChange"
});
@@ -211,7 +220,8 @@ export default function GeneralForm() {
name: data.name,
subdomain: data.subdomain,
domainId: data.domainId,
proxyPort: data.proxyPort
proxyPort: data.proxyPort,
enableProxy: data.enableProxy
}
)
.catch((e) => {
@@ -238,7 +248,8 @@ export default function GeneralForm() {
name: data.name,
subdomain: data.subdomain,
fullDomain: resource.fullDomain,
proxyPort: data.proxyPort
proxyPort: data.proxyPort,
enableProxy: data.enableProxy
});
router.refresh();
@@ -357,16 +368,29 @@ export default function GeneralForm() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t("resourcePortNumber")}
{t(
"resourcePortNumber"
)}
</FormLabel>
<FormControl>
<Input
type="number"
value={field.value ?? ""}
onChange={(e) =>
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e.target.value
? parseInt(e.target.value)
e
.target
.value
? parseInt(
e
.target
.value
)
: undefined
)
}
@@ -374,11 +398,49 @@ export default function GeneralForm() {
</FormControl>
<FormMessage />
<FormDescription>
{t("resourcePortNumberDescription")}
{t(
"resourcePortNumberDescription"
)}
</FormDescription>
</FormItem>
)}
/>
{build == "oss" && (
<FormField
control={form.control}
name="enableProxy"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
variant={
"outlinePrimarySquare"
}
checked={
field.value
}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{t(
"resourceEnableProxy"
)}
</FormLabel>
<FormDescription>
{t(
"resourceEnableProxyDescription"
)}
</FormDescription>
</div>
</FormItem>
)}
/>
)}
</>
)}

View File

@@ -25,6 +25,7 @@ import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { Button } from "@app/components/ui/button";
import { Checkbox } from "@app/components/ui/checkbox";
import { useParams, useRouter } from "next/navigation";
import { ListSitesResponse } from "@server/routers/site";
import { formatAxiosError } from "@app/lib/api";
@@ -64,6 +65,7 @@ import CopyTextBox from "@app/components/CopyTextBox";
import Link from "next/link";
import { useTranslations } from "next-intl";
import DomainPicker from "@app/components/DomainPicker";
import { build } from "@server/build";
const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255),
@@ -78,7 +80,8 @@ const httpResourceFormSchema = z.object({
const tcpUdpResourceFormSchema = z.object({
protocol: z.string(),
proxyPort: z.number().int().min(1).max(65535)
proxyPort: z.number().int().min(1).max(65535),
enableProxy: z.boolean().default(false)
});
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
@@ -144,7 +147,8 @@ export default function Page() {
resolver: zodResolver(tcpUdpResourceFormSchema),
defaultValues: {
protocol: "tcp",
proxyPort: undefined
proxyPort: undefined,
enableProxy: false
}
});
@@ -163,16 +167,17 @@ export default function Page() {
if (isHttp) {
const httpData = httpForm.getValues();
Object.assign(payload, {
subdomain: httpData.subdomain,
domainId: httpData.domainId,
protocol: "tcp",
});
Object.assign(payload, {
subdomain: httpData.subdomain,
domainId: httpData.domainId,
protocol: "tcp"
});
} else {
const tcpUdpData = tcpUdpForm.getValues();
Object.assign(payload, {
protocol: tcpUdpData.protocol,
proxyPort: tcpUdpData.proxyPort
proxyPort: tcpUdpData.proxyPort,
enableProxy: tcpUdpData.enableProxy
});
}
@@ -198,8 +203,15 @@ export default function Page() {
if (isHttp) {
router.push(`/${orgId}/settings/resources/${id}`);
} else {
setShowSnippets(true);
router.refresh();
const tcpUdpData = tcpUdpForm.getValues();
// Only show config snippets if enableProxy is explicitly true
if (tcpUdpData.enableProxy === true) {
setShowSnippets(true);
router.refresh();
} else {
// If enableProxy is false or undefined, go directly to resource page
router.push(`/${orgId}/settings/resources/${id}`);
}
}
}
} catch (e) {
@@ -603,6 +615,46 @@ export default function Page() {
</FormItem>
)}
/>
{build == "oss" && (
<FormField
control={
tcpUdpForm.control
}
name="enableProxy"
render={({
field
}) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
variant={
"outlinePrimarySquare"
}
checked={
field.value
}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{t(
"resourceEnableProxy"
)}
</FormLabel>
<FormDescription>
{t(
"resourceEnableProxyDescription"
)}
</FormDescription>
</div>
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>