Merge branch 'dev' into audit-logs

This commit is contained in:
Owen
2025-10-27 10:02:32 -07:00
47 changed files with 2183 additions and 511 deletions

View File

@@ -44,7 +44,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
mbIn: formatSize(client.megabytesIn || 0),
mbOut: formatSize(client.megabytesOut || 0),
orgId: params.orgId,
online: client.online
online: client.online,
olmVersion: client.olmVersion || undefined,
olmUpdateAvailable: client.olmUpdateAvailable || false,
};
});

View File

@@ -0,0 +1,32 @@
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { internal } from "@app/lib/api";
import { GetDomainResponse } from "@server/routers/domain/getDomain";
import { AxiosResponse } from "axios";
import DomainProvider from "@app/providers/DomainProvider";
interface SettingsLayoutProps {
children: React.ReactNode;
params: Promise<{ domainId: string; orgId: string }>;
}
export default async function SettingsLayout({ children, params }: SettingsLayoutProps) {
const { domainId, orgId } = await params;
let domain = null;
try {
const res = await internal.get<AxiosResponse<GetDomainResponse>>(
`/org/${orgId}/domain/${domainId}`,
await authCookieHeader()
);
domain = res.data.data;
} catch {
redirect(`/${orgId}/settings/domains`);
}
return (
<DomainProvider domain={domain} orgId={orgId}>
{children}
</DomainProvider>
);
}

View File

@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import { RefreshCw } from "lucide-react";
import { Button } from "@app/components/ui/button";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import DomainInfoCard from "@app/components/DomainInfoCard";
import { useDomain } from "@app/contexts/domainContext";
import { useTranslations } from "next-intl";
export default function DomainSettingsPage() {
const { domain, orgId } = useDomain();
const router = useRouter();
const api = createApiClient(useEnvContext());
const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(new Set());
const t = useTranslations();
const refreshData = async () => {
setIsRefreshing(true);
try {
await new Promise((resolve) => setTimeout(resolve, 200));
router.refresh();
} catch {
toast({
title: t("error"),
description: t("refreshError"),
variant: "destructive",
});
} finally {
setIsRefreshing(false);
}
};
const restartDomain = async (domainId: string) => {
setRestartingDomains((prev) => new Set(prev).add(domainId));
try {
await api.post(`/org/${orgId}/domain/${domainId}/restart`);
toast({
title: t("success"),
description: t("domainRestartedDescription", {
fallback: "Domain verification restarted successfully",
}),
});
refreshData();
} catch (e) {
toast({
title: t("error"),
description: formatAxiosError(e),
variant: "destructive",
});
} finally {
setRestartingDomains((prev) => {
const newSet = new Set(prev);
newSet.delete(domainId);
return newSet;
});
}
};
if (!domain) {
return null;
}
const isRestarting = restartingDomains.has(domain.domainId);
return (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
<Button
variant="outline"
onClick={() => restartDomain(domain.domainId)}
disabled={isRestarting}
>
{isRestarting ? (
<>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("restarting", { fallback: "Restarting..." })}
</>
) : (
<>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("restart", { fallback: "Restart" })}
</>
)}
</Button>
</div>
<div className="space-y-6">
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
</div>
</>
);
}

View File

@@ -60,7 +60,7 @@ export default async function DomainsPage(props: Props) {
title={t("domains")}
description={t("domainsDescription")}
/>
<DomainsTable domains={domains} />
<DomainsTable domains={domains} orgId={org.org.orgId} />
</OrgProvider>
</>
);

View File

