Merge branch 'dev' into transfer-resource-to-new-site

This commit is contained in:
Owen Schwartz
2025-02-01 17:03:05 -05:00
85 changed files with 3396 additions and 1197 deletions

View File

@@ -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>

View File

@@ -2,12 +2,8 @@
import { useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
InfoIcon,
LinkIcon,
CheckIcon,
CopyIcon,
ShieldCheck,
ShieldOff
} from "lucide-react";
@@ -42,37 +38,65 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</AlertTitle>
<AlertDescription className="mt-4">
<InfoSections>
<InfoSection>
<InfoSectionTitle>Authentication</InfoSectionTitle>
<InfoSectionContent>
{authInfo.password ||
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" />
<span>
This resource is protected with at least
one auth method.
</span>
</div>
) : (
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff className="w-4 h-4" />
<span>
Anyone can access this resource.
</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
<Separator orientation="vertical" />
<InfoSection>
<InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard text={fullUrl} isLink={true} />
</InfoSectionContent>
</InfoSection>
{resource.http ? (
<>
<InfoSection>
<InfoSectionTitle>
Authentication
</InfoSectionTitle>
<InfoSectionContent>
{authInfo.password ||
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" />
<span>
This resource is protected with
at least one auth method.
</span>
</div>
) : (
<div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff className="w-4 h-4" />
<span>
Anyone can access this resource.
</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
<Separator orientation="vertical" />
<InfoSection>
<InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={fullUrl}
isLink={true}
/>
</InfoSectionContent>
</InfoSection>
</>
) : (
<>
<InfoSection>
<InfoSectionTitle>Protocol</InfoSectionTitle>
<InfoSectionContent>
<span>{resource.protocol.toUpperCase()}</span>
</InfoSectionContent>
</InfoSection>
<Separator orientation="vertical" />
<InfoSection>
<InfoSectionTitle>Port</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={resource.proxyPort!.toString()}
isLink={false}
/>
</InfoSectionContent>
</InfoSection>
</>
)}
</InfoSections>
</AlertDescription>
</Alert>

View File

@@ -48,6 +48,7 @@ import {
SettingsSectionFooter
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { InfoPopup } from "@app/components/ui/info-popup";
const UsersRolesFormSchema = z.object({
roles: z.array(
@@ -665,10 +666,12 @@ export default function ResourceAuthenticationPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Whitelisted Emails
<InfoPopup
text="Whitelisted Emails"
info="Only users with these email addresses will be able to access this resource. They will be prompted to enter a one-time password sent to their email. Wildcards (*@example.com) can be used to allow any email address from a domain."
/>
</FormLabel>
<FormControl>
{/* @ts-ignore */}
{/* @ts-ignore */}
<TagInput
{...field}
@@ -681,6 +684,17 @@ export default function ResourceAuthenticationPage() {
return z
.string()
.email()
.or(
z
.string()
.regex(
/^\*@[\w.-]+\.[a-zA-Z]{2,}$/,
{
message:
"Invalid email address. Wildcard (*) must be the entire local part."
}
)
)
.safeParse(
tag
).success;

View File

@@ -63,6 +63,7 @@ import {
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { useSiteContext } from "@app/hooks/useSiteContext";
import { InfoPopup } from "@app/components/ui/info-popup";
// Regular expressions for validation
const DOMAIN_REGEX =
@@ -94,7 +95,7 @@ const domainSchema = z
const addTargetSchema = z.object({
ip: domainSchema,
method: z.string(),
method: z.string().nullable(),
port: z.coerce.number().int().positive()
// protocol: z.string(),
});
@@ -130,8 +131,8 @@ export default function ReverseProxyTargets(props: {
resolver: zodResolver(addTargetSchema),
defaultValues: {
ip: "",
method: "http",
port: 80
method: resource.http ? "http" : null,
port: resource.http ? 80 : resource.proxyPort || 1234
// protocol: "TCP",
}
});
@@ -321,7 +322,7 @@ export default function ReverseProxyTargets(props: {
});
setSslEnabled(val);
updateResource({ ssl: sslEnabled });
updateResource({ ssl: val });
toast({
title: "SSL Configuration",
@@ -330,26 +331,6 @@ export default function ReverseProxyTargets(props: {
}
const columns: ColumnDef<LocalTarget>[] = [
{
accessorKey: "method",
header: "Method",
cell: ({ row }) => (
<Select
defaultValue={row.original.method}
onValueChange={(value) =>
updateTarget(row.original.targetId, { method: value })
}
>
<SelectTrigger className="min-w-[100px]">
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
</SelectContent>
</Select>
)
},
{
accessorKey: "ip",
header: "IP / Hostname",
@@ -436,6 +417,32 @@ export default function ReverseProxyTargets(props: {
}
];
if (resource.http) {
const methodCol: ColumnDef<LocalTarget> = {
accessorKey: "method",
header: "Method",
cell: ({ row }) => (
<Select
defaultValue={row.original.method ?? ""}
onValueChange={(value) =>
updateTarget(row.original.targetId, { method: value })
}
>
<SelectTrigger className="min-w-[100px]">
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
</SelectContent>
</Select>
)
};
// add this to the first column
columns.unshift(methodCol);
}
const table = useReactTable({
data: targets,
columns,
@@ -451,29 +458,29 @@ export default function ReverseProxyTargets(props: {
return (
<SettingsContainer>
{/* SSL Section */}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
SSL Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Setup SSL to secure your connections with LetsEncrypt
certificates
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={resource.ssl}
onCheckedChange={async (val) => {
await saveSsl(val);
}}
/>
</SettingsSectionBody>
</SettingsSection>
{resource.http && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
SSL Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Setup SSL to secure your connections with
LetsEncrypt certificates
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={resource.ssl}
onCheckedChange={async (val) => {
await saveSsl(val);
}}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{/* Targets Section */}
<SettingsSection>
<SettingsSectionHeader>
@@ -491,39 +498,47 @@ export default function ReverseProxyTargets(props: {
className="space-y-4"
>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<FormField
control={addTargetForm.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => {
addTargetForm.setValue(
"method",
{resource.http && (
<FormField
control={addTargetForm.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>Method</FormLabel>
<FormControl>
<Select
value={
field.value ||
undefined
}
onValueChange={(
value
);
}}
>
<SelectTrigger id="method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
) => {
addTargetForm.setValue(
"method",
value
);
}}
>
<SelectTrigger id="method">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={addTargetForm.control}
name="ip"
@@ -637,6 +652,9 @@ export default function ReverseProxyTargets(props: {
</TableBody>
</Table>
</TableContainer>
<p className="text-sm text-muted-foreground">
Adding more than one target above will enable load balancing.
</p>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button

View File

@@ -13,7 +13,6 @@ import {
FormLabel,
FormMessage
} from "@/components/ui/form";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { Input } from "@/components/ui/input";
import {
Command,
@@ -21,10 +20,8 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@/components/ui/command";
import { cn } from "@app/lib/cn";
import {
Popover,
PopoverContent,
@@ -50,16 +47,47 @@ import {
} from "@app/components/Settings";
import { useOrgContext } from "@app/hooks/useOrgContext";
import CustomDomainInput from "../CustomDomainInput";
import ResourceInfoBox from "../ResourceInfoBox";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
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";
const GeneralFormSchema = z.object({
name: z.string(),
subdomain: subdomainSchema
// siteId: z.number(),
});
const GeneralFormSchema = z
.object({
subdomain: z.string().optional(),
name: z.string().min(1).max(255),
proxyPort: z.number().optional(),
http: z.boolean()
})
.refine(
(data) => {
if (!data.http) {
return z
.number()
.int()
.min(1)
.max(65535)
.safeParse(data.proxyPort).success;
}
return true;
},
{
message: "Invalid port number",
path: ["proxyPort"]
}
)
.refine(
(data) => {
if (data.http) {
return subdomainSchema.safeParse(data.subdomain).success;
}
return true;
},
{
message: "Invalid subdomain",
path: ["subdomain"]
}
);
const TransferFormSchema = z.object({
siteId: z.number()
@@ -89,8 +117,9 @@ export default function GeneralForm() {
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: resource.name,
subdomain: resource.subdomain
// siteId: resource.siteId!,
subdomain: resource.subdomain ? resource.subdomain : undefined,
proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
http: resource.http
},
mode: "onChange"
});
@@ -211,33 +240,78 @@ export default function GeneralForm() {
)}
/>
<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 ? (
<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>
)}
/>
) : (
<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
)
}
/>
</FormControl>
<FormDescription>
This is the port that will
be used to access the
resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>

View File

@@ -90,13 +90,16 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
title: "Connectivity",
href: `/{orgId}/settings/resources/{resourceId}/connectivity`
// icon: <Cloud className="w-4 h-4" />,
},
{
}
];
if (resource.http) {
sidebarNavItems.push({
title: "Authentication",
href: `/{orgId}/settings/resources/{resourceId}/authentication`
// icon: <Shield className="w-4 h-4" />,
}
];
});
}
return (
<>