mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-21 12:26:40 +00:00
Merge branch 'dev' into audit-logs
This commit is contained in:
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
32
src/app/[orgId]/settings/domains/[domainId]/layout.tsx
Normal file
32
src/app/[orgId]/settings/domains/[domainId]/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
104
src/app/[orgId]/settings/domains/[domainId]/page.tsx
Normal file
104
src/app/[orgId]/settings/domains/[domainId]/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user