@@ -77,7 +77,8 @@ import {
MoveRight,
ArrowUp,
Info,
ArrowDown
ArrowDown,
AlertTriangle
} from "lucide-react";
import { ContainersSelector } from "@app/components/ContainersSelector";
import { useTranslations } from "next-intl";
@@ -115,6 +116,7 @@ import {
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
import { Alert, AlertDescription } from "@app/components/ui/alert";
const addTargetSchema = z
.object({
@@ -288,7 +290,9 @@ export default function ReverseProxyTargets(props: {
),
headers: z
.array(z.object({ name: z.string(), value: z.string() }))
.nullable()
.nullable(),
proxyProtocol: z.boolean().optional(),
proxyProtocolVersion: z.number().int().min(1).max(2).optional()
});
const tlsSettingsSchema = z.object({
@@ -325,7 +329,9 @@ export default function ReverseProxyTargets(props: {
resolver: zodResolver(proxySettingsSchema),
defaultValues: {
setHostHeader: resource.setHostHeader || "",
headers: resource.headers
headers: resource.headers,
proxyProtocol: resource.proxyProtocol || false,
proxyProtocolVersion: resource.proxyProtocolVersion || 1
}
});
@@ -549,11 +555,11 @@ export default function ReverseProxyTargets(props: {
prev.map((t) =>
t.targetId === target.targetId
? {
...t,
targetId: response.data.data.targetId,
new: false,
updated: false
}
...t,
targetId: response.data.data.targetId,
new: false,
updated: false
}
: t
)
);
@@ -673,11 +679,11 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
)
);
@@ -688,10 +694,10 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? {
...target,
...config,
updated: true
}
...target,
...config,
updated: true
}
: target
)
);
@@ -800,6 +806,22 @@ export default function ReverseProxyTargets(props: {
setHostHeader: proxyData.setHostHeader || null,
headers: proxyData.headers || null
});
} else {
// For TCP/UDP resources, save proxy protocol settings
const proxyData = proxySettingsForm.getValues();
const payload = {
proxyProtocol: proxyData.proxyProtocol || false,
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
};
await api.post(`/resource/${resource.resourceId}`, payload);
updateResource({
...resource,
proxyProtocol: proxyData.proxyProtocol || false,
proxyProtocolVersion: proxyData.proxyProtocolVersion || 1
});
}
toast({
@@ -1064,7 +1086,7 @@ export default function ReverseProxyTargets(props: {
className={cn(
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
!row.original.siteId &&
"text-muted-foreground"
"text-muted-foreground"
)}
>
<span className="truncate max-w-[150px]">
@@ -1404,12 +1426,12 @@ export default function ReverseProxyTargets(props: {
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
)
)}
@@ -1675,6 +1697,102 @@ export default function ReverseProxyTargets(props: {
</SettingsSection>
)}
{!resource.http && resource.protocol && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("proxyProtocol")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("proxyProtocolDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveAllSettings
)}
className="space-y-4"
id="proxy-protocol-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="proxyProtocol"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="proxy-protocol-toggle"
label={t(
"enableProxyProtocol"
)}
description={t(
"proxyProtocolInfo"
)}
defaultChecked={
field.value || false
}
onCheckedChange={(val) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
/>
{proxySettingsForm.watch("proxyProtocol") && (
<>
<FormField
control={proxySettingsForm.control}
name="proxyProtocolVersion"
render={({ field }) => (
<FormItem>
<FormLabel>{t("proxyProtocolVersion")}</FormLabel>
<FormControl>
<Select
value={String(field.value || 1)}
onValueChange={(value) =>
field.onChange(parseInt(value, 10))
}
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t("version1")}
</SelectItem>
<SelectItem value="2">
{t("version2")}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
{t("versionDescription")}
</FormDescription>
</FormItem>
)}
/>
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>{t("warning")}:</strong> {t("proxyProtocolWarning")}
</AlertDescription>
</Alert>
</>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
<div className="flex justify-end mt-6">
<Button
onClick={saveAllSettings}

View File

@@ -130,7 +130,7 @@ export default function ResourceRules(props: {
PATH: t('path'),
IP: "IP",
CIDR: t('ipAddressRange'),
GEOIP: t('country')
COUNTRY: t('country')
} as const;
const addRuleForm = useForm({
@@ -212,7 +212,7 @@ export default function ResourceRules(props: {
setLoading(false);
return;
}
if (data.match === "GEOIP" && !COUNTRIES.some(c => c.code === data.value)) {
if (data.match === "COUNTRY" && !COUNTRIES.some(c => c.code === data.value)) {
toast({
variant: "destructive",
title: t('rulesErrorInvalidCountry'),
@@ -270,7 +270,7 @@ export default function ResourceRules(props: {
return t('rulesMatchIpAddress');
case "PATH":
return t('rulesMatchUrl');
case "GEOIP":
case "COUNTRY":
return t('rulesMatchCountry');
}
}
@@ -492,8 +492,8 @@ export default function ResourceRules(props: {
cell: ({ row }) => (
<Select
defaultValue={row.original.match}
onValueChange={(value: "CIDR" | "IP" | "PATH" | "GEOIP") =>
updateRule(row.original.ruleId, { match: value, value: value === "GEOIP" ? "US" : row.original.value })
onValueChange={(value: "CIDR" | "IP" | "PATH" | "COUNTRY") =>
updateRule(row.original.ruleId, { match: value, value: value === "COUNTRY" ? "US" : row.original.value })
}
>
<SelectTrigger className="min-w-[125px]">
@@ -504,7 +504,7 @@ export default function ResourceRules(props: {
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="GEOIP">{RuleMatch.GEOIP}</SelectItem>
<SelectItem value="COUNTRY">{RuleMatch.COUNTRY}</SelectItem>
)}
</SelectContent>
</Select>
@@ -514,7 +514,7 @@ export default function ResourceRules(props: {
accessorKey: "value",
header: t('value'),
cell: ({ row }) => (
row.original.match === "GEOIP" ? (
row.original.match === "COUNTRY" ? (
<Popover>
<PopoverTrigger asChild>
<Button
@@ -748,8 +748,8 @@ export default function ResourceRules(props: {
{RuleMatch.CIDR}
</SelectItem>
{isMaxmindAvailable && (
<SelectItem value="GEOIP">
{RuleMatch.GEOIP}
<SelectItem value="COUNTRY">
{RuleMatch.COUNTRY}
</SelectItem>
)}
</SelectContent>
@@ -775,7 +775,7 @@ export default function ResourceRules(props: {
}
/>
<FormControl>
{addRuleForm.watch("match") === "GEOIP" ? (
{addRuleForm.watch("match") === "COUNTRY" ? (
<Popover open={openAddRuleCountrySelect} onOpenChange={setOpenAddRuleCountrySelect}>
<PopoverTrigger asChild>
<Button