Various fixes for rc

This commit is contained in:
Owen
2025-10-27 16:33:21 -07:00
parent 6b18a24f9b
commit 15d63ddffa
10 changed files with 544 additions and 452 deletions

View File

@@ -1744,7 +1744,6 @@
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
"idpDisabled": "Identity providers are disabled.",
"orgAuthPageDisabled": "Organization auth page is disabled.",
"domainRestartedDescription": "Domain verification restarted successfully",
@@ -2040,5 +2039,7 @@
"version2": "Version 2",
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible.",
"warning": "Warning",
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections. Make sure to configure your backend to trust Proxy Protocol headers from Traefik."
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
"restarting": "Restarting...",
"manual": "Manual"
}

View File

@@ -105,7 +105,7 @@ export async function logRequestAudit(
try {
if (data.orgId) {
const retentionDays = await getRetentionDays(data.orgId);
if (retentionDays === 0) {
if (retentionDays == 0) {
// do not log
return;
}

View File

@@ -14,7 +14,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { response } from "@server/lib/response";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { logAccessAudit } from "#private/lib/logAccessAudit";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
const getExchangeTokenParams = z
.object({

View File

@@ -9,7 +9,7 @@ export default async function migration() {
try {
await db.execute(sql`BEGIN`);
await db.execute(sql`UPDATE "resourceRules" SET "match" = "COUNTRY" WHERE "match" = "GEOIP"`);
await db.execute(sql`UPDATE "resourceRules" SET "match" = 'COUNTRY' WHERE "match" = 'GEOIP'`);
await db.execute(sql`
CREATE TABLE "accessAuditLog" (

View File

@@ -15,7 +15,7 @@ export default async function migration() {
db.transaction(() => {
db.prepare(
`UPDATE resourceRules SET match = "COUNTRY" WHERE match = "GEOIP"`
`UPDATE 'resourceRules' SET 'match' = 'COUNTRY' WHERE 'match' = 'GEOIP'`
).run();
db.prepare(
@@ -155,7 +155,7 @@ export default async function migration() {
).run();
db.prepare(
`INSERT INTO '__new_resources'("resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers", "proxyProtocol", "proxyProtocolVersion") SELECT "resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers", "proxyProtocol", "proxyProtocolVersion" FROM 'resources';`
`INSERT INTO '__new_resources'("resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers") SELECT "resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers" FROM 'resources';`
).run();
db.prepare(`DROP TABLE 'resources';`).run();
db.prepare(

View File

@@ -16,8 +16,11 @@ export default function DomainSettingsPage() {
const router = useRouter();
const api = createApiClient(useEnvContext());
const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(new Set());
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
new Set()
);
const t = useTranslations();
const { env } = useEnvContext();
const refreshData = async () => {
setIsRefreshing(true);
@@ -28,7 +31,7 @@ export default function DomainSettingsPage() {
toast({
title: t("error"),
description: t("refreshError"),
variant: "destructive",
variant: "destructive"
});
} finally {
setIsRefreshing(false);
@@ -42,15 +45,15 @@ export default function DomainSettingsPage() {
toast({
title: t("success"),
description: t("domainRestartedDescription", {
fallback: "Domain verification restarted successfully",
}),
fallback: "Domain verification restarted successfully"
})
});
refreshData();
} catch (e) {
toast({
title: t("error"),
description: formatAxiosError(e),
variant: "destructive",
variant: "destructive"
});
} finally {
setRestartingDomains((prev) => {
@@ -74,6 +77,7 @@ export default function DomainSettingsPage() {
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
{env.flags.usePangolinDns && (
<Button
variant="outline"
onClick={() => restartDomain(domain.domainId)}
@@ -95,6 +99,7 @@ export default function DomainSettingsPage() {
</>
)}
</Button>
)}
</div>
<div className="space-y-6">
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />

View File

@@ -416,7 +416,7 @@ export default function GeneralPage() {
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full">
<DropdownMenuContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (
@@ -627,9 +627,7 @@ export default function GeneralPage() {
</form>
</Form>
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
{/* Security Settings Section */}
{build !== "oss" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@@ -718,7 +716,9 @@ export default function GeneralPage() {
onValueChange={(
value
) => {
if (!isDisabled) {
if (
!isDisabled
) {
const numValue =
value ===
"null"
@@ -733,7 +733,9 @@ export default function GeneralPage() {
);
}
}}
disabled={isDisabled}
disabled={
isDisabled
}
>
<SelectTrigger>
<SelectValue
@@ -744,7 +746,9 @@ export default function GeneralPage() {
</SelectTrigger>
<SelectContent>
{SESSION_LENGTH_OPTIONS.map(
(option) => (
(
option
) => (
<SelectItem
key={
option.value ===
@@ -788,7 +792,9 @@ export default function GeneralPage() {
return (
<FormItem className="col-span-2">
<FormLabel>
{t("passwordExpiryDays")}
{t(
"passwordExpiryDays"
)}
</FormLabel>
<FormControl>
<Select
@@ -799,7 +805,9 @@ export default function GeneralPage() {
onValueChange={(
value
) => {
if (!isDisabled) {
if (
!isDisabled
) {
const numValue =
value ===
"null"
@@ -814,7 +822,9 @@ export default function GeneralPage() {
);
}
}}
disabled={isDisabled}
disabled={
isDisabled
}
>
<SelectTrigger>
<SelectValue
@@ -825,7 +835,9 @@ export default function GeneralPage() {
</SelectTrigger>
<SelectContent>
{PASSWORD_EXPIRY_OPTIONS.map(
(option) => (
(
option
) => (
<SelectItem
key={
option.value ===
@@ -864,6 +876,7 @@ export default function GeneralPage() {
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}

View File

@@ -18,9 +18,15 @@ type Props = {
records: DNSRecordRow[];
domainId: string;
isRefreshing?: boolean;
type: string | null;
};
export default function DNSRecordsTable({ records, domainId, isRefreshing }: Props) {
export default function DNSRecordsTable({
records,
domainId,
isRefreshing,
type
}: Props) {
const t = useTranslations();
const columns: ColumnDef<DNSRecordRow>[] = [
@@ -28,56 +34,31 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
accessorKey: "baseDomain",
header: ({ column }) => {
return (
<div
>
{t("recordName", { fallback: "Record name" })}
</div>
<div>{t("recordName", { fallback: "Record name" })}</div>
);
},
cell: ({ row }) => {
const baseDomain = row.original.baseDomain;
return (
<div>
{baseDomain || "-"}
</div>
);
return <div>{baseDomain || "-"}</div>;
}
},
{
accessorKey: "recordType",
header: ({ column }) => {
return (
<div
>
{t("type")}
</div>
);
return <div>{t("type")}</div>;
},
cell: ({ row }) => {
const type = row.original.recordType;
return (
<div className="">
{type}
</div>
);
return <div className="">{type}</div>;
}
},
{
accessorKey: "ttl",
header: ({ column }) => {
return (
<div
>
{t("TTL")}
</div>
);
return <div>{t("TTL")}</div>;
},
cell: ({ row }) => {
return (
<div>
{t("auto")}
</div>
);
return <div>{t("auto")}</div>;
}
},
{
@@ -87,44 +68,39 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
},
cell: ({ row }) => {
const value = row.original.value;
return (
<div>
{value}
</div>
);
return <div>{value}</div>;
}
},
{
accessorKey: "verified",
header: ({ column }) => {
return (
<div
>
{t("status")}
</div>
);
return <div>{t("status")}</div>;
},
cell: ({ row }) => {
const verified = row.original.verified;
return (
verified ? (
return verified ? (
type === "wildcard" ? (
<Badge variant="outlinePrimary">
{t("manual", { fallback: "Manual" })}
</Badge>
) : (
<Badge variant="green">{t("verified")}</Badge>
)
) : (
<Badge variant="yellow">
{t("pending", { fallback: "Pending" })}
</Badge>
)
);
}
}
];
return (
<DNSRecordsDataTable
columns={columns}
data={records}
isRefreshing={isRefreshing}
type={type}
/>
);
}

View File

@@ -29,7 +29,8 @@ import {
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
import { useTranslations } from "next-intl";
import { Badge } from "./ui/badge";
import Link from "next/link";
import { build } from "@server/build";
type TabFilter = {
id: string;
@@ -55,6 +56,7 @@ type DNSRecordsDataTableProps<TData, TValue> = {
defaultTab?: string;
persistPageSize?: boolean | string;
defaultPageSize?: number;
type?: string | null;
};
export function DNSRecordsDataTable<TData, TValue>({
@@ -68,7 +70,7 @@ export function DNSRecordsDataTable<TData, TValue>({
defaultSort,
tabs,
defaultTab,
type
}: DNSRecordsDataTableProps<TData, TValue>) {
const t = useTranslations();
@@ -97,12 +99,9 @@ export function DNSRecordsDataTable<TData, TValue>({
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getFilteredRowModel: getFilteredRowModel()
});
return (
<div className="container mx-auto max-w-12xl">
<Card>
@@ -112,19 +111,22 @@ export function DNSRecordsDataTable<TData, TValue>({
<h1 className="font-bold">{t("dnsRecord")}</h1>
<Badge variant="secondary">{t("required")}</Badge>
</div>
<Button
variant="outline"
>
<ExternalLink className="h-4 w-4 mr-1"/>
<Link href="https://docs.pangolin.net/self-host/dns-and-networking">
<Button variant="outline">
<ExternalLink className="h-4 w-4 mr-1" />
{t("howToAddRecords")}
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-secondary dark:bg-transparent">
<TableRow
key={headerGroup.id}
className="bg-secondary dark:bg-transparent"
>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder

View File

@@ -10,7 +10,16 @@ import {
import { useTranslations } from "next-intl";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useDomainContext } from "@app/hooks/useDomainContext";
import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "./Settings";
import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "./Settings";
import { Button } from "./ui/button";
import {
Form,
@@ -21,7 +30,13 @@ import {
FormMessage,
FormDescription
} from "@app/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "./ui/select";
import { Input } from "./ui/input";
import { useForm } from "react-hook-form";
import z from "zod";
@@ -51,7 +66,6 @@ function toPunycode(domain: string): string {
}
}
function isValidDomainFormat(domain: string): boolean {
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
@@ -59,9 +73,9 @@ function isValidDomainFormat(domain: string): boolean {
return false;
}
const parts = domain.split('.');
const parts = domain.split(".");
for (const part of parts) {
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) {
return false;
}
if (part.length > 63) {
@@ -94,8 +108,10 @@ const certResolverOptions = [
{ id: "custom", title: "Custom Resolver" }
];
export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) {
export default function DomainInfoCard({
orgId,
domainId
}: DomainInfoCardProps) {
const { domain, updateDomain } = useDomainContext();
const t = useTranslations();
const { env } = useEnvContext();
@@ -111,21 +127,24 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
resolver: zodResolver(formSchema),
defaultValues: {
baseDomain: "",
type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
certResolver: domain.certResolver ?? "",
type:
build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
certResolver: domain.certResolver,
preferWildcardCert: false
}
});
useEffect(() => {
if (domain.domainId) {
const certResolverValue = domain.certResolver && domain.certResolver.trim() !== ""
const certResolverValue =
domain.certResolver && domain.certResolver.trim() !== ""
? domain.certResolver
: null;
form.reset({
baseDomain: domain.baseDomain || "",
type: (domain.type as "ns" | "cname" | "wildcard") || "wildcard",
type:
(domain.type as "ns" | "cname" | "wildcard") || "wildcard",
certResolver: certResolverValue,
preferWildcardCert: domain.preferWildcardCert || false
});
@@ -170,7 +189,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
if (!orgId || !domainId) {
toast({
title: t("error"),
description: t("orgOrDomainIdMissing", { fallback: "Organization or Domain ID is missing" }),
description: t("orgOrDomainIdMissing", {
fallback: "Organization or Domain ID is missing"
}),
variant: "destructive"
});
return;
@@ -179,7 +200,11 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
setSaveLoading(true);
try {
const response = await api.patch(
if (!values.certResolver) {
values.certResolver = null;
}
await api.patch(
`/org/${orgId}/domain/${domainId}`,
{
certResolver: values.certResolver,
@@ -195,7 +220,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
toast({
title: t("success"),
description: t("domainSettingsUpdated", { fallback: "Domain settings updated successfully" }),
description: t("domainSettingsUpdated", {
fallback: "Domain settings updated successfully"
}),
variant: "default"
});
} catch (error) {
@@ -222,30 +249,36 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
}
};
return (
<>
<Alert>
<AlertDescription>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("type")}
</InfoSectionTitle>
<InfoSectionTitle>{t("type")}</InfoSectionTitle>
<InfoSectionContent>
<span>
{getTypeDisplay(domain.type ? domain.type : "")}
{getTypeDisplay(
domain.type ? domain.type : ""
)}
</span>
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("status")}
</InfoSectionTitle>
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
<InfoSectionContent>
{domain.verified ? (
<Badge variant="green">{t("verified")}</Badge>
domain.type === "wildcard" ? (
<Badge variant="outlinePrimary">
{t("manual", {
fallback: "Manual"
})}
</Badge>
) : (
<Badge variant="green">
{t("verified")}
</Badge>
)
) : (
<Badge variant="yellow">
{t("pending", { fallback: "Pending" })}
@@ -257,20 +290,13 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
</AlertDescription>
</Alert>
{loadingRecords ? (
<div className="space-y-4">
{t("loadingDNSRecords", { fallback: "Loading DNS Records..." })}
</div>
) : (
<DNSRecordsTable
domainId={domain.domainId}
records={dnsRecords}
isRefreshing={isRefreshing}
type={domain.type}
/>
)
}
{/* Domain Settings - Only show for wildcard domains */}
{domain.type === "wildcard" && (
<SettingsContainer>
<SettingsSection>
@@ -294,33 +320,73 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
name="certResolver"
render={({ field }) => (
<FormItem>
<FormLabel>{t("certResolver")}</FormLabel>
<FormLabel>
{t("certResolver")}
</FormLabel>
<FormControl>
<Select
value={
field.value === null ? "default" :
(field.value === "" || (field.value && field.value !== "default")) ? "custom" :
"default"
field.value ===
null
? "default"
: field.value ===
"" ||
(field.value &&
field.value !==
"default")
? "custom"
: "default"
}
onValueChange={(val) => {
if (val === "default") {
field.onChange(null);
} else if (val === "custom") {
field.onChange("");
onValueChange={(
val
) => {
if (
val ===
"default"
) {
field.onChange(
null
);
} else if (
val ===
"custom"
) {
field.onChange(
""
);
} else {
field.onChange(val);
field.onChange(
val
);
}
}}
>
<SelectTrigger>
<SelectValue placeholder={t("selectCertResolver")} />
<SelectValue
placeholder={t(
"selectCertResolver"
)}
/>
</SelectTrigger>
<SelectContent>
{certResolverOptions.map((opt) => (
<SelectItem key={opt.id} value={opt.id}>
{opt.title}
{certResolverOptions.map(
(
opt
) => (
<SelectItem
key={
opt.id
}
value={
opt.id
}
>
{
opt.title
}
</SelectItem>
))}
)
)}
</SelectContent>
</Select>
</FormControl>
@@ -328,8 +394,10 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
</FormItem>
)}
/>
{form.watch("certResolver") !== null &&
form.watch("certResolver") !== "default" && (
{form.watch("certResolver") !==
null &&
form.watch("certResolver") !==
"default" && (
<FormField
control={form.control}
name="certResolver"
@@ -337,9 +405,22 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
<FormItem>
<FormControl>
<Input
placeholder={t("enterCustomResolver")}
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
placeholder={t(
"enterCustomResolver"
)}
value={
field.value ||
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
)
}
/>
</FormControl>
<FormMessage />
@@ -348,25 +429,39 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
/>
)}
{form.watch("certResolver") !== null &&
form.watch("certResolver") !== "default" && (
{form.watch("certResolver") !==
null &&
form.watch("certResolver") !==
"default" && (
<FormField
control={form.control}
name="preferWildcardCert"
render={({ field: switchField }) => (
render={({
field: switchField
}) => (
<FormItem className="items-center space-y-2 mt-4">
<FormControl>
<div className="flex items-center space-x-2">
<Switch
checked={switchField.value}
onCheckedChange={switchField.onChange}
checked={
switchField.value
}
onCheckedChange={
switchField.onChange
}
/>
<FormLabel>{t("preferWildcardCert")}</FormLabel>
<FormLabel>
{t(
"preferWildcardCert"
)}
</FormLabel>
</div>
</FormControl>
<FormDescription>
{t("preferWildcardCertDescription")}
{t(
"preferWildcardCertDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>