This commit is contained in:
Owen
2025-12-22 16:28:41 -05:00
parent e28ab19ed4
commit 5c67a1cb12
55 changed files with 636 additions and 490 deletions

View File

@@ -71,7 +71,9 @@ export default async function GeneralSettingsPage({
<div className="space-y-6">
<OrgInfoCard />
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
<HorizontalTabs items={navItems}>
{children}
</HorizontalTabs>
</div>
</OrgUserProvider>
</OrgProvider>

View File

@@ -71,7 +71,7 @@ export default async function ClientResourcesPage(
niceId: siteResource.niceId,
tcpPortRangeString: siteResource.tcpPortRangeString || null,
udpPortRangeString: siteResource.udpPortRangeString || null,
disableIcmp: siteResource.disableIcmp || false,
disableIcmp: siteResource.disableIcmp || false
};
}
);

View File

@@ -240,13 +240,7 @@ export default function ResourceAuthenticationPage() {
}))
);
hasInitializedRef.current = true;
}, [
pageLoading,
resourceRoles,
resourceUsers,
whitelist,
orgIdps
]);
}, [pageLoading, resourceRoles, resourceUsers, whitelist, orgIdps]);
const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState(
onSubmitUsersRoles,
@@ -602,9 +596,7 @@ export default function ResourceAuthenticationPage() {
{ssoEnabled && allIdps.length > 0 && (
<div className="space-y-2">
<label className="text-sm font-medium">
{t(
"defaultIdentityProvider"
)}
{t("defaultIdentityProvider")}
</label>
<Select
onValueChange={(value) => {

View File

@@ -118,8 +118,7 @@ export default function ResourceRules(props: {
const [countrySelectValue, setCountrySelectValue] = useState("");
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
useState(false);
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] =
useState(false);
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false);
const router = useRouter();
const t = useTranslations();
const { env } = useEnvContext();
@@ -181,7 +180,7 @@ export default function ResourceRules(props: {
// Normalize ASN value
if (data.match === "ASN") {
const originalValue = data.value.toUpperCase();
// Handle special "ALL" case
if (originalValue === "ALL" || originalValue === "AS0") {
data.value = "ALL";
@@ -542,7 +541,11 @@ export default function ResourceRules(props: {
updateRule(row.original.ruleId, {
match: value,
value:
value === "COUNTRY" ? "US" : value === "ASN" ? "AS15169" : row.original.value
value === "COUNTRY"
? "US"
: value === "ASN"
? "AS15169"
: row.original.value
})
}
>
@@ -559,9 +562,7 @@ export default function ResourceRules(props: {
</SelectItem>
)}
{isMaxmindAsnAvailable && (
<SelectItem value="ASN">
{RuleMatch.ASN}
</SelectItem>
<SelectItem value="ASN">{RuleMatch.ASN}</SelectItem>
)}
</SelectContent>
</Select>
@@ -654,9 +655,7 @@ export default function ResourceRules(props: {
</PopoverTrigger>
<PopoverContent className="min-w-[200px] p-0">
<Command>
<CommandInput
placeholder="Search ASNs or enter custom..."
/>
<CommandInput placeholder="Search ASNs or enter custom..." />
<CommandList>
<CommandEmpty>
No ASN found. Enter a custom ASN below.
@@ -665,7 +664,9 @@ export default function ResourceRules(props: {
{MAJOR_ASNS.map((asn) => (
<CommandItem
key={asn.code}
value={asn.name + " " + asn.code}
value={
asn.name + " " + asn.code
}
onSelect={() => {
updateRule(
row.original.ruleId,
@@ -1056,8 +1057,8 @@ export default function ResourceRules(props: {
</PopoverContent>
</Popover>
) : addRuleForm.watch(
"match"
) === "ASN" ? (
"match"
) === "ASN" ? (
<Popover
open={
openAddRuleAsnSelect
@@ -1086,21 +1087,27 @@ export default function ResourceRules(props: {
field.value
)
?.name +
" (" +
field.value +
")" || field.value
" (" +
field.value +
")" ||
field.value
: "Select ASN"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput
placeholder="Search ASNs or enter custom..."
/>
<CommandInput placeholder="Search ASNs or enter custom..." />
<CommandList>
<CommandEmpty>
No ASN found. Use the custom input below.
No
ASN
found.
Use
the
custom
input
below.
</CommandEmpty>
<CommandGroup>
{MAJOR_ASNS.map(
@@ -1112,7 +1119,9 @@ export default function ResourceRules(props: {
asn.code
}
value={
asn.name + " " + asn.code
asn.name +
" " +
asn.code
}
onSelect={() => {
field.onChange(
@@ -1138,6 +1147,7 @@ export default function ResourceRules(props: {
{
asn.code
}
)
</CommandItem>
)
@@ -1148,14 +1158,32 @@ export default function ResourceRules(props: {
<div className="border-t p-2">
<Input
placeholder="Enter custom ASN (e.g., AS15169)"
onKeyDown={(e) => {
if (e.key === "Enter") {
const value = e.currentTarget.value
.toUpperCase()
.replace(/^AS/, "");
if (/^\d+$/.test(value)) {
field.onChange("AS" + value);
setOpenAddRuleAsnSelect(false);
onKeyDown={(
e
) => {
if (
e.key ===
"Enter"
) {
const value =
e.currentTarget.value
.toUpperCase()
.replace(
/^AS/,
""
);
if (
/^\d+$/.test(
value
)
) {
field.onChange(
"AS" +
value
);
setOpenAddRuleAsnSelect(
false
);
}
}
}}

View File

@@ -756,7 +756,9 @@ WantedBy=default.target`
render={({ field }) => (
<FormItem className="md:col-start-1 md:col-span-2">
<FormLabel>
{t("siteAddress")}
{t(
"siteAddress"
)}
</FormLabel>
<FormControl>
<Input

View File

@@ -66,4 +66,3 @@ export const ClientDownloadBanner = () => {
};
export default ClientDownloadBanner;

View File

@@ -99,14 +99,12 @@ export default function ClientResourcesTable({
siteId: number
) => {
try {
await api
.delete(`/site-resource/${resourceId}`)
.then(() => {
startTransition(() => {
router.refresh();
setIsDeleteModalOpen(false);
});
await api.delete(`/site-resource/${resourceId}`).then(() => {
startTransition(() => {
router.refresh();
setIsDeleteModalOpen(false);
});
});
} catch (e) {
console.error(t("resourceErrorDelete"), e);
toast({

View File

@@ -87,7 +87,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
return false;
}
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
if (
startPort < 1 ||
startPort > 65535 ||
endPort < 1 ||
endPort > 65535
) {
return false;
}
@@ -131,7 +136,10 @@ const getPortModeFromString = (val: string | undefined | null): PortMode => {
};
// Helper to get the port string for API from mode and custom value
const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => {
const getPortStringFromMode = (
mode: PortMode,
customValue: string
): string | undefined => {
if (mode === "all") return "*";
if (mode === "blocked") return "";
return customValue;
@@ -1097,8 +1105,7 @@ export default function CreateInternalResourceDialog({
size="sm"
tags={
form.getValues()
.roles ||
[]
.roles || []
}
setTags={(
newRoles
@@ -1154,8 +1161,7 @@ export default function CreateInternalResourceDialog({
)}
tags={
form.getValues()
.users ||
[]
.users || []
}
size="sm"
setTags={(
@@ -1245,9 +1251,7 @@ export default function CreateInternalResourceDialog({
restrictTagsToAutocompleteOptions={
true
}
sortTags={
true
}
sortTags={true}
/>
</FormControl>
<FormMessage />

View File

@@ -95,4 +95,3 @@ export const DismissableBanner = ({
};
export default DismissableBanner;

View File

@@ -501,13 +501,19 @@ export default function EditInternalResourceDialog({
// ]);
await queryClient.invalidateQueries(
resourceQueries.siteResourceRoles({ siteResourceId: resource.id })
resourceQueries.siteResourceRoles({
siteResourceId: resource.id
})
);
await queryClient.invalidateQueries(
resourceQueries.siteResourceUsers({ siteResourceId: resource.id })
resourceQueries.siteResourceUsers({
siteResourceId: resource.id
})
);
await queryClient.invalidateQueries(
resourceQueries.siteResourceClients({ siteResourceId: resource.id })
resourceQueries.siteResourceClients({
siteResourceId: resource.id
})
);
toast({

View File

@@ -330,7 +330,7 @@ export default function ExitNodesTable({
isRefreshing={isRefreshing}
columnVisibility={{
type: false,
address: false,
address: false
}}
enableColumnVisibility={true}
/>

View File

@@ -116,10 +116,12 @@ export function LayoutSidebar({
isCollapsed={isSidebarCollapsed}
/>
</div>
<div className={cn(
"w-full border-b border-border",
isSidebarCollapsed && "mb-2"
)} />
<div
className={cn(
"w-full border-b border-border",
isSidebarCollapsed && "mb-2"
)}
/>
<div className="flex-1 overflow-y-auto relative">
<div className="px-2 pt-1">
{!isAdminPage && user.serverAdmin && (

View File

@@ -120,7 +120,9 @@ export default function LoginForm({
const focusInput = () => {
// Try using the ref first
if (otpContainerRef.current) {
const hiddenInput = otpContainerRef.current.querySelector('input') as HTMLInputElement;
const hiddenInput = otpContainerRef.current.querySelector(
"input"
) as HTMLInputElement;
if (hiddenInput) {
hiddenInput.focus();
return;
@@ -128,17 +130,23 @@ export default function LoginForm({
}
// Fallback: query the DOM
const otpContainer = document.querySelector('[data-slot="input-otp"]');
const otpContainer = document.querySelector(
'[data-slot="input-otp"]'
);
if (!otpContainer) return;
const hiddenInput = otpContainer.querySelector('input') as HTMLInputElement;
const hiddenInput = otpContainer.querySelector(
"input"
) as HTMLInputElement;
if (hiddenInput) {
hiddenInput.focus();
return;
}
// Last resort: click the first slot
const firstSlot = otpContainer.querySelector('[data-slot="input-otp-slot"]') as HTMLElement;
const firstSlot = otpContainer.querySelector(
'[data-slot="input-otp-slot"]'
) as HTMLElement;
if (firstSlot) {
firstSlot.click();
}
@@ -508,7 +516,10 @@ export default function LoginForm({
render={({ field }) => (
<FormItem>
<FormControl>
<div ref={otpContainerRef} className="flex justify-center">
<div
ref={otpContainerRef}
className="flex justify-center"
>
<InputOTP
maxLength={6}
{...field}

View File

@@ -11,9 +11,7 @@ type MachineClientsBannerProps = {
orgId: string;
};
export const MachineClientsBanner = ({
orgId
}: MachineClientsBannerProps) => {
export const MachineClientsBanner = ({ orgId }: MachineClientsBannerProps) => {
const t = useTranslations();
return (
@@ -57,4 +55,3 @@ export const MachineClientsBanner = ({
};
export default MachineClientsBanner;

View File

@@ -39,4 +39,3 @@ export default function OrgInfoCard({}: OrgInfoCardProps) {
</Alert>
);
}

View File

@@ -119,4 +119,3 @@ export default async function OrgLoginPage({
</div>
);
}

View File

@@ -51,4 +51,3 @@ export const PrivateResourcesBanner = ({
};
export default PrivateResourcesBanner;

View File

@@ -81,10 +81,10 @@ export default function ProductUpdates({
const showNewVersionPopup = Boolean(
latestVersion &&
valid(latestVersion) &&
valid(currentVersion) &&
ignoredVersionUpdate !== latestVersion &&
gt(latestVersion, currentVersion)
valid(latestVersion) &&
valid(currentVersion) &&
ignoredVersionUpdate !== latestVersion &&
gt(latestVersion, currentVersion)
);
const filteredUpdates = data.updates.filter(

View File

@@ -20,4 +20,3 @@ export const ProxyResourcesBanner = () => {
};
export default ProxyResourcesBanner;

View File

@@ -1,6 +1,6 @@
"use client";
import {Button} from "@app/components/ui/button";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
@@ -9,12 +9,12 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
import {Input} from "@app/components/ui/input";
import {toast} from "@app/hooks/useToast";
import {zodResolver} from "@hookform/resolvers/zod";
import {useEffect, useState} from "react";
import {useForm} from "react-hook-form";
import {z} from "zod";
import { Input } from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Credenza,
CredenzaBody,
@@ -25,14 +25,14 @@ import {
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import {formatAxiosError} from "@app/lib/api";
import {AxiosResponse} from "axios";
import {Resource} from "@server/db";
import {createApiClient} from "@app/lib/api";
import {useEnvContext} from "@app/hooks/useEnvContext";
import {useTranslations} from "next-intl";
import {SwitchInput} from "@/components/SwitchInput";
import {InfoPopup} from "@/components/ui/info-popup";
import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import { SwitchInput } from "@/components/SwitchInput";
import { InfoPopup } from "@/components/ui/info-popup";
const setHeaderAuthFormSchema = z.object({
user: z.string().min(4).max(100),
@@ -56,11 +56,11 @@ type SetHeaderAuthFormProps = {
};
export default function SetResourceHeaderAuthForm({
open,
setOpen,
resourceId,
onSetHeaderAuth
}: SetHeaderAuthFormProps) {
open,
setOpen,
resourceId,
onSetHeaderAuth
}: SetHeaderAuthFormProps) {
const api = createApiClient(useEnvContext());
const t = useTranslations();
@@ -82,11 +82,14 @@ export default function SetResourceHeaderAuthForm({
async function onSubmit(data: SetHeaderAuthFormValues) {
setLoading(true);
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/header-auth`, {
user: data.user,
password: data.password,
extendedCompatibility: data.extendedCompatibility
})
api.post<AxiosResponse<Resource>>(
`/resource/${resourceId}/header-auth`,
{
user: data.user,
password: data.password,
extendedCompatibility: data.extendedCompatibility
}
)
.then(() => {
toast({
title: t("resourceHeaderAuthSetup"),
@@ -100,10 +103,10 @@ export default function SetResourceHeaderAuthForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorHeaderAuthSetup'),
title: t("resourceErrorHeaderAuthSetup"),
description: formatAxiosError(
e,
t('resourceErrorHeaderAuthSetupDescription')
t("resourceErrorHeaderAuthSetupDescription")
)
});
})
@@ -139,7 +142,7 @@ export default function SetResourceHeaderAuthForm({
<FormField
control={form.control}
name="user"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>{t("user")}</FormLabel>
<FormControl>
@@ -149,14 +152,14 @@ export default function SetResourceHeaderAuthForm({
{...field}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>
{t("password")}
@@ -168,25 +171,31 @@ export default function SetResourceHeaderAuthForm({
{...field}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="extendedCompatibility"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="header-auth-compatibility-toggle"
label={t("headerAuthCompatibility")}
info={t('headerAuthCompatibilityInfo')}
label={t(
"headerAuthCompatibility"
)}
info={t(
"headerAuthCompatibilityInfo"
)}
checked={field.value}
onCheckedChange={field.onChange}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>

View File

@@ -91,10 +91,10 @@ export default function SetResourcePasswordForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPasswordSetup'),
title: t("resourceErrorPasswordSetup"),
description: formatAxiosError(
e,
t('resourceErrorPasswordSetupDescription')
t("resourceErrorPasswordSetupDescription")
)
});
})

View File

@@ -97,10 +97,10 @@ export default function SetResourcePincodeForm({
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPincodeSetup'),
title: t("resourceErrorPincodeSetup"),
description: formatAxiosError(
e,
t('resourceErrorPincodeSetupDescription')
t("resourceErrorPincodeSetupDescription")
)
});
})

View File

@@ -37,4 +37,3 @@ export const SitesBanner = () => {
};
export default SitesBanner;

View File

@@ -1,10 +1,14 @@
import React from "react";
import {Switch} from "./ui/switch";
import {Label} from "./ui/label";
import {Button} from "@/components/ui/button";
import {Info} from "lucide-react";
import {info} from "winston";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import { Switch } from "./ui/switch";
import { Label } from "./ui/label";
import { Button } from "@/components/ui/button";
import { Info } from "lucide-react";
import { info } from "winston";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@/components/ui/popover";
interface SwitchComponentProps {
id: string;
@@ -18,22 +22,22 @@ interface SwitchComponentProps {
}
export function SwitchInput({
id,
label,
description,
info,
disabled,
checked,
defaultChecked = false,
onCheckedChange
}: SwitchComponentProps) {
id,
label,
description,
info,
disabled,
checked,
defaultChecked = false,
onCheckedChange
}: SwitchComponentProps) {
const defaultTrigger = (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full p-0"
>
<Info className="h-4 w-4"/>
<Info className="h-4 w-4" />
<span className="sr-only">Show info</span>
</Button>
);
@@ -49,18 +53,20 @@ export function SwitchInput({
disabled={disabled}
/>
{label && <Label htmlFor={id}>{label}</Label>}
{info && <Popover>
<PopoverTrigger asChild>
{defaultTrigger}
</PopoverTrigger>
<PopoverContent className="w-80">
{info && (
<p className="text-sm text-muted-foreground">
{info}
</p>
)}
</PopoverContent>
</Popover>}
{info && (
<Popover>
<PopoverTrigger asChild>
{defaultTrigger}
</PopoverTrigger>
<PopoverContent className="w-80">
{info && (
<p className="text-sm text-muted-foreground">
{info}
</p>
)}
</PopoverContent>
</Popover>
)}
</div>
{description && (
<span className="text-muted-foreground text-sm">

View File

@@ -276,14 +276,15 @@ function AuthPageSettings({
<>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>{t("customDomain")}</SettingsSectionTitle>
<SettingsSectionTitle>
{t("customDomain")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("authPageDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<PaidFeaturesAlert />
<Form {...form}>

View File

@@ -58,12 +58,7 @@ export default function IdpLoginButtons({
let redirectToUrl: string | undefined;
try {
console.log(
"generating",
idpId,
redirect || "/",
orgId
);
console.log("generating", idpId, redirect || "/", orgId);
const safeRedirect = cleanRedirect(redirect || "/");
const response = await generateOidcUrlProxy(
idpId,

View File

@@ -288,7 +288,10 @@ export function DataTable<TData, TValue>({
useEffect(() => {
if (persistPageSize && pagination.pageSize !== pageSize) {
// Only store if user has actually changed it from initial value
if (hasUserChangedPageSize.current && pagination.pageSize !== initialPageSize.current) {
if (
hasUserChangedPageSize.current &&
pagination.pageSize !== initialPageSize.current
) {
setStoredPageSize(pagination.pageSize, tableId);
}
setPageSize(pagination.pageSize);
@@ -298,7 +301,9 @@ export function DataTable<TData, TValue>({
useEffect(() => {
// Persist column visibility to localStorage when it changes (but not on initial mount)
if (shouldPersistColumnVisibility) {
const hasChanged = JSON.stringify(columnVisibility) !== JSON.stringify(initialColumnVisibilityState.current);
const hasChanged =
JSON.stringify(columnVisibility) !==
JSON.stringify(initialColumnVisibilityState.current);
if (hasChanged) {
// Mark as user-initiated change and persist
hasUserChangedColumnVisibility.current = true;