Compare commits
9 Commits
v1.0-RC
...
copilot/ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c78a8e8f6 | ||
|
|
2c70f2859a | ||
|
|
60633bf6c3 | ||
|
|
a520717aab | ||
|
|
8f4c24119e | ||
|
|
390840dbd9 | ||
|
|
3a4807b5cd | ||
|
|
0c632811b4 | ||
|
|
81be14e95e |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -17,8 +17,8 @@ updates:
|
|||||||
prefix: "ci"
|
prefix: "ci"
|
||||||
include: "scope"
|
include: "scope"
|
||||||
|
|
||||||
# NPM dependencies updates
|
# Bun dependencies updates
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "bun"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|||||||
32
.github/workflows/dependabot-automerge.yml
vendored
Normal file
32
.github/workflows/dependabot-automerge.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Dependabot auto-merge
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automerge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
steps:
|
||||||
|
- name: Fetch Dependabot metadata
|
||||||
|
id: metadata
|
||||||
|
uses: dependabot/fetch-metadata@v2
|
||||||
|
|
||||||
|
- name: Auto-approve the PR
|
||||||
|
run: gh pr review --approve "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Enable auto-merge
|
||||||
|
run: gh pr merge --auto --squash "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -77,6 +77,7 @@ function parseAuthentikConfig(formData: FormData): ProxyHostAuthentikInput | und
|
|||||||
const copyHeaders = parseCsv(formData.get("authentik_copy_headers"));
|
const copyHeaders = parseCsv(formData.get("authentik_copy_headers"));
|
||||||
const trustedProxies = parseCsv(formData.get("authentik_trusted_proxies"));
|
const trustedProxies = parseCsv(formData.get("authentik_trusted_proxies"));
|
||||||
const protectedPaths = parseCsv(formData.get("authentik_protected_paths"));
|
const protectedPaths = parseCsv(formData.get("authentik_protected_paths"));
|
||||||
|
const excludedPaths = parseCsv(formData.get("authentik_excluded_paths"));
|
||||||
const setHostHeader = formData.has("authentik_set_host_header_present")
|
const setHostHeader = formData.has("authentik_set_host_header_present")
|
||||||
? parseCheckbox(formData.get("authentik_set_host_header"))
|
? parseCheckbox(formData.get("authentik_set_host_header"))
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -103,6 +104,9 @@ function parseAuthentikConfig(formData: FormData): ProxyHostAuthentikInput | und
|
|||||||
if (protectedPaths.length > 0 || formData.has("authentik_protected_paths")) {
|
if (protectedPaths.length > 0 || formData.has("authentik_protected_paths")) {
|
||||||
result.protectedPaths = protectedPaths;
|
result.protectedPaths = protectedPaths;
|
||||||
}
|
}
|
||||||
|
if (excludedPaths.length > 0 || formData.has("authentik_excluded_paths")) {
|
||||||
|
result.excludedPaths = excludedPaths;
|
||||||
|
}
|
||||||
if (setHostHeader !== undefined) {
|
if (setHostHeader !== undefined) {
|
||||||
result.setOutpostHostHeader = setHostHeader;
|
result.setOutpostHostHeader = setHostHeader;
|
||||||
}
|
}
|
||||||
@@ -122,6 +126,7 @@ function parseCpmForwardAuthConfig(formData: FormData): CpmForwardAuthInput | un
|
|||||||
: false
|
: false
|
||||||
: undefined;
|
: undefined;
|
||||||
const protectedPaths = parseCsv(formData.get("cpm_forward_auth_protected_paths"));
|
const protectedPaths = parseCsv(formData.get("cpm_forward_auth_protected_paths"));
|
||||||
|
const excludedPaths = parseCsv(formData.get("cpm_forward_auth_excluded_paths"));
|
||||||
|
|
||||||
const result: CpmForwardAuthInput = {};
|
const result: CpmForwardAuthInput = {};
|
||||||
if (enabledValue !== undefined) {
|
if (enabledValue !== undefined) {
|
||||||
@@ -130,6 +135,9 @@ function parseCpmForwardAuthConfig(formData: FormData): CpmForwardAuthInput | un
|
|||||||
if (protectedPaths.length > 0 || formData.has("cpm_forward_auth_protected_paths")) {
|
if (protectedPaths.length > 0 || formData.has("cpm_forward_auth_protected_paths")) {
|
||||||
result.protected_paths = protectedPaths.length > 0 ? protectedPaths : null;
|
result.protected_paths = protectedPaths.length > 0 ? protectedPaths : null;
|
||||||
}
|
}
|
||||||
|
if (excludedPaths.length > 0 || formData.has("cpm_forward_auth_excluded_paths")) {
|
||||||
|
result.excluded_paths = excludedPaths.length > 0 ? excludedPaths : null;
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(result).length > 0 ? result : undefined;
|
return Object.keys(result).length > 0 ? result : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ import type {
|
|||||||
MetricsSettings,
|
MetricsSettings,
|
||||||
LoggingSettings,
|
LoggingSettings,
|
||||||
DnsSettings,
|
DnsSettings,
|
||||||
|
DnsProviderSettings,
|
||||||
UpstreamDnsResolutionSettings,
|
UpstreamDnsResolutionSettings,
|
||||||
GeoBlockSettings,
|
GeoBlockSettings,
|
||||||
} from "@/lib/settings";
|
} from "@/lib/settings";
|
||||||
|
import type { DnsProviderDefinition } from "@/src/lib/dns-providers";
|
||||||
import { GeoBlockFields } from "@/components/proxy-hosts/GeoBlockFields";
|
import { GeoBlockFields } from "@/components/proxy-hosts/GeoBlockFields";
|
||||||
import OAuthProvidersSection from "./OAuthProvidersSection";
|
import OAuthProvidersSection from "./OAuthProvidersSection";
|
||||||
import type { OAuthProvider } from "@/src/lib/models/oauth-providers";
|
import type { OAuthProvider } from "@/src/lib/models/oauth-providers";
|
||||||
import {
|
import {
|
||||||
updateCloudflareSettingsAction,
|
updateDnsProviderSettingsAction,
|
||||||
updateGeneralSettingsAction,
|
updateGeneralSettingsAction,
|
||||||
updateAuthentikSettingsAction,
|
updateAuthentikSettingsAction,
|
||||||
updateMetricsSettingsAction,
|
updateMetricsSettingsAction,
|
||||||
@@ -112,7 +114,7 @@ function SettingSection({
|
|||||||
const A: Record<string, AccentConfig> = {
|
const A: Record<string, AccentConfig> = {
|
||||||
sync: { border: "border-l-violet-500", icon: "border-violet-500/30 bg-violet-500/10 text-violet-500" },
|
sync: { border: "border-l-violet-500", icon: "border-violet-500/30 bg-violet-500/10 text-violet-500" },
|
||||||
general: { border: "border-l-zinc-400", icon: "border-zinc-500/30 bg-zinc-500/10 text-zinc-500" },
|
general: { border: "border-l-zinc-400", icon: "border-zinc-500/30 bg-zinc-500/10 text-zinc-500" },
|
||||||
cloudflare: { border: "border-l-orange-500", icon: "border-orange-500/30 bg-orange-500/10 text-orange-500" },
|
dnsProvider:{ border: "border-l-orange-500", icon: "border-orange-500/30 bg-orange-500/10 text-orange-500" },
|
||||||
dns: { border: "border-l-cyan-500", icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500" },
|
dns: { border: "border-l-cyan-500", icon: "border-cyan-500/30 bg-cyan-500/10 text-cyan-500" },
|
||||||
upstreamDns:{ border: "border-l-emerald-500", icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500" },
|
upstreamDns:{ border: "border-l-emerald-500", icon: "border-emerald-500/30 bg-emerald-500/10 text-emerald-500" },
|
||||||
authentik: { border: "border-l-purple-500", icon: "border-purple-500/30 bg-purple-500/10 text-purple-500" },
|
authentik: { border: "border-l-purple-500", icon: "border-purple-500/30 bg-purple-500/10 text-purple-500" },
|
||||||
@@ -126,11 +128,8 @@ const A: Record<string, AccentConfig> = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
general: GeneralSettings | null;
|
general: GeneralSettings | null;
|
||||||
cloudflare: {
|
dnsProvider: DnsProviderSettings | null;
|
||||||
hasToken: boolean;
|
dnsProviderDefinitions: DnsProviderDefinition[];
|
||||||
zoneId?: string;
|
|
||||||
accountId?: string;
|
|
||||||
};
|
|
||||||
authentik: AuthentikSettings | null;
|
authentik: AuthentikSettings | null;
|
||||||
metrics: MetricsSettings | null;
|
metrics: MetricsSettings | null;
|
||||||
logging: LoggingSettings | null;
|
logging: LoggingSettings | null;
|
||||||
@@ -145,7 +144,7 @@ type Props = {
|
|||||||
tokenFromEnv: boolean;
|
tokenFromEnv: boolean;
|
||||||
overrides: {
|
overrides: {
|
||||||
general: boolean;
|
general: boolean;
|
||||||
cloudflare: boolean;
|
dnsProvider: boolean;
|
||||||
authentik: boolean;
|
authentik: boolean;
|
||||||
metrics: boolean;
|
metrics: boolean;
|
||||||
logging: boolean;
|
logging: boolean;
|
||||||
@@ -178,7 +177,8 @@ type Props = {
|
|||||||
|
|
||||||
export default function SettingsClient({
|
export default function SettingsClient({
|
||||||
general,
|
general,
|
||||||
cloudflare,
|
dnsProvider,
|
||||||
|
dnsProviderDefinitions,
|
||||||
authentik,
|
authentik,
|
||||||
metrics,
|
metrics,
|
||||||
logging,
|
logging,
|
||||||
@@ -190,7 +190,9 @@ export default function SettingsClient({
|
|||||||
instanceSync
|
instanceSync
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null);
|
const [generalState, generalFormAction] = useFormState(updateGeneralSettingsAction, null);
|
||||||
const [cloudflareState, cloudflareFormAction] = useFormState(updateCloudflareSettingsAction, null);
|
const [dnsProviderState, dnsProviderFormAction] = useFormState(updateDnsProviderSettingsAction, null);
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState("none");
|
||||||
|
const configuredProviders = dnsProvider?.providers ? Object.keys(dnsProvider.providers) : [];
|
||||||
const [authentikState, authentikFormAction] = useFormState(updateAuthentikSettingsAction, null);
|
const [authentikState, authentikFormAction] = useFormState(updateAuthentikSettingsAction, null);
|
||||||
const [metricsState, metricsFormAction] = useFormState(updateMetricsSettingsAction, null);
|
const [metricsState, metricsFormAction] = useFormState(updateMetricsSettingsAction, null);
|
||||||
const [loggingState, loggingFormAction] = useFormState(updateLoggingSettingsAction, null);
|
const [loggingState, loggingFormAction] = useFormState(updateLoggingSettingsAction, null);
|
||||||
@@ -207,7 +209,7 @@ export default function SettingsClient({
|
|||||||
const isSlave = instanceSync.mode === "slave";
|
const isSlave = instanceSync.mode === "slave";
|
||||||
const isMaster = instanceSync.mode === "master";
|
const isMaster = instanceSync.mode === "master";
|
||||||
const [generalOverride, setGeneralOverride] = useState(instanceSync.overrides.general);
|
const [generalOverride, setGeneralOverride] = useState(instanceSync.overrides.general);
|
||||||
const [cloudflareOverride, setCloudflareOverride] = useState(instanceSync.overrides.cloudflare);
|
const [dnsProviderOverride, setDnsProviderOverride] = useState(instanceSync.overrides.dnsProvider);
|
||||||
const [authentikOverride, setAuthentikOverride] = useState(instanceSync.overrides.authentik);
|
const [authentikOverride, setAuthentikOverride] = useState(instanceSync.overrides.authentik);
|
||||||
const [metricsOverride, setMetricsOverride] = useState(instanceSync.overrides.metrics);
|
const [metricsOverride, setMetricsOverride] = useState(instanceSync.overrides.metrics);
|
||||||
const [loggingOverride, setLoggingOverride] = useState(instanceSync.overrides.logging);
|
const [loggingOverride, setLoggingOverride] = useState(instanceSync.overrides.logging);
|
||||||
@@ -463,65 +465,159 @@ export default function SettingsClient({
|
|||||||
</form>
|
</form>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
{/* ── Cloudflare DNS ── */}
|
{/* ── DNS Providers ── */}
|
||||||
<SettingSection
|
<SettingSection
|
||||||
icon={<Cloud className="h-4 w-4" />}
|
icon={<Cloud className="h-4 w-4" />}
|
||||||
title="Cloudflare DNS"
|
title="DNS Providers"
|
||||||
description="Configure a Cloudflare API token with Zone.DNS Edit permissions to enable DNS-01 challenges for wildcard certificates."
|
description="Configure DNS providers for ACME DNS-01 challenges (required for wildcard certificates). You can add multiple providers and select a default."
|
||||||
accent={A.cloudflare}
|
accent={A.dnsProvider}
|
||||||
>
|
>
|
||||||
{cloudflare.hasToken && (
|
{dnsProviderState?.message && (
|
||||||
<InfoAlert>
|
<StatusAlert message={dnsProviderState.message} success={dnsProviderState.success} />
|
||||||
A Cloudflare API token is already configured. Leave the token field blank to keep it, or select “Remove existing token” to delete it.
|
|
||||||
</InfoAlert>
|
|
||||||
)}
|
)}
|
||||||
<form action={cloudflareFormAction} className="flex flex-col gap-3">
|
{isSlave && (
|
||||||
{cloudflareState?.message && (
|
|
||||||
<StatusAlert message={cloudflareState.message} success={cloudflareState.success} />
|
|
||||||
)}
|
|
||||||
{isSlave && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="cloudflare-override"
|
|
||||||
name="overrideEnabled"
|
|
||||||
checked={cloudflareOverride}
|
|
||||||
onCheckedChange={(v) => setCloudflareOverride(!!v)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="cloudflare-override">Override master settings</Label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<Label htmlFor="cf-apiToken">API token</Label>
|
|
||||||
<Input
|
|
||||||
id="cf-apiToken"
|
|
||||||
name="apiToken"
|
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
placeholder="Enter new token"
|
|
||||||
disabled={isSlave && !cloudflareOverride}
|
|
||||||
className="h-8 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="cf-clearToken"
|
id="dnsprovider-override"
|
||||||
name="clearToken"
|
name="overrideEnabled"
|
||||||
disabled={!cloudflare.hasToken || (isSlave && !cloudflareOverride)}
|
form="dnsp-add-form"
|
||||||
|
checked={dnsProviderOverride}
|
||||||
|
onCheckedChange={(v) => setDnsProviderOverride(!!v)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="cf-clearToken">Remove existing token</Label>
|
<Label htmlFor="dnsprovider-override">Override master settings</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
)}
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<Label htmlFor="cf-zoneId">Zone ID</Label>
|
{/* Configured providers list */}
|
||||||
<Input id="cf-zoneId" name="zoneId" defaultValue={cloudflare.zoneId ?? ""} disabled={isSlave && !cloudflareOverride} className="h-8 text-sm font-mono" />
|
{configuredProviders.length > 0 && (
|
||||||
</div>
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-1.5">
|
<Label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Configured providers</Label>
|
||||||
<Label htmlFor="cf-accountId">Account ID</Label>
|
{configuredProviders.map((name) => {
|
||||||
<Input id="cf-accountId" name="accountId" defaultValue={cloudflare.accountId ?? ""} disabled={isSlave && !cloudflareOverride} className="h-8 text-sm font-mono" />
|
const def = dnsProviderDefinitions.find((p) => p.name === name);
|
||||||
</div>
|
const isDefault = dnsProvider?.default === name;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-md border px-4 py-2.5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-semibold">{def?.displayName ?? name}</span>
|
||||||
|
{isDefault && <StatusChip status="active" label="Default" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!isDefault && (
|
||||||
|
<form action={dnsProviderFormAction}>
|
||||||
|
<input type="hidden" name="action" value="set-default" />
|
||||||
|
<input type="hidden" name="provider" value={name} />
|
||||||
|
{isSlave && <input type="hidden" name="overrideEnabled" value={dnsProviderOverride ? "on" : ""} />}
|
||||||
|
<Button type="submit" variant="outline" size="sm" className="text-emerald-600 border-emerald-500/50">
|
||||||
|
Set default
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<form action={dnsProviderFormAction}>
|
||||||
|
<input type="hidden" name="action" value="remove" />
|
||||||
|
<input type="hidden" name="provider" value={name} />
|
||||||
|
{isSlave && <input type="hidden" name="overrideEnabled" value={dnsProviderOverride ? "on" : ""} />}
|
||||||
|
<Button type="submit" variant="outline" size="sm" className="text-destructive border-destructive/50">
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{dnsProvider?.default && (
|
||||||
|
<form action={dnsProviderFormAction}>
|
||||||
|
<input type="hidden" name="action" value="set-default" />
|
||||||
|
<input type="hidden" name="provider" value="none" />
|
||||||
|
{isSlave && <input type="hidden" name="overrideEnabled" value={dnsProviderOverride ? "on" : ""} />}
|
||||||
|
<Button type="submit" variant="ghost" size="sm" className="text-xs text-muted-foreground">
|
||||||
|
Clear default (HTTP-01 only)
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add / update provider form */}
|
||||||
|
<form id="dnsp-add-form" action={dnsProviderFormAction} className="flex flex-col gap-3">
|
||||||
|
<input type="hidden" name="action" value="save" />
|
||||||
|
<Label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
{configuredProviders.length > 0 ? "Add or update provider" : "Add a provider"}
|
||||||
|
</Label>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label htmlFor="dns-provider-select">Provider</Label>
|
||||||
|
<Select
|
||||||
|
name="provider"
|
||||||
|
value={selectedProvider}
|
||||||
|
onValueChange={setSelectedProvider}
|
||||||
|
disabled={isSlave && !dnsProviderOverride}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="dns-provider-select">
|
||||||
|
<SelectValue placeholder="Select a DNS provider..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">Select...</SelectItem>
|
||||||
|
{dnsProviderDefinitions.map((p) => (
|
||||||
|
<SelectItem key={p.name} value={p.name}>
|
||||||
|
{p.displayName}{configuredProviders.includes(p.name) ? " (update)" : ""}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic credential fields */}
|
||||||
|
{selectedProvider && selectedProvider !== "none" && (() => {
|
||||||
|
const providerDef = dnsProviderDefinitions.find((p) => p.name === selectedProvider);
|
||||||
|
if (!providerDef) return null;
|
||||||
|
const isUpdate = configuredProviders.includes(selectedProvider);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{providerDef.description && (
|
||||||
|
<p className="text-xs text-muted-foreground">{providerDef.description}</p>
|
||||||
|
)}
|
||||||
|
{providerDef.fields.map((field) => (
|
||||||
|
<div key={field.key} className="flex flex-col gap-1.5">
|
||||||
|
<Label htmlFor={`dnsp-${field.key}`} className="text-xs">
|
||||||
|
{field.label}{field.required ? "" : " (optional)"}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`dnsp-${field.key}`}
|
||||||
|
name={`credential_${field.key}`}
|
||||||
|
type={field.type === "password" ? "password" : "text"}
|
||||||
|
autoComplete={field.type === "password" ? "new-password" : "off"}
|
||||||
|
placeholder={field.placeholder ?? ""}
|
||||||
|
disabled={isSlave && !dnsProviderOverride}
|
||||||
|
className="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
{field.description && (
|
||||||
|
<p className="text-xs text-muted-foreground">{field.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isUpdate && (
|
||||||
|
<InfoAlert>
|
||||||
|
Credentials are already configured. Leave fields blank to keep existing values.
|
||||||
|
</InfoAlert>
|
||||||
|
)}
|
||||||
|
{providerDef.docsUrl && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
<a href={providerDef.docsUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
Provider documentation
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{isSlave && <input type="hidden" name="overrideEnabled" value={dnsProviderOverride ? "on" : ""} />}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" size="sm">Save Cloudflare settings</Button>
|
<Button type="submit" size="sm" disabled={!selectedProvider || selectedProvider === "none"}>
|
||||||
|
{selectedProvider && selectedProvider !== "none" && configuredProviders.includes(selectedProvider) ? "Update provider" : "Add provider"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { requireAdmin } from "@/src/lib/auth";
|
|||||||
import { applyCaddyConfig } from "@/src/lib/caddy";
|
import { applyCaddyConfig } from "@/src/lib/caddy";
|
||||||
import { getInstanceMode, getSlaveMasterToken, setInstanceMode, setSlaveMasterToken, syncInstances } from "@/src/lib/instance-sync";
|
import { getInstanceMode, getSlaveMasterToken, setInstanceMode, setSlaveMasterToken, syncInstances } from "@/src/lib/instance-sync";
|
||||||
import { createInstance, deleteInstance, updateInstance } from "@/src/lib/models/instances";
|
import { createInstance, deleteInstance, updateInstance } from "@/src/lib/models/instances";
|
||||||
import { clearSetting, getSetting, saveCloudflareSettings, saveGeneralSettings, saveAuthentikSettings, saveMetricsSettings, saveLoggingSettings, saveDnsSettings, saveUpstreamDnsResolutionSettings, saveGeoBlockSettings, saveWafSettings, getWafSettings } from "@/src/lib/settings";
|
import { clearSetting, getSetting, saveCloudflareSettings, getDnsProviderSettings, saveDnsProviderSettings, saveGeneralSettings, saveAuthentikSettings, saveMetricsSettings, saveLoggingSettings, saveDnsSettings, saveUpstreamDnsResolutionSettings, saveGeoBlockSettings, saveWafSettings, getWafSettings } from "@/src/lib/settings";
|
||||||
import { listProxyHosts, updateProxyHost } from "@/src/lib/models/proxy-hosts";
|
import { listProxyHosts, updateProxyHost } from "@/src/lib/models/proxy-hosts";
|
||||||
import { getWafRuleMessages } from "@/src/lib/models/waf-events";
|
import { getWafRuleMessages } from "@/src/lib/models/waf-events";
|
||||||
import type { CloudflareSettings, GeoBlockSettings, WafSettings } from "@/src/lib/settings";
|
import type { CloudflareSettings, DnsProviderSettings, GeoBlockSettings, WafSettings } from "@/src/lib/settings";
|
||||||
|
import { getProviderDefinition, encryptProviderCredentials } from "@/src/lib/dns-providers";
|
||||||
|
|
||||||
type ActionResult = {
|
type ActionResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -113,6 +114,122 @@ export async function updateCloudflareSettingsAction(_prevState: ActionResult |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateDnsProviderSettingsAction(_prevState: ActionResult | null, formData: FormData): Promise<ActionResult> {
|
||||||
|
try {
|
||||||
|
await requireAdmin();
|
||||||
|
const mode = await getInstanceMode();
|
||||||
|
const overrideEnabled = formData.get("overrideEnabled") === "on";
|
||||||
|
if (mode === "slave" && !overrideEnabled) {
|
||||||
|
await clearSetting("dns_provider");
|
||||||
|
try {
|
||||||
|
await applyCaddyConfig();
|
||||||
|
revalidatePath("/settings");
|
||||||
|
return { success: true, message: "DNS provider settings reset to master defaults" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to apply Caddy config:", error);
|
||||||
|
revalidatePath("/settings");
|
||||||
|
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
await syncInstances();
|
||||||
|
return { success: true, message: `Settings reset, but could not apply to Caddy: ${errorMsg}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = String(formData.get("action") ?? "save").trim();
|
||||||
|
const providerName = String(formData.get("provider") ?? "").trim();
|
||||||
|
const current = await getDnsProviderSettings();
|
||||||
|
const settings: DnsProviderSettings = current ?? { providers: {}, default: null };
|
||||||
|
|
||||||
|
if (action === "remove") {
|
||||||
|
if (!providerName || !settings.providers[providerName]) {
|
||||||
|
return { success: false, message: "No provider to remove" };
|
||||||
|
}
|
||||||
|
const def = getProviderDefinition(providerName);
|
||||||
|
delete settings.providers[providerName];
|
||||||
|
if (settings.default === providerName) {
|
||||||
|
// Pick next configured provider, or null
|
||||||
|
const remaining = Object.keys(settings.providers);
|
||||||
|
settings.default = remaining.length > 0 ? remaining[0] : null;
|
||||||
|
}
|
||||||
|
await saveDnsProviderSettings(settings);
|
||||||
|
await syncInstances();
|
||||||
|
try { await applyCaddyConfig(); } catch { /* non-fatal */ }
|
||||||
|
revalidatePath("/settings");
|
||||||
|
return { success: true, message: `${def?.displayName ?? providerName} removed${settings.default ? `. Default is now ${settings.default}.` : "."}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "set-default") {
|
||||||
|
const newDefault = providerName === "none" ? null : providerName;
|
||||||
|
if (newDefault && !settings.providers[newDefault]) {
|
||||||
|
return { success: false, message: `Cannot set default: ${providerName} is not configured` };
|
||||||
|
}
|
||||||
|
settings.default = newDefault;
|
||||||
|
await saveDnsProviderSettings(settings);
|
||||||
|
await syncInstances();
|
||||||
|
try { await applyCaddyConfig(); } catch { /* non-fatal */ }
|
||||||
|
revalidatePath("/settings");
|
||||||
|
const label = newDefault ? (getProviderDefinition(newDefault)?.displayName ?? newDefault) : "None";
|
||||||
|
return { success: true, message: `Default DNS provider set to ${label}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// action === "save": add or update a provider's credentials
|
||||||
|
if (!providerName || providerName === "none") {
|
||||||
|
return { success: false, message: "Select a provider to configure" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const def = getProviderDefinition(providerName);
|
||||||
|
if (!def) {
|
||||||
|
return { success: false, message: `Unknown DNS provider: ${providerName}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingCreds = settings.providers[providerName];
|
||||||
|
|
||||||
|
// Collect credentials from form
|
||||||
|
const credentials: Record<string, string> = {};
|
||||||
|
for (const field of def.fields) {
|
||||||
|
const rawValue = formData.get(`credential_${field.key}`);
|
||||||
|
const value = rawValue ? String(rawValue).trim() : "";
|
||||||
|
if (value) {
|
||||||
|
credentials[field.key] = value;
|
||||||
|
} else if (existingCreds?.[field.key]) {
|
||||||
|
credentials[field.key] = existingCreds[field.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
for (const field of def.fields) {
|
||||||
|
if (field.required && !credentials[field.key]) {
|
||||||
|
return { success: false, message: `${field.label} is required for ${def.displayName}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt password fields before storing
|
||||||
|
settings.providers[providerName] = encryptProviderCredentials(providerName, credentials);
|
||||||
|
|
||||||
|
// If this is the first provider, make it the default
|
||||||
|
if (!settings.default) {
|
||||||
|
settings.default = providerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveDnsProviderSettings(settings);
|
||||||
|
await syncInstances();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await applyCaddyConfig();
|
||||||
|
revalidatePath("/settings");
|
||||||
|
const isDefault = settings.default === providerName;
|
||||||
|
return { success: true, message: `${def.displayName} saved${isDefault ? " (default)" : ""}` };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to apply Caddy config:", error);
|
||||||
|
revalidatePath("/settings");
|
||||||
|
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
return { success: true, message: `Settings saved, but could not apply to Caddy: ${errorMsg}` };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save DNS provider settings:", error);
|
||||||
|
return { success: false, message: error instanceof Error ? error.message : "Failed to save DNS provider settings" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateAuthentikSettingsAction(_prevState: ActionResult | null, formData: FormData): Promise<ActionResult> {
|
export async function updateAuthentikSettingsAction(_prevState: ActionResult | null, formData: FormData): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
await requireAdmin();
|
await requireAdmin();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import SettingsClient from "./SettingsClient";
|
import SettingsClient from "./SettingsClient";
|
||||||
import { getCloudflareSettings, getGeneralSettings, getAuthentikSettings, getMetricsSettings, getLoggingSettings, getDnsSettings, getSetting, getUpstreamDnsResolutionSettings, getGeoBlockSettings } from "@/src/lib/settings";
|
import { getGeneralSettings, getAuthentikSettings, getMetricsSettings, getLoggingSettings, getDnsSettings, getDnsProviderSettings, getSetting, getUpstreamDnsResolutionSettings, getGeoBlockSettings } from "@/src/lib/settings";
|
||||||
import { getInstanceMode, getSlaveLastSync, getSlaveMasterToken, isInstanceModeFromEnv, isSyncTokenFromEnv, getEnvSlaveInstances } from "@/src/lib/instance-sync";
|
import { getInstanceMode, getSlaveLastSync, getSlaveMasterToken, isInstanceModeFromEnv, isSyncTokenFromEnv, getEnvSlaveInstances } from "@/src/lib/instance-sync";
|
||||||
import { listInstances } from "@/src/lib/models/instances";
|
import { listInstances } from "@/src/lib/models/instances";
|
||||||
import { listOAuthProviders } from "@/src/lib/models/oauth-providers";
|
import { listOAuthProviders } from "@/src/lib/models/oauth-providers";
|
||||||
|
import { DNS_PROVIDERS } from "@/src/lib/dns-providers";
|
||||||
import { config } from "@/src/lib/config";
|
import { config } from "@/src/lib/config";
|
||||||
import { requireAdmin } from "@/src/lib/auth";
|
import { requireAdmin } from "@/src/lib/auth";
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ export default async function SettingsPage() {
|
|||||||
const modeFromEnv = isInstanceModeFromEnv();
|
const modeFromEnv = isInstanceModeFromEnv();
|
||||||
const tokenFromEnv = isSyncTokenFromEnv();
|
const tokenFromEnv = isSyncTokenFromEnv();
|
||||||
|
|
||||||
const [general, cloudflare, authentik, metrics, logging, dns, upstreamDnsResolution, instanceMode, globalGeoBlock, oauthProviders] = await Promise.all([
|
const [general, dnsProvider, authentik, metrics, logging, dns, upstreamDnsResolution, instanceMode, globalGeoBlock, oauthProviders] = await Promise.all([
|
||||||
getGeneralSettings(),
|
getGeneralSettings(),
|
||||||
getCloudflareSettings(),
|
getDnsProviderSettings(),
|
||||||
getAuthentikSettings(),
|
getAuthentikSettings(),
|
||||||
getMetricsSettings(),
|
getMetricsSettings(),
|
||||||
getLoggingSettings(),
|
getLoggingSettings(),
|
||||||
@@ -26,11 +27,11 @@ export default async function SettingsPage() {
|
|||||||
listOAuthProviders(),
|
listOAuthProviders(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [overrideGeneral, overrideCloudflare, overrideAuthentik, overrideMetrics, overrideLogging, overrideDns, overrideUpstreamDnsResolution] =
|
const [overrideGeneral, overrideDnsProvider, overrideAuthentik, overrideMetrics, overrideLogging, overrideDns, overrideUpstreamDnsResolution] =
|
||||||
instanceMode === "slave"
|
instanceMode === "slave"
|
||||||
? await Promise.all([
|
? await Promise.all([
|
||||||
getSetting("general"),
|
getSetting("general"),
|
||||||
getSetting("cloudflare"),
|
getSetting("dns_provider"),
|
||||||
getSetting("authentik"),
|
getSetting("authentik"),
|
||||||
getSetting("metrics"),
|
getSetting("metrics"),
|
||||||
getSetting("logging"),
|
getSetting("logging"),
|
||||||
@@ -49,11 +50,8 @@ export default async function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<SettingsClient
|
<SettingsClient
|
||||||
general={general}
|
general={general}
|
||||||
cloudflare={{
|
dnsProvider={dnsProvider}
|
||||||
hasToken: Boolean(cloudflare?.apiToken),
|
dnsProviderDefinitions={DNS_PROVIDERS}
|
||||||
zoneId: cloudflare?.zoneId,
|
|
||||||
accountId: cloudflare?.accountId
|
|
||||||
}}
|
|
||||||
authentik={authentik}
|
authentik={authentik}
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
logging={logging}
|
logging={logging}
|
||||||
@@ -68,7 +66,7 @@ export default async function SettingsPage() {
|
|||||||
tokenFromEnv,
|
tokenFromEnv,
|
||||||
overrides: {
|
overrides: {
|
||||||
general: overrideGeneral !== null,
|
general: overrideGeneral !== null,
|
||||||
cloudflare: overrideCloudflare !== null,
|
dnsProvider: overrideDnsProvider !== null,
|
||||||
authentik: overrideAuthentik !== null,
|
authentik: overrideAuthentik !== null,
|
||||||
metrics: overrideMetrics !== null,
|
metrics: overrideMetrics !== null,
|
||||||
logging: overrideLogging !== null,
|
logging: overrideLogging !== null,
|
||||||
|
|||||||
30
app/api/v1/dns-providers/route.ts
Normal file
30
app/api/v1/dns-providers/route.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { requireApiUser, apiErrorResponse } from "@/src/lib/api-auth";
|
||||||
|
import { DNS_PROVIDERS } from "@/src/lib/dns-providers";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
await requireApiUser(request);
|
||||||
|
|
||||||
|
// Return provider definitions without any credential values
|
||||||
|
const providers = DNS_PROVIDERS.map(({ name, displayName, description, docsUrl, fields, modulePath }) => ({
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
docsUrl,
|
||||||
|
modulePath,
|
||||||
|
fields: fields.map(({ key, label, type, placeholder, description, required }) => ({
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
placeholder,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json(providers);
|
||||||
|
} catch (error) {
|
||||||
|
return apiErrorResponse(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -784,6 +784,7 @@ const spec = {
|
|||||||
enum: [
|
enum: [
|
||||||
"general",
|
"general",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
|
"dns-provider",
|
||||||
"authentik",
|
"authentik",
|
||||||
"metrics",
|
"metrics",
|
||||||
"logging",
|
"logging",
|
||||||
@@ -807,6 +808,7 @@ const spec = {
|
|||||||
oneOf: [
|
oneOf: [
|
||||||
{ $ref: "#/components/schemas/GeneralSettings" },
|
{ $ref: "#/components/schemas/GeneralSettings" },
|
||||||
{ $ref: "#/components/schemas/CloudflareSettings" },
|
{ $ref: "#/components/schemas/CloudflareSettings" },
|
||||||
|
{ $ref: "#/components/schemas/DnsProviderSettings" },
|
||||||
{ $ref: "#/components/schemas/AuthentikSettings" },
|
{ $ref: "#/components/schemas/AuthentikSettings" },
|
||||||
{ $ref: "#/components/schemas/MetricsSettings" },
|
{ $ref: "#/components/schemas/MetricsSettings" },
|
||||||
{ $ref: "#/components/schemas/LoggingSettings" },
|
{ $ref: "#/components/schemas/LoggingSettings" },
|
||||||
@@ -836,6 +838,7 @@ const spec = {
|
|||||||
enum: [
|
enum: [
|
||||||
"general",
|
"general",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
|
"dns-provider",
|
||||||
"authentik",
|
"authentik",
|
||||||
"metrics",
|
"metrics",
|
||||||
"logging",
|
"logging",
|
||||||
@@ -1448,6 +1451,7 @@ const spec = {
|
|||||||
trustedProxies: { type: "array", items: { type: "string" }, example: ["private_ranges"] },
|
trustedProxies: { type: "array", items: { type: "string" }, example: ["private_ranges"] },
|
||||||
setOutpostHostHeader: { type: "boolean" },
|
setOutpostHostHeader: { type: "boolean" },
|
||||||
protectedPaths: { type: ["array", "null"], items: { type: "string" }, description: "Paths to protect (null = all)" },
|
protectedPaths: { type: ["array", "null"], items: { type: "string" }, description: "Paths to protect (null = all)" },
|
||||||
|
excludedPaths: { type: ["array", "null"], items: { type: "string" }, description: "Paths to exclude from auth (bypassed while rest is protected)" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
LoadBalancerConfig: {
|
LoadBalancerConfig: {
|
||||||
@@ -1863,6 +1867,27 @@ const spec = {
|
|||||||
},
|
},
|
||||||
required: ["apiToken"],
|
required: ["apiToken"],
|
||||||
},
|
},
|
||||||
|
DnsProviderSettings: {
|
||||||
|
type: "object",
|
||||||
|
description: "DNS provider configuration for ACME DNS-01 challenges. Supports multiple configured providers with a default.",
|
||||||
|
properties: {
|
||||||
|
providers: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: { type: "string" },
|
||||||
|
description: "Credential key-value pairs for this provider",
|
||||||
|
},
|
||||||
|
description: "Configured providers keyed by name (e.g. { cloudflare: { api_token: '...' }, route53: { ... } })",
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
description: "Name of the default provider used for DNS-01 challenges (null = HTTP-01 only)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["providers", "default"],
|
||||||
|
},
|
||||||
AuthentikSettings: {
|
AuthentikSettings: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getMetricsSettings, saveMetricsSettings,
|
getMetricsSettings, saveMetricsSettings,
|
||||||
getLoggingSettings, saveLoggingSettings,
|
getLoggingSettings, saveLoggingSettings,
|
||||||
getDnsSettings, saveDnsSettings,
|
getDnsSettings, saveDnsSettings,
|
||||||
|
getDnsProviderSettings, saveDnsProviderSettings,
|
||||||
getUpstreamDnsResolutionSettings, saveUpstreamDnsResolutionSettings,
|
getUpstreamDnsResolutionSettings, saveUpstreamDnsResolutionSettings,
|
||||||
getGeoBlockSettings, saveGeoBlockSettings,
|
getGeoBlockSettings, saveGeoBlockSettings,
|
||||||
getWafSettings, saveWafSettings,
|
getWafSettings, saveWafSettings,
|
||||||
@@ -27,6 +28,7 @@ const SETTINGS_HANDLERS: Record<string, SettingsHandler> = {
|
|||||||
metrics: { get: getMetricsSettings, save: saveMetricsSettings as (data: never) => Promise<void>, applyCaddy: true },
|
metrics: { get: getMetricsSettings, save: saveMetricsSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
logging: { get: getLoggingSettings, save: saveLoggingSettings as (data: never) => Promise<void>, applyCaddy: true },
|
logging: { get: getLoggingSettings, save: saveLoggingSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
dns: { get: getDnsSettings, save: saveDnsSettings as (data: never) => Promise<void>, applyCaddy: true },
|
dns: { get: getDnsSettings, save: saveDnsSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
|
"dns-provider": { get: getDnsProviderSettings, save: saveDnsProviderSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
"upstream-dns": { get: getUpstreamDnsResolutionSettings, save: saveUpstreamDnsResolutionSettings as (data: never) => Promise<void>, applyCaddy: true },
|
"upstream-dns": { get: getUpstreamDnsResolutionSettings, save: saveUpstreamDnsResolutionSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
geoblock: { get: getGeoBlockSettings, save: saveGeoBlockSettings as (data: never) => Promise<void>, applyCaddy: true },
|
geoblock: { get: getGeoBlockSettings, save: saveGeoBlockSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
waf: { get: getWafSettings, save: saveWafSettings as (data: never) => Promise<void>, applyCaddy: true },
|
waf: { get: getWafSettings, save: saveWafSettings as (data: never) => Promise<void>, applyCaddy: true },
|
||||||
|
|||||||
100
bun.lock
100
bun.lock
@@ -22,9 +22,9 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"apexcharts": "^5.10.6",
|
"apexcharts": "^5.10.6",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.5.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "^1.6.2",
|
"better-auth": "^1.6.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"maplibre-gl": "^5.22.0",
|
"maplibre-gl": "^5.23.0",
|
||||||
"maxmind": "^5.0.6",
|
"maxmind": "^5.0.6",
|
||||||
"next": "^16.2.3",
|
"next": "^16.2.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-forge": "^1.4.0",
|
"node-forge": "^1.4.0",
|
||||||
"postcss": "^8.5.9",
|
"postcss": "^8.5.10",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-apexcharts": "^2.1.0",
|
"react-apexcharts": "^2.1.0",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.14.0",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@next/eslint-plugin-next": "^16.2.3",
|
"@next/eslint-plugin-next": "^16.2.4",
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/bun": "^1.3.12",
|
"@types/bun": "^1.3.12",
|
||||||
@@ -62,12 +62,12 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/topojson-client": "^3.1.5",
|
"@types/topojson-client": "^3.1.5",
|
||||||
"@vitest/ui": "^4.1.4",
|
"@vitest/ui": "^4.1.4",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"eslint": "^10.2.0",
|
"eslint": "^10.2.0",
|
||||||
"shadcn": "^4.2.0",
|
"shadcn": "^4.2.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.1",
|
"typescript-eslint": "^8.58.2",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "^4.1.4",
|
"vitest": "^4.1.4",
|
||||||
"world-atlas": "^2.0.2",
|
"world-atlas": "^2.0.2",
|
||||||
@@ -133,19 +133,19 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
|
"@better-auth/core": ["@better-auth/core@1.6.4", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-G52PXrx+qQcwkmP5Kmjjl8kWl15DFaZF5H1n9jY7kuCJiVIJMnr4WTSGMIOt07OwD+CSy9b4IA5DpuADZC5znA=="],
|
||||||
|
|
||||||
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-KawrNNuhgmpcc5PgLs6HesMckxCscz5J+BQ99iRmU1cLzG/A87IcydrmYtep+K8WHPN0HmZ/i4z/nOBCtxE2qA=="],
|
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.4", "", { "peerDependencies": { "@better-auth/core": "^1.6.4", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-LYyFTTZD/VcSh59taYZ2V5clHP4+udQYYZlW0KTiLTzljZcmuETuflGB7++WKSdCJg7KtLQzoM0rMo9PgT/Prw=="],
|
||||||
|
|
||||||
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-YMMm75jek/MNCAFWTAaq/U3VPmFnrwZW4NhBjjAwruHQJEIrSZZaOaUEXuUpFRRBhWqg7OOltQcHMwU/45CkuA=="],
|
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.4", "", { "peerDependencies": { "@better-auth/core": "^1.6.4", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-CVuvhy81gs66oHjjMTVKV1bfVCPivJzf9za8xghGB9wrkeMaZnPVKVqDobHf8juDfT5XRMQJmrcGLxubI2le/A=="],
|
||||||
|
|
||||||
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0" } }, "sha512-QvuK5m7NFgkzLPHyab+NORu3J683nj36Tix58qq6DPcniyY6KZk5gY2yyh4+z1wgSjrxwY5NFx/DC2qz8B8NJg=="],
|
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.4", "", { "peerDependencies": { "@better-auth/core": "^1.6.4", "@better-auth/utils": "0.4.0" } }, "sha512-eV6jw1roUohNHRo4qdUXJdaRpNmyWqtC1ADC2AMSRF3f6d4XPW+FqtAYhE87ZBq7nWDVgjrtawoUSVHREc8PrQ=="],
|
||||||
|
|
||||||
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-IvR2Q+1pjzxA4JXI3ED76+6fsqervIpZ2K5MxoX/+miLQhLEmNcbqqcItg4O2kfkxN8h33/ev57sjTW8QH9Tuw=="],
|
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.4", "", { "peerDependencies": { "@better-auth/core": "^1.6.4", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-N2pjOSuZHeNeRuqCXuEzSN2WFqexzb7KbtuxHislkXMIQtUiMNhM8NJMxCqnVj4t22qryim4hv0K59QfqqGXWQ=="],
|
||||||
|
|
||||||
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-bQkXYTo1zPau+xAiMpo1yCjEDSy7i7oeYlkYO+fSfRDCo52DE/9oPOOuI+EStmFkPUNSk9L2rhk8Fulifi8WCg=="],
|
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.4", "", { "peerDependencies": { "@better-auth/core": "^1.6.4", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-h6zuYYI+p5yK1TLci0V9JKfIitThBDmRK88ipoZzmZH3EyWUQfXcgC05WwOMCuM3mH+A9Auu/hLiiM7/VRfJgw=="],
|
||||||
|
|
||||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-o4gHKXqizUxVUUYChZZTowLEzdsz3ViBE/fKFzfHqNFUnF+aVt8QsbLSfipq1WpTIXyJVT/SnH0hgSdWxdssbQ=="],
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.4", "", { "peerDependencies": { "@better-auth/core": "^1.6.4", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-x6ctfiHcdUshXvrAgGaAprxJI6obZP09BUmQn0LcQX4KYbx0B5M8+DToyVmv30iEL8rEmGyL187XamXbY4FwYw=="],
|
||||||
|
|
||||||
"@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
"@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
|
|
||||||
"@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="],
|
"@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="],
|
||||||
|
|
||||||
"@maplibre/geojson-vt": ["@maplibre/geojson-vt@6.0.4", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ=="],
|
"@maplibre/geojson-vt": ["@maplibre/geojson-vt@6.1.0", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ=="],
|
||||||
|
|
||||||
"@maplibre/maplibre-gl-style-spec": ["@maplibre/maplibre-gl-style-spec@24.8.1", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs", "gl-style-format": "dist/gl-style-format.mjs" } }, "sha512-zxa92qF96ZNojLxeAjnaRpjVCy+swoUNJvDhtpC90k7u5F0TMr4GmvNqMKvYrMoPB8d7gRSXbMG1hBbmgESIsw=="],
|
"@maplibre/maplibre-gl-style-spec": ["@maplibre/maplibre-gl-style-spec@24.8.1", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs", "gl-style-format": "dist/gl-style-format.mjs" } }, "sha512-zxa92qF96ZNojLxeAjnaRpjVCy+swoUNJvDhtpC90k7u5F0TMr4GmvNqMKvYrMoPB8d7gRSXbMG1hBbmgESIsw=="],
|
||||||
|
|
||||||
@@ -355,25 +355,25 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
|
"@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.3", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.4", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-tOX826JJ96gYK/go18sPUgMq9FK1tqxBFfUCEufJb5XIkWFFmpgU7mahJANKGkHs7F41ir3tReJ3Lv5La0RvhA=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="],
|
||||||
|
|
||||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ=="],
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q=="],
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw=="],
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ=="],
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw=="],
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="],
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw=="],
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="],
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="],
|
||||||
|
|
||||||
"@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="],
|
"@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="],
|
||||||
|
|
||||||
@@ -601,25 +601,25 @@
|
|||||||
|
|
||||||
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
"@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.1", "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g=="],
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="],
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw=="],
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w=="],
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="],
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.1", "@typescript-eslint/tsconfig-utils": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg=="],
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ=="],
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="],
|
||||||
|
|
||||||
"@vis.gl/react-mapbox": ["@vis.gl/react-mapbox@8.1.1", "", { "peerDependencies": { "mapbox-gl": ">=3.5.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl"] }, "sha512-KMDTjtWESXxHS4uqWxjsvgQUHvuL3Z6SdKe68o7Nxma2qUfuyH3x4TCkIqGn3FQTrFvZLWvTnSAbGvtm+Kd13A=="],
|
"@vis.gl/react-mapbox": ["@vis.gl/react-mapbox@8.1.1", "", { "peerDependencies": { "mapbox-gl": ">=3.5.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl"] }, "sha512-KMDTjtWESXxHS4uqWxjsvgQUHvuL3Z6SdKe68o7Nxma2qUfuyH3x4TCkIqGn3FQTrFvZLWvTnSAbGvtm+Kd13A=="],
|
||||||
|
|
||||||
@@ -671,21 +671,21 @@
|
|||||||
|
|
||||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||||
|
|
||||||
"autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="],
|
"autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="],
|
||||||
|
|
||||||
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
||||||
|
|
||||||
"better-auth": ["better-auth@1.6.2", "", { "dependencies": { "@better-auth/core": "1.6.2", "@better-auth/drizzle-adapter": "1.6.2", "@better-auth/kysely-adapter": "1.6.2", "@better-auth/memory-adapter": "1.6.2", "@better-auth/mongo-adapter": "1.6.2", "@better-auth/prisma-adapter": "1.6.2", "@better-auth/telemetry": "1.6.2", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-5nqDAIj5xexmnk+GjjdrBknJCabi1mlvsVWJbxs4usHreao4vNdxIxINWDzCyDF9iDR1ildRZdXWSiYPAvTHhA=="],
|
"better-auth": ["better-auth@1.6.4", "", { "dependencies": { "@better-auth/core": "1.6.4", "@better-auth/drizzle-adapter": "1.6.4", "@better-auth/kysely-adapter": "1.6.4", "@better-auth/memory-adapter": "1.6.4", "@better-auth/mongo-adapter": "1.6.4", "@better-auth/prisma-adapter": "1.6.4", "@better-auth/telemetry": "1.6.4", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-yb/IDzoheBcoP8vI4jB0KkGCg0UabvEKo8GDBjcHW/w2Bb9ltlHzWuwcIauowhPezDcyRWUr2ub2HNO2rp6p7A=="],
|
||||||
|
|
||||||
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
|
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
|
||||||
|
|
||||||
"better-sqlite3": ["better-sqlite3@12.8.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ=="],
|
"better-sqlite3": ["better-sqlite3@12.9.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ=="],
|
||||||
|
|
||||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||||
|
|
||||||
@@ -719,7 +719,7 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="],
|
||||||
|
|
||||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
@@ -1129,7 +1129,7 @@
|
|||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
"maplibre-gl": ["maplibre-gl@5.22.0", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.7", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@maplibre/geojson-vt": "^6.0.4", "@maplibre/maplibre-gl-style-spec": "^24.8.1", "@maplibre/mlt": "^1.1.8", "@maplibre/vt-pbf": "^4.3.0", "@types/geojson": "^7946.0.16", "earcut": "^3.0.2", "gl-matrix": "^3.4.4", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.1.0", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" } }, "sha512-nc8YA+YSEioMZg5W0cb6Cf3wQ8aJge66dsttyBgpOArOnlmFJO1Kc5G32kYVPeUYhLpBja83T99uanmJvYAIyQ=="],
|
"maplibre-gl": ["maplibre-gl@5.23.0", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.7", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@maplibre/geojson-vt": "^6.1.0", "@maplibre/maplibre-gl-style-spec": "^24.8.1", "@maplibre/mlt": "^1.1.8", "@maplibre/vt-pbf": "^4.3.0", "@types/geojson": "^7946.0.16", "earcut": "^3.0.2", "gl-matrix": "^3.4.4", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.1.0", "quickselect": "^3.0.0", "tinyqueue": "^3.0.0" } }, "sha512-aou8YBNFS8uVtDWFWt0W/6oorfl18wt+oIA8fnXk1kivjkbtXi9gGrQvflTpwrR3hG13aWdIdbYWeN0NFMV7ag=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
@@ -1183,7 +1183,7 @@
|
|||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"next": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="],
|
"next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="],
|
||||||
|
|
||||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
@@ -1255,7 +1255,7 @@
|
|||||||
|
|
||||||
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
|
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
|
||||||
|
|
||||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||||
|
|
||||||
@@ -1497,7 +1497,7 @@
|
|||||||
|
|
||||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.58.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.1", "@typescript-eslint/parser": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/utils": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg=="],
|
"typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="],
|
||||||
|
|
||||||
"typewise": ["typewise@1.0.3", "", { "dependencies": { "typewise-core": "^1.2.0" } }, "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ=="],
|
"typewise": ["typewise@1.0.3", "", { "dependencies": { "typewise-core": "^1.2.0" } }, "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ=="],
|
||||||
|
|
||||||
@@ -1645,8 +1645,14 @@
|
|||||||
|
|
||||||
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
|
"autoprefixer/browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
"better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.10.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg=="],
|
||||||
|
|
||||||
|
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
|
||||||
|
|
||||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
@@ -1681,6 +1687,8 @@
|
|||||||
|
|
||||||
"shadcn/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
"shadcn/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"shadcn/postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
|
||||||
|
|
||||||
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
|
"split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
|
||||||
@@ -1771,6 +1779,8 @@
|
|||||||
|
|
||||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"autoprefixer/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.338", "", {}, "sha512-KVQQ3xko9/coDX3qXLUEEbqkKT8L+1DyAovrtu0Khtrt9wjSZ+7CZV4GVzxFy9Oe1NbrIU1oVXCwHJruIA1PNg=="],
|
||||||
|
|
||||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
|||||||
# GOPROXY=direct bypasses the module proxy cache so the latest commit is always fetched
|
# GOPROXY=direct bypasses the module proxy cache so the latest commit is always fetched
|
||||||
RUN GOPROXY=direct xcaddy build \
|
RUN GOPROXY=direct xcaddy build \
|
||||||
--with github.com/caddy-dns/cloudflare \
|
--with github.com/caddy-dns/cloudflare \
|
||||||
|
--with github.com/caddy-dns/route53 \
|
||||||
|
--with github.com/caddy-dns/digitalocean \
|
||||||
|
--with github.com/caddy-dns/duckdns \
|
||||||
|
--with github.com/caddy-dns/hetzner \
|
||||||
|
--with github.com/caddy-dns/vultr \
|
||||||
|
--with github.com/caddy-dns/porkbun \
|
||||||
|
--with github.com/caddy-dns/godaddy \
|
||||||
|
--with github.com/caddy-dns/namecheap \
|
||||||
|
--with github.com/caddy-dns/ovh \
|
||||||
|
--with github.com/caddy-dns/ionos \
|
||||||
|
--with github.com/caddy-dns/linode \
|
||||||
--with github.com/mholt/caddy-l4 \
|
--with github.com/mholt/caddy-l4 \
|
||||||
--with github.com/fuomag9/caddy-blocker-plugin \
|
--with github.com/fuomag9/caddy-blocker-plugin \
|
||||||
--with github.com/corazawaf/coraza-caddy/v2 \
|
--with github.com/corazawaf/coraza-caddy/v2 \
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -37,9 +37,9 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"apexcharts": "^5.10.6",
|
"apexcharts": "^5.10.6",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.5.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "^1.6.2",
|
"better-auth": "^1.6.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"maplibre-gl": "^5.22.0",
|
"maplibre-gl": "^5.23.0",
|
||||||
"maxmind": "^5.0.6",
|
"maxmind": "^5.0.6",
|
||||||
"next": "^16.2.3",
|
"next": "^16.2.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-forge": "^1.4.0",
|
"node-forge": "^1.4.0",
|
||||||
"postcss": "^8.5.9",
|
"postcss": "^8.5.10",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-apexcharts": "^2.1.0",
|
"react-apexcharts": "^2.1.0",
|
||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.14.0",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@next/eslint-plugin-next": "^16.2.3",
|
"@next/eslint-plugin-next": "^16.2.4",
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/bun": "^1.3.12",
|
"@types/bun": "^1.3.12",
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/topojson-client": "^3.1.5",
|
"@types/topojson-client": "^3.1.5",
|
||||||
"@vitest/ui": "^4.1.4",
|
"@vitest/ui": "^4.1.4",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"eslint": "^10.2.0",
|
"eslint": "^10.2.0",
|
||||||
"shadcn": "^4.2.0",
|
"shadcn": "^4.2.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.1",
|
"typescript-eslint": "^8.58.2",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "^4.1.4",
|
"vitest": "^4.1.4",
|
||||||
"world-atlas": "^2.0.2"
|
"world-atlas": "^2.0.2"
|
||||||
|
|||||||
@@ -162,6 +162,19 @@ export function AuthentikFields({
|
|||||||
Leave empty to protect entire domain. Specify paths to protect specific routes only.
|
Leave empty to protect entire domain. Specify paths to protect specific routes only.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1 block">Excluded Paths (Optional)</label>
|
||||||
|
<Textarea
|
||||||
|
name="authentik_excluded_paths"
|
||||||
|
placeholder="/share/*, /rest/*"
|
||||||
|
defaultValue={initial?.excludedPaths?.join(", ") ?? ""}
|
||||||
|
disabled={!enabled}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Paths to exclude from authentication. These paths will bypass forward auth while all other paths remain protected. Ignored if Protected Paths is set.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<HiddenCheckboxField
|
<HiddenCheckboxField
|
||||||
name="authentik_set_host_header"
|
name="authentik_set_host_header"
|
||||||
defaultChecked={setHostHeaderDefault}
|
defaultChecked={setHostHeaderDefault}
|
||||||
|
|||||||
@@ -99,6 +99,19 @@ export function CpmForwardAuthFields({
|
|||||||
Leave empty to protect entire domain. Comma-separated paths to protect specific routes only.
|
Leave empty to protect entire domain. Comma-separated paths to protect specific routes only.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-1 block">Excluded Paths (Optional)</label>
|
||||||
|
<Textarea
|
||||||
|
name="cpm_forward_auth_excluded_paths"
|
||||||
|
placeholder="/share/*, /rest/*"
|
||||||
|
defaultValue={initial?.excluded_paths?.join(", ") ?? ""}
|
||||||
|
disabled={!enabled}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Paths to exclude from authentication. These paths will bypass forward auth while all other paths remain protected. Ignored if Protected Paths is set.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Allowed Groups */}
|
{/* Allowed Groups */}
|
||||||
{groups.length > 0 && (
|
{groups.length > 0 && (
|
||||||
|
|||||||
171
src/lib/caddy.ts
171
src/lib/caddy.ts
@@ -26,11 +26,11 @@ import db, { nowIso } from "./db";
|
|||||||
import { eq, isNull } from "drizzle-orm";
|
import { eq, isNull } from "drizzle-orm";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import {
|
import {
|
||||||
getCloudflareSettings,
|
|
||||||
getGeneralSettings,
|
getGeneralSettings,
|
||||||
getMetricsSettings,
|
getMetricsSettings,
|
||||||
getLoggingSettings,
|
getLoggingSettings,
|
||||||
getDnsSettings,
|
getDnsSettings,
|
||||||
|
getDnsProviderSettings,
|
||||||
getUpstreamDnsResolutionSettings,
|
getUpstreamDnsResolutionSettings,
|
||||||
getGeoBlockSettings,
|
getGeoBlockSettings,
|
||||||
getWafSettings,
|
getWafSettings,
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
type GeoBlockSettings,
|
type GeoBlockSettings,
|
||||||
type WafSettings
|
type WafSettings
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
|
import { buildDnsChallengeConfig, type DnsProviderCredentials } from "./dns-providers";
|
||||||
import { syncInstances } from "./instance-sync";
|
import { syncInstances } from "./instance-sync";
|
||||||
import {
|
import {
|
||||||
accessListEntries,
|
accessListEntries,
|
||||||
@@ -109,6 +110,7 @@ type UpstreamDnsResolutionMeta = {
|
|||||||
type CpmForwardAuthMeta = {
|
type CpmForwardAuthMeta = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
protected_paths?: string[];
|
protected_paths?: string[];
|
||||||
|
excluded_paths?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProxyHostMeta = {
|
type ProxyHostMeta = {
|
||||||
@@ -144,6 +146,7 @@ type ProxyHostAuthentikMeta = {
|
|||||||
trusted_proxies?: string[];
|
trusted_proxies?: string[];
|
||||||
set_outpost_host_header?: boolean;
|
set_outpost_host_header?: boolean;
|
||||||
protected_paths?: string[];
|
protected_paths?: string[];
|
||||||
|
excluded_paths?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AuthentikRouteConfig = {
|
type AuthentikRouteConfig = {
|
||||||
@@ -155,6 +158,7 @@ type AuthentikRouteConfig = {
|
|||||||
trustedProxies: string[];
|
trustedProxies: string[];
|
||||||
setOutpostHostHeader: boolean;
|
setOutpostHostHeader: boolean;
|
||||||
protectedPaths: string[] | null;
|
protectedPaths: string[] | null;
|
||||||
|
excludedPaths: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoadBalancerActiveHealthCheckMeta = {
|
type LoadBalancerActiveHealthCheckMeta = {
|
||||||
@@ -1030,6 +1034,7 @@ async function buildProxyRoutes(
|
|||||||
|
|
||||||
// Path-based authentication support
|
// Path-based authentication support
|
||||||
if (authentik.protectedPaths && authentik.protectedPaths.length > 0) {
|
if (authentik.protectedPaths && authentik.protectedPaths.length > 0) {
|
||||||
|
// Whitelist mode: only specified paths get auth
|
||||||
for (const domainGroup of domainGroups) {
|
for (const domainGroup of domainGroups) {
|
||||||
// Create separate routes for each protected path
|
// Create separate routes for each protected path
|
||||||
for (const protectedPath of authentik.protectedPaths) {
|
for (const protectedPath of authentik.protectedPaths) {
|
||||||
@@ -1087,7 +1092,54 @@ async function buildProxyRoutes(
|
|||||||
terminal: true
|
terminal: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (authentik.excludedPaths && authentik.excludedPaths.length > 0) {
|
||||||
|
// Exclusion mode: protect everything EXCEPT specified paths
|
||||||
|
const locationRules = meta.location_rules ?? [];
|
||||||
|
for (const domainGroup of domainGroups) {
|
||||||
|
if (outpostRoute) {
|
||||||
|
const outpostMatches = (outpostRoute.match as Array<Record<string, unknown>> | undefined) ?? [];
|
||||||
|
hostRoutes.push({
|
||||||
|
...outpostRoute,
|
||||||
|
match: outpostMatches.map((match) => ({
|
||||||
|
...match,
|
||||||
|
host: domainGroup
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unprotected routes for each excluded path (before the catch-all)
|
||||||
|
for (const excludedPath of authentik.excludedPaths) {
|
||||||
|
hostRoutes.push({
|
||||||
|
match: [{ host: domainGroup, path: [excludedPath] }],
|
||||||
|
handle: [...handlers, JSON.parse(JSON.stringify(reverseProxyHandler))],
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location rules get auth (same as full-site mode)
|
||||||
|
for (const rule of locationRules) {
|
||||||
|
const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy(
|
||||||
|
rule,
|
||||||
|
Boolean(row.skipHttpsHostnameValidation),
|
||||||
|
Boolean(row.preserveHostHeader)
|
||||||
|
);
|
||||||
|
if (!safePath) continue;
|
||||||
|
hostRoutes.push({
|
||||||
|
match: [{ host: domainGroup, path: [safePath] }],
|
||||||
|
handle: [...handlers, forwardAuthHandler, locationProxy],
|
||||||
|
terminal: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch-all with auth (everything not excluded)
|
||||||
|
hostRoutes.push({
|
||||||
|
match: [{ host: domainGroup }],
|
||||||
|
handle: [...handlers, forwardAuthHandler, reverseProxyHandler],
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Full-site mode: protect everything
|
||||||
const locationRules = meta.location_rules ?? [];
|
const locationRules = meta.location_rules ?? [];
|
||||||
for (const domainGroup of domainGroups) {
|
for (const domainGroup of domainGroups) {
|
||||||
if (outpostRoute) {
|
if (outpostRoute) {
|
||||||
@@ -1229,7 +1281,7 @@ async function buildProxyRoutes(
|
|||||||
const locationRules = meta.location_rules ?? [];
|
const locationRules = meta.location_rules ?? [];
|
||||||
|
|
||||||
if (cpmForwardAuth.protected_paths && cpmForwardAuth.protected_paths.length > 0) {
|
if (cpmForwardAuth.protected_paths && cpmForwardAuth.protected_paths.length > 0) {
|
||||||
// Path-specific authentication
|
// Whitelist mode: only specified paths get auth
|
||||||
for (const domainGroup of domainGroups) {
|
for (const domainGroup of domainGroups) {
|
||||||
// Add callback route (unprotected)
|
// Add callback route (unprotected)
|
||||||
hostRoutes.push({
|
hostRoutes.push({
|
||||||
@@ -1273,8 +1325,48 @@ async function buildProxyRoutes(
|
|||||||
terminal: true
|
terminal: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (cpmForwardAuth.excluded_paths && cpmForwardAuth.excluded_paths.length > 0) {
|
||||||
|
// Exclusion mode: protect everything EXCEPT specified paths
|
||||||
|
for (const domainGroup of domainGroups) {
|
||||||
|
// Callback route first (unprotected)
|
||||||
|
hostRoutes.push({
|
||||||
|
...cpmCallbackRoute,
|
||||||
|
match: [{ host: domainGroup, path: ["/.cpm-auth/callback"] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Excluded paths — unprotected, before the catch-all
|
||||||
|
for (const excludedPath of cpmForwardAuth.excluded_paths) {
|
||||||
|
hostRoutes.push({
|
||||||
|
match: [{ host: domainGroup, path: [excludedPath] }],
|
||||||
|
handle: [...handlers, JSON.parse(JSON.stringify(reverseProxyHandler))],
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location rules with forward auth
|
||||||
|
for (const rule of locationRules) {
|
||||||
|
const { safePath, reverseProxyHandler: locationProxy } = buildLocationReverseProxy(
|
||||||
|
rule,
|
||||||
|
Boolean(row.skipHttpsHostnameValidation),
|
||||||
|
Boolean(row.preserveHostHeader)
|
||||||
|
);
|
||||||
|
if (!safePath) continue;
|
||||||
|
hostRoutes.push({
|
||||||
|
match: [{ host: domainGroup, path: [safePath] }],
|
||||||
|
handle: [...handlers, cpmForwardAuthHandler, locationProxy],
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch-all with auth (everything not excluded)
|
||||||
|
hostRoutes.push({
|
||||||
|
match: [{ host: domainGroup }],
|
||||||
|
handle: [...handlers, cpmForwardAuthHandler, reverseProxyHandler],
|
||||||
|
terminal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Protect entire site
|
// Full-site mode: protect everything
|
||||||
for (const domainGroup of domainGroups) {
|
for (const domainGroup of domainGroups) {
|
||||||
// Callback route first (unprotected)
|
// Callback route first (unprotected)
|
||||||
hostRoutes.push({
|
hostRoutes.push({
|
||||||
@@ -1499,8 +1591,11 @@ async function buildTlsAutomation(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloudflare = await getCloudflareSettings();
|
const dnsProviderSettings = await getDnsProviderSettings();
|
||||||
const hasCloudflare = cloudflare && cloudflare.apiToken;
|
const globalDnsProvider: DnsProviderCredentials | null =
|
||||||
|
dnsProviderSettings?.default && dnsProviderSettings.providers[dnsProviderSettings.default]
|
||||||
|
? { provider: dnsProviderSettings.default, credentials: dnsProviderSettings.providers[dnsProviderSettings.default] }
|
||||||
|
: null;
|
||||||
|
|
||||||
const dnsSettings = options.dnsSettings ?? await getDnsSettings();
|
const dnsSettings = options.dnsSettings ?? await getDnsSettings();
|
||||||
const hasDnsResolvers = dnsSettings && dnsSettings.enabled && dnsSettings.resolvers && dnsSettings.resolvers.length > 0;
|
const hasDnsResolvers = dnsSettings && dnsSettings.enabled && dnsSettings.resolvers && dnsSettings.resolvers.length > 0;
|
||||||
@@ -1528,23 +1623,15 @@ async function buildTlsAutomation(
|
|||||||
issuer.email = options.acmeEmail;
|
issuer.email = options.acmeEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCloudflare) {
|
if (globalDnsProvider) {
|
||||||
const providerConfig: Record<string, string> = {
|
const dnsChallenge = buildDnsChallengeConfig(
|
||||||
name: "cloudflare",
|
globalDnsProvider.provider,
|
||||||
api_token: cloudflare.apiToken
|
globalDnsProvider.credentials,
|
||||||
};
|
dnsResolvers
|
||||||
|
);
|
||||||
const dnsChallenge: Record<string, unknown> = {
|
if (dnsChallenge) {
|
||||||
provider: providerConfig
|
issuer.challenges = { dns: dnsChallenge };
|
||||||
};
|
|
||||||
|
|
||||||
if (dnsResolvers.length > 0) {
|
|
||||||
dnsChallenge.resolvers = dnsResolvers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
issuer.challenges = {
|
|
||||||
dns: dnsChallenge
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
policies.push({
|
policies.push({
|
||||||
@@ -1563,6 +1650,16 @@ async function buildTlsAutomation(
|
|||||||
|
|
||||||
managedCertificateIds.add(entry.certificate.id);
|
managedCertificateIds.add(entry.certificate.id);
|
||||||
|
|
||||||
|
// Per-certificate provider override, falling back to global default
|
||||||
|
let effectiveProvider = globalDnsProvider;
|
||||||
|
const certOptions = entry.certificate.providerOptions as { provider?: string } | null;
|
||||||
|
if (certOptions?.provider && dnsProviderSettings?.providers[certOptions.provider]) {
|
||||||
|
effectiveProvider = {
|
||||||
|
provider: certOptions.provider,
|
||||||
|
credentials: dnsProviderSettings.providers[certOptions.provider],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (const subjectGroup of groupHostPatternsByPriority(subjects)) {
|
for (const subjectGroup of groupHostPatternsByPriority(subjects)) {
|
||||||
const issuer: Record<string, unknown> = {
|
const issuer: Record<string, unknown> = {
|
||||||
module: "acme"
|
module: "acme"
|
||||||
@@ -1572,23 +1669,15 @@ async function buildTlsAutomation(
|
|||||||
issuer.email = options.acmeEmail;
|
issuer.email = options.acmeEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasCloudflare) {
|
if (effectiveProvider) {
|
||||||
const providerConfig: Record<string, string> = {
|
const dnsChallenge = buildDnsChallengeConfig(
|
||||||
name: "cloudflare",
|
effectiveProvider.provider,
|
||||||
api_token: cloudflare.apiToken
|
effectiveProvider.credentials,
|
||||||
};
|
dnsResolvers
|
||||||
|
);
|
||||||
const dnsChallenge: Record<string, unknown> = {
|
if (dnsChallenge) {
|
||||||
provider: providerConfig
|
issuer.challenges = { dns: dnsChallenge };
|
||||||
};
|
|
||||||
|
|
||||||
if (dnsResolvers.length > 0) {
|
|
||||||
dnsChallenge.resolvers = dnsResolvers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
issuer.challenges = {
|
|
||||||
dns: dnsChallenge
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
policies.push({
|
policies.push({
|
||||||
@@ -2282,6 +2371,11 @@ function parseAuthentikConfig(meta: ProxyHostAuthentikMeta | undefined | null):
|
|||||||
? meta.protected_paths.map((path) => path?.trim()).filter((path): path is string => Boolean(path))
|
? meta.protected_paths.map((path) => path?.trim()).filter((path): path is string => Boolean(path))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const excludedPaths =
|
||||||
|
Array.isArray(meta.excluded_paths) && meta.excluded_paths.length > 0
|
||||||
|
? meta.excluded_paths.map((path) => path?.trim()).filter((path): path is string => Boolean(path))
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
outpostDomain,
|
outpostDomain,
|
||||||
@@ -2290,7 +2384,8 @@ function parseAuthentikConfig(meta: ProxyHostAuthentikMeta | undefined | null):
|
|||||||
copyHeaders,
|
copyHeaders,
|
||||||
trustedProxies,
|
trustedProxies,
|
||||||
setOutpostHostHeader,
|
setOutpostHostHeader,
|
||||||
protectedPaths
|
protectedPaths,
|
||||||
|
excludedPaths
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,9 +252,59 @@ function runEnvProviderSync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-time migration: convert legacy Cloudflare DNS settings to the new
|
||||||
|
* generic dns_provider format. Idempotent — skips if already run or if
|
||||||
|
* the new setting already exists.
|
||||||
|
*/
|
||||||
|
function runCloudflareToProviderMigration() {
|
||||||
|
if (sqlitePath === ":memory:") return;
|
||||||
|
|
||||||
|
const { settings: settingsTable } = schema;
|
||||||
|
|
||||||
|
// Skip if migration already ran
|
||||||
|
const flag = db.select().from(settingsTable).where(eq(settingsTable.key, "dns_provider_migrated")).get();
|
||||||
|
if (flag) return;
|
||||||
|
|
||||||
|
// Skip if new dns_provider setting already exists (user already configured it)
|
||||||
|
const existing = db.select().from(settingsTable).where(eq(settingsTable.key, "dns_provider")).get();
|
||||||
|
if (existing) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
db.insert(settingsTable).values({ key: "dns_provider_migrated", value: "true", updatedAt: now }).run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for legacy cloudflare setting
|
||||||
|
const cfRow = db.select().from(settingsTable).where(eq(settingsTable.key, "cloudflare")).get();
|
||||||
|
if (!cfRow) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
db.insert(settingsTable).values({ key: "dns_provider_migrated", value: "true", updatedAt: now }).run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cf = JSON.parse(cfRow.value) as { apiToken?: string; zoneId?: string; accountId?: string };
|
||||||
|
if (cf.apiToken) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const newSetting = {
|
||||||
|
providers: { cloudflare: { api_token: cf.apiToken } },
|
||||||
|
default: "cloudflare",
|
||||||
|
};
|
||||||
|
db.insert(settingsTable).values({ key: "dns_provider", value: JSON.stringify(newSetting), updatedAt: now }).run();
|
||||||
|
console.log("Migrated legacy Cloudflare DNS settings to dns_provider format");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to parse legacy cloudflare setting during migration:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
db.insert(settingsTable).values({ key: "dns_provider_migrated", value: "true", updatedAt: now }).run();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runBetterAuthDataMigration();
|
runBetterAuthDataMigration();
|
||||||
runEnvProviderSync();
|
runEnvProviderSync();
|
||||||
|
runCloudflareToProviderMigration();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Better Auth data migration warning:", error);
|
console.warn("Better Auth data migration warning:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
268
src/lib/dns-providers.ts
Normal file
268
src/lib/dns-providers.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import { encryptSecret, decryptSecret, isEncryptedSecret } from "./secret";
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type DnsProviderFieldType = "string" | "password";
|
||||||
|
|
||||||
|
export type DnsProviderField = {
|
||||||
|
/** Key sent to Caddy config (e.g. "api_token") */
|
||||||
|
key: string;
|
||||||
|
/** Human-readable label */
|
||||||
|
label: string;
|
||||||
|
/** "password" fields are encrypted at rest */
|
||||||
|
type: DnsProviderFieldType;
|
||||||
|
/** Placeholder text for the input */
|
||||||
|
placeholder?: string;
|
||||||
|
/** Help text shown below the input */
|
||||||
|
description?: string;
|
||||||
|
/** Whether the field is required */
|
||||||
|
required: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DnsProviderDefinition = {
|
||||||
|
/** Caddy DNS module name (e.g. "cloudflare", "route53") */
|
||||||
|
name: string;
|
||||||
|
/** Human-readable display name */
|
||||||
|
displayName: string;
|
||||||
|
/** Short description */
|
||||||
|
description?: string;
|
||||||
|
/** Link to caddy-dns module docs */
|
||||||
|
docsUrl?: string;
|
||||||
|
/** Credential fields this provider requires */
|
||||||
|
fields: DnsProviderField[];
|
||||||
|
/** caddy-dns Go module path (for Dockerfile reference) */
|
||||||
|
modulePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DnsProviderCredentials = {
|
||||||
|
provider: string;
|
||||||
|
credentials: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Registry ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const DNS_PROVIDERS: DnsProviderDefinition[] = [
|
||||||
|
{
|
||||||
|
name: "cloudflare",
|
||||||
|
displayName: "Cloudflare",
|
||||||
|
description: "Cloudflare DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/cloudflare",
|
||||||
|
modulePath: "github.com/caddy-dns/cloudflare",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: "api_token",
|
||||||
|
label: "API Token",
|
||||||
|
type: "password",
|
||||||
|
required: true,
|
||||||
|
placeholder: "Cloudflare API token with Zone:DNS:Edit permission",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "route53",
|
||||||
|
displayName: "Amazon Route 53",
|
||||||
|
description: "AWS Route 53 DNS API (supports IAM roles when fields are empty)",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/route53",
|
||||||
|
modulePath: "github.com/caddy-dns/route53",
|
||||||
|
fields: [
|
||||||
|
{ key: "access_key_id", label: "Access Key ID", type: "string", required: false, placeholder: "AKIA..." },
|
||||||
|
{ key: "secret_access_key", label: "Secret Access Key", type: "password", required: false },
|
||||||
|
{ key: "region", label: "AWS Region", type: "string", required: false, placeholder: "us-east-1" },
|
||||||
|
{
|
||||||
|
key: "hosted_zone_id",
|
||||||
|
label: "Hosted Zone ID",
|
||||||
|
type: "string",
|
||||||
|
required: false,
|
||||||
|
placeholder: "Z1234567890",
|
||||||
|
description: "Optional. Required only if you have multiple zones for the same domain.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "digitalocean",
|
||||||
|
displayName: "DigitalOcean",
|
||||||
|
description: "DigitalOcean DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/digitalocean",
|
||||||
|
modulePath: "github.com/caddy-dns/digitalocean",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_token", label: "API Token", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duckdns",
|
||||||
|
displayName: "Duck DNS",
|
||||||
|
description: "Duck DNS dynamic DNS service",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/duckdns",
|
||||||
|
modulePath: "github.com/caddy-dns/duckdns",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_token", label: "Token", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hetzner",
|
||||||
|
displayName: "Hetzner",
|
||||||
|
description: "Hetzner DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/hetzner",
|
||||||
|
modulePath: "github.com/caddy-dns/hetzner",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_token", label: "API Token", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "vultr",
|
||||||
|
displayName: "Vultr",
|
||||||
|
description: "Vultr DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/vultr",
|
||||||
|
modulePath: "github.com/caddy-dns/vultr",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_token", label: "API Key", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "porkbun",
|
||||||
|
displayName: "Porkbun",
|
||||||
|
description: "Porkbun DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/porkbun",
|
||||||
|
modulePath: "github.com/caddy-dns/porkbun",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_key", label: "API Key", type: "password", required: true },
|
||||||
|
{ key: "api_secret_key", label: "API Secret Key", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "godaddy",
|
||||||
|
displayName: "GoDaddy",
|
||||||
|
description: "GoDaddy DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/godaddy",
|
||||||
|
modulePath: "github.com/caddy-dns/godaddy",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: "api_token",
|
||||||
|
label: "API Key:Secret",
|
||||||
|
type: "password",
|
||||||
|
required: true,
|
||||||
|
placeholder: "key:secret",
|
||||||
|
description: "Format: API_KEY:API_SECRET",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "namecheap",
|
||||||
|
displayName: "Namecheap",
|
||||||
|
description: "Namecheap DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/namecheap",
|
||||||
|
modulePath: "github.com/caddy-dns/namecheap",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_key", label: "API Key", type: "password", required: true },
|
||||||
|
{ key: "user", label: "Username", type: "string", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ovh",
|
||||||
|
displayName: "OVH",
|
||||||
|
description: "OVH DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/ovh",
|
||||||
|
modulePath: "github.com/caddy-dns/ovh",
|
||||||
|
fields: [
|
||||||
|
{ key: "endpoint", label: "Endpoint", type: "string", required: true, placeholder: "ovh-eu" },
|
||||||
|
{ key: "application_key", label: "Application Key", type: "string", required: true },
|
||||||
|
{ key: "application_secret", label: "Application Secret", type: "password", required: true },
|
||||||
|
{ key: "consumer_key", label: "Consumer Key", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ionos",
|
||||||
|
displayName: "IONOS",
|
||||||
|
description: "IONOS DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/ionos",
|
||||||
|
modulePath: "github.com/caddy-dns/ionos",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_token", label: "API Token", type: "password", required: true, placeholder: "prefix.secret" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "linode",
|
||||||
|
displayName: "Linode (Akamai)",
|
||||||
|
description: "Linode/Akamai DNS API",
|
||||||
|
docsUrl: "https://github.com/caddy-dns/linode",
|
||||||
|
modulePath: "github.com/caddy-dns/linode",
|
||||||
|
fields: [
|
||||||
|
{ key: "api_token", label: "API Token", type: "password", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getProviderDefinition(name: string): DnsProviderDefinition | undefined {
|
||||||
|
return DNS_PROVIDERS.find((p) => p.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt password-type credential fields for storage.
|
||||||
|
* Non-password fields and already-encrypted values are left unchanged.
|
||||||
|
*/
|
||||||
|
export function encryptProviderCredentials(
|
||||||
|
providerName: string,
|
||||||
|
credentials: Record<string, string>
|
||||||
|
): Record<string, string> {
|
||||||
|
const def = getProviderDefinition(providerName);
|
||||||
|
if (!def) return credentials;
|
||||||
|
|
||||||
|
const result = { ...credentials };
|
||||||
|
for (const field of def.fields) {
|
||||||
|
if (field.type === "password" && result[field.key] && !isEncryptedSecret(result[field.key])) {
|
||||||
|
result[field.key] = encryptSecret(result[field.key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt password-type credential fields for use in Caddy config.
|
||||||
|
*/
|
||||||
|
export function decryptProviderCredentials(
|
||||||
|
providerName: string,
|
||||||
|
credentials: Record<string, string>
|
||||||
|
): Record<string, string> {
|
||||||
|
const def = getProviderDefinition(providerName);
|
||||||
|
if (!def) return credentials;
|
||||||
|
|
||||||
|
const result = { ...credentials };
|
||||||
|
for (const field of def.fields) {
|
||||||
|
if (field.type === "password" && result[field.key] && isEncryptedSecret(result[field.key])) {
|
||||||
|
result[field.key] = decryptSecret(result[field.key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Caddy DNS challenge provider config from a provider name + credentials.
|
||||||
|
* Returns the object to set as `issuer.challenges.dns`.
|
||||||
|
*/
|
||||||
|
export function buildDnsChallengeConfig(
|
||||||
|
providerName: string,
|
||||||
|
credentials: Record<string, string>,
|
||||||
|
dnsResolvers: string[]
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
const def = getProviderDefinition(providerName);
|
||||||
|
if (!def) return null;
|
||||||
|
|
||||||
|
const decrypted = decryptProviderCredentials(providerName, credentials);
|
||||||
|
|
||||||
|
// Build provider config: { name: "cloudflare", api_token: "..." }
|
||||||
|
const providerConfig: Record<string, string> = { name: providerName };
|
||||||
|
for (const [key, value] of Object.entries(decrypted)) {
|
||||||
|
if (value) {
|
||||||
|
providerConfig[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnsChallenge: Record<string, unknown> = { provider: providerConfig };
|
||||||
|
if (dnsResolvers.length > 0) {
|
||||||
|
dnsChallenge.resolvers = dnsResolvers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dnsChallenge;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ export type InstanceMode = "standalone" | "master" | "slave";
|
|||||||
export type SyncSettings = {
|
export type SyncSettings = {
|
||||||
general: unknown | null;
|
general: unknown | null;
|
||||||
cloudflare: unknown | null;
|
cloudflare: unknown | null;
|
||||||
|
dns_provider: unknown | null;
|
||||||
authentik: unknown | null;
|
authentik: unknown | null;
|
||||||
metrics: unknown | null;
|
metrics: unknown | null;
|
||||||
logging: unknown | null;
|
logging: unknown | null;
|
||||||
@@ -249,6 +250,7 @@ export async function buildSyncPayload(): Promise<SyncPayload> {
|
|||||||
const settings = {
|
const settings = {
|
||||||
general: await getSetting("general"),
|
general: await getSetting("general"),
|
||||||
cloudflare: await getSetting("cloudflare"),
|
cloudflare: await getSetting("cloudflare"),
|
||||||
|
dns_provider: await getSetting("dns_provider"),
|
||||||
authentik: await getSetting("authentik"),
|
authentik: await getSetting("authentik"),
|
||||||
metrics: await getSetting("metrics"),
|
metrics: await getSetting("metrics"),
|
||||||
logging: await getSetting("logging"),
|
logging: await getSetting("logging"),
|
||||||
@@ -422,6 +424,7 @@ export async function syncInstances(): Promise<{ total: number; success: number;
|
|||||||
export async function applySyncPayload(payload: SyncPayload) {
|
export async function applySyncPayload(payload: SyncPayload) {
|
||||||
await setSyncedSetting("general", payload.settings.general);
|
await setSyncedSetting("general", payload.settings.general);
|
||||||
await setSyncedSetting("cloudflare", payload.settings.cloudflare);
|
await setSyncedSetting("cloudflare", payload.settings.cloudflare);
|
||||||
|
await setSyncedSetting("dns_provider", payload.settings.dns_provider ?? null);
|
||||||
await setSyncedSetting("authentik", payload.settings.authentik);
|
await setSyncedSetting("authentik", payload.settings.authentik);
|
||||||
await setSyncedSetting("metrics", payload.settings.metrics);
|
await setSyncedSetting("metrics", payload.settings.metrics);
|
||||||
await setSyncedSetting("logging", payload.settings.logging);
|
await setSyncedSetting("logging", payload.settings.logging);
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ export type ProxyHostAuthentikConfig = {
|
|||||||
trustedProxies: string[];
|
trustedProxies: string[];
|
||||||
setOutpostHostHeader: boolean;
|
setOutpostHostHeader: boolean;
|
||||||
protectedPaths: string[] | null;
|
protectedPaths: string[] | null;
|
||||||
|
excludedPaths: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProxyHostAuthentikInput = {
|
export type ProxyHostAuthentikInput = {
|
||||||
@@ -219,6 +220,7 @@ export type ProxyHostAuthentikInput = {
|
|||||||
trustedProxies?: string[] | null;
|
trustedProxies?: string[] | null;
|
||||||
setOutpostHostHeader?: boolean | null;
|
setOutpostHostHeader?: boolean | null;
|
||||||
protectedPaths?: string[] | null;
|
protectedPaths?: string[] | null;
|
||||||
|
excludedPaths?: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProxyHostAuthentikMeta = {
|
type ProxyHostAuthentikMeta = {
|
||||||
@@ -230,6 +232,7 @@ type ProxyHostAuthentikMeta = {
|
|||||||
trusted_proxies?: string[];
|
trusted_proxies?: string[];
|
||||||
set_outpost_host_header?: boolean;
|
set_outpost_host_header?: boolean;
|
||||||
protected_paths?: string[];
|
protected_paths?: string[];
|
||||||
|
excluded_paths?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MtlsConfig = {
|
export type MtlsConfig = {
|
||||||
@@ -245,16 +248,19 @@ export type MtlsConfig = {
|
|||||||
export type CpmForwardAuthConfig = {
|
export type CpmForwardAuthConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
protected_paths: string[] | null;
|
protected_paths: string[] | null;
|
||||||
|
excluded_paths: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CpmForwardAuthInput = {
|
export type CpmForwardAuthInput = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
protected_paths?: string[] | null;
|
protected_paths?: string[] | null;
|
||||||
|
excluded_paths?: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CpmForwardAuthMeta = {
|
type CpmForwardAuthMeta = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
protected_paths?: string[];
|
protected_paths?: string[];
|
||||||
|
excluded_paths?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProxyHostMeta = {
|
type ProxyHostMeta = {
|
||||||
@@ -396,6 +402,13 @@ function sanitizeAuthentikMeta(meta: ProxyHostAuthentikMeta | undefined): ProxyH
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(meta.excluded_paths)) {
|
||||||
|
const paths = meta.excluded_paths.map((path) => path?.trim().replace(/\{[^}]*\}/g, "")).filter((path): path is string => Boolean(path));
|
||||||
|
if (paths.length > 0) {
|
||||||
|
normalized.excluded_paths = paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,6 +588,12 @@ function sanitizeCpmForwardAuthMeta(meta: CpmForwardAuthMeta | undefined): CpmFo
|
|||||||
normalized.protected_paths = paths;
|
normalized.protected_paths = paths;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(meta.excluded_paths)) {
|
||||||
|
const paths = meta.excluded_paths.map((p) => p?.trim().replace(/\{[^}]*\}/g, "")).filter((p): p is string => Boolean(p)); // codeql[js/polynomial-redos] false positive: [^}]* is linear, no backtracking ambiguity
|
||||||
|
if (paths.length > 0) {
|
||||||
|
normalized.excluded_paths = paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,6 +825,17 @@ function normalizeAuthentikInput(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.excludedPaths !== undefined) {
|
||||||
|
const paths = (input.excludedPaths ?? [])
|
||||||
|
.map((path) => path?.trim())
|
||||||
|
.filter((path): path is string => Boolean(path));
|
||||||
|
if (paths.length > 0) {
|
||||||
|
next.excluded_paths = paths;
|
||||||
|
} else {
|
||||||
|
delete next.excluded_paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((next.enabled ?? false) && next.outpost_domain && !next.auth_endpoint) {
|
if ((next.enabled ?? false) && next.outpost_domain && !next.auth_endpoint) {
|
||||||
next.auth_endpoint = `/${next.outpost_domain}/auth/caddy`;
|
next.auth_endpoint = `/${next.outpost_domain}/auth/caddy`;
|
||||||
}
|
}
|
||||||
@@ -1198,6 +1228,9 @@ function buildMeta(existing: ProxyHostMeta, input: Partial<ProxyHostInput>): str
|
|||||||
if (input.cpmForwardAuth.protected_paths && input.cpmForwardAuth.protected_paths.length > 0) {
|
if (input.cpmForwardAuth.protected_paths && input.cpmForwardAuth.protected_paths.length > 0) {
|
||||||
cfa.protected_paths = input.cpmForwardAuth.protected_paths;
|
cfa.protected_paths = input.cpmForwardAuth.protected_paths;
|
||||||
}
|
}
|
||||||
|
if (input.cpmForwardAuth.excluded_paths && input.cpmForwardAuth.excluded_paths.length > 0) {
|
||||||
|
cfa.excluded_paths = input.cpmForwardAuth.excluded_paths;
|
||||||
|
}
|
||||||
next.cpm_forward_auth = cfa;
|
next.cpm_forward_auth = cfa;
|
||||||
} else {
|
} else {
|
||||||
delete next.cpm_forward_auth;
|
delete next.cpm_forward_auth;
|
||||||
@@ -1254,6 +1287,8 @@ function hydrateAuthentik(meta: ProxyHostAuthentikMeta | undefined): ProxyHostAu
|
|||||||
meta.set_outpost_host_header !== undefined ? Boolean(meta.set_outpost_host_header) : true;
|
meta.set_outpost_host_header !== undefined ? Boolean(meta.set_outpost_host_header) : true;
|
||||||
const protectedPaths =
|
const protectedPaths =
|
||||||
Array.isArray(meta.protected_paths) && meta.protected_paths.length > 0 ? meta.protected_paths : null;
|
Array.isArray(meta.protected_paths) && meta.protected_paths.length > 0 ? meta.protected_paths : null;
|
||||||
|
const excludedPaths =
|
||||||
|
Array.isArray(meta.excluded_paths) && meta.excluded_paths.length > 0 ? meta.excluded_paths : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
@@ -1263,7 +1298,8 @@ function hydrateAuthentik(meta: ProxyHostAuthentikMeta | undefined): ProxyHostAu
|
|||||||
copyHeaders,
|
copyHeaders,
|
||||||
trustedProxies,
|
trustedProxies,
|
||||||
setOutpostHostHeader,
|
setOutpostHostHeader,
|
||||||
protectedPaths
|
protectedPaths,
|
||||||
|
excludedPaths
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1295,6 +1331,9 @@ function dehydrateAuthentik(config: ProxyHostAuthentikConfig | null): ProxyHostA
|
|||||||
if (config.protectedPaths && config.protectedPaths.length > 0) {
|
if (config.protectedPaths && config.protectedPaths.length > 0) {
|
||||||
meta.protected_paths = [...config.protectedPaths];
|
meta.protected_paths = [...config.protectedPaths];
|
||||||
}
|
}
|
||||||
|
if (config.excludedPaths && config.excludedPaths.length > 0) {
|
||||||
|
meta.excluded_paths = [...config.excludedPaths];
|
||||||
|
}
|
||||||
|
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
@@ -1559,7 +1598,7 @@ function parseProxyHost(row: ProxyHostRow): ProxyHost {
|
|||||||
waf: meta.waf ?? null,
|
waf: meta.waf ?? null,
|
||||||
mtls: meta.mtls ?? null,
|
mtls: meta.mtls ?? null,
|
||||||
cpmForwardAuth: meta.cpm_forward_auth?.enabled
|
cpmForwardAuth: meta.cpm_forward_auth?.enabled
|
||||||
? { enabled: true, protected_paths: meta.cpm_forward_auth.protected_paths ?? null }
|
? { enabled: true, protected_paths: meta.cpm_forward_auth.protected_paths ?? null, excluded_paths: meta.cpm_forward_auth.excluded_paths ?? null }
|
||||||
: null,
|
: null,
|
||||||
redirects: meta.redirects ?? [],
|
redirects: meta.redirects ?? [],
|
||||||
rewrite: meta.rewrite ?? null,
|
rewrite: meta.rewrite ?? null,
|
||||||
@@ -1702,7 +1741,8 @@ export async function updateProxyHost(id: number, input: Partial<ProxyHostInput>
|
|||||||
...(existing.cpmForwardAuth?.enabled ? {
|
...(existing.cpmForwardAuth?.enabled ? {
|
||||||
cpm_forward_auth: {
|
cpm_forward_auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
...(existing.cpmForwardAuth.protected_paths ? { protected_paths: existing.cpmForwardAuth.protected_paths } : {})
|
...(existing.cpmForwardAuth.protected_paths ? { protected_paths: existing.cpmForwardAuth.protected_paths } : {}),
|
||||||
|
...(existing.cpmForwardAuth.excluded_paths ? { excluded_paths: existing.cpmForwardAuth.excluded_paths } : {})
|
||||||
}
|
}
|
||||||
} : {}),
|
} : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ export type DnsSettings = {
|
|||||||
timeout?: string; // DNS query timeout (e.g., "5s")
|
timeout?: string; // DNS query timeout (e.g., "5s")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DnsProviderSettings = {
|
||||||
|
/** Configured providers: keyed by provider name, value is credential map */
|
||||||
|
providers: Record<string, Record<string, string>>;
|
||||||
|
/** Name of the default provider (null = no DNS-01 challenges) */
|
||||||
|
default: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpstreamDnsAddressFamily = "ipv6" | "ipv4" | "both";
|
export type UpstreamDnsAddressFamily = "ipv6" | "ipv4" | "both";
|
||||||
|
|
||||||
export type UpstreamDnsResolutionSettings = {
|
export type UpstreamDnsResolutionSettings = {
|
||||||
@@ -195,6 +202,25 @@ export async function saveDnsSettings(settings: DnsSettings): Promise<void> {
|
|||||||
await setSetting("dns", settings);
|
await setSetting("dns", settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDnsProviderSettings(): Promise<DnsProviderSettings | null> {
|
||||||
|
const raw = await getEffectiveSetting<Record<string, unknown>>("dns_provider");
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
// Normalize old single-provider format { provider, credentials }
|
||||||
|
// to new multi-provider format { providers, default }
|
||||||
|
if ("provider" in raw && "credentials" in raw && !("providers" in raw)) {
|
||||||
|
const name = raw.provider as string;
|
||||||
|
const creds = raw.credentials as Record<string, string>;
|
||||||
|
return { providers: { [name]: creds }, default: name };
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw as unknown as DnsProviderSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDnsProviderSettings(settings: DnsProviderSettings): Promise<void> {
|
||||||
|
await setSetting("dns_provider", settings);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUpstreamDnsResolutionSettings(): Promise<UpstreamDnsResolutionSettings | null> {
|
export async function getUpstreamDnsResolutionSettings(): Promise<UpstreamDnsResolutionSettings | null> {
|
||||||
return await getEffectiveSetting<UpstreamDnsResolutionSettings>("upstream_dns_resolution");
|
return await getEffectiveSetting<UpstreamDnsResolutionSettings>("upstream_dns_resolution");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ test.describe('Cross-user isolation', () => {
|
|||||||
|
|
||||||
test('user can GET their own profile', async ({ request }) => {
|
test('user can GET their own profile', async ({ request }) => {
|
||||||
// First find the user's own ID
|
// First find the user's own ID
|
||||||
const res = await request.get(`${ORIGIN}/api/auth/get-session`, {
|
await request.get(`${ORIGIN}/api/auth/get-session`, {
|
||||||
headers: { 'Authorization': `Bearer ${userToken}` },
|
headers: { 'Authorization': `Bearer ${userToken}` },
|
||||||
});
|
});
|
||||||
// Bearer tokens go through our api-auth, not Better Auth session — use a different approach
|
// Bearer tokens go through our api-auth, not Better Auth session — use a different approach
|
||||||
|
|||||||
139
tests/e2e/functional/forward-auth-excluded-paths.spec.ts
Normal file
139
tests/e2e/functional/forward-auth-excluded-paths.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Functional tests: CPM Forward Auth with excluded paths.
|
||||||
|
*
|
||||||
|
* Creates a proxy host with CPM forward auth enabled and excluded_paths set,
|
||||||
|
* then verifies:
|
||||||
|
* - Excluded paths bypass auth and reach the upstream directly
|
||||||
|
* - Non-excluded paths still require authentication (redirect to portal)
|
||||||
|
* - The callback route still works for completing auth on non-excluded paths
|
||||||
|
*
|
||||||
|
* This validates the fix for GitHub issue #108: the ability to exclude
|
||||||
|
* specific paths from forward auth (e.g., /share/*, /rest/* for Navidrome).
|
||||||
|
*
|
||||||
|
* Domain: func-fwd-auth-excl.test
|
||||||
|
*/
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { httpGet, waitForStatus } from '../../helpers/http';
|
||||||
|
|
||||||
|
const DOMAIN = 'func-fwd-auth-excl.test';
|
||||||
|
const ECHO_BODY = 'echo-ok';
|
||||||
|
const BASE_URL = 'http://localhost:3000';
|
||||||
|
const API = `${BASE_URL}/api/v1`;
|
||||||
|
|
||||||
|
let proxyHostId: number;
|
||||||
|
|
||||||
|
test.describe.serial('Forward Auth Excluded Paths', () => {
|
||||||
|
test('setup: create proxy host with forward auth and excluded paths via API', async ({ page }) => {
|
||||||
|
const res = await page.request.post(`${API}/proxy-hosts`, {
|
||||||
|
data: {
|
||||||
|
name: 'Excluded Paths Test',
|
||||||
|
domains: [DOMAIN],
|
||||||
|
upstreams: ['echo-server:8080'],
|
||||||
|
sslForced: false,
|
||||||
|
cpmForwardAuth: {
|
||||||
|
enabled: true,
|
||||||
|
excluded_paths: ['/share/*', '/rest/*'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
||||||
|
});
|
||||||
|
expect(res.status()).toBe(201);
|
||||||
|
const host = await res.json();
|
||||||
|
proxyHostId = host.id;
|
||||||
|
|
||||||
|
// Grant testadmin (user ID 1) forward auth access
|
||||||
|
const accessRes = await page.request.put(`${API}/proxy-hosts/${proxyHostId}/forward-auth-access`, {
|
||||||
|
data: { userIds: [1], groupIds: [] },
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
||||||
|
});
|
||||||
|
expect(accessRes.status()).toBe(200);
|
||||||
|
|
||||||
|
// Wait for Caddy to pick up the config — non-excluded paths should redirect (302)
|
||||||
|
await waitForStatus(DOMAIN, 302, 20_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('excluded path /share/* bypasses auth and reaches upstream', async () => {
|
||||||
|
const res = await httpGet(DOMAIN, '/share/some-track');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toContain(ECHO_BODY);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('excluded path /rest/* bypasses auth and reaches upstream', async () => {
|
||||||
|
const res = await httpGet(DOMAIN, '/rest/ping');
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toContain(ECHO_BODY);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-excluded root path requires auth (redirects to portal)', async () => {
|
||||||
|
const res = await httpGet(DOMAIN, '/');
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
const location = String(res.headers['location']);
|
||||||
|
expect(location).toContain('/portal?rd=');
|
||||||
|
expect(location).toContain(DOMAIN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-excluded arbitrary path requires auth', async () => {
|
||||||
|
const res = await httpGet(DOMAIN, '/admin/dashboard');
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(String(res.headers['location'])).toContain('/portal');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('credential login works for non-excluded paths', async ({ page }) => {
|
||||||
|
const context = await page.context().browser()!.newContext({ storageState: { cookies: [], origins: [] } });
|
||||||
|
const freshPage = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await freshPage.goto(`${BASE_URL}/portal?rd=http://${DOMAIN}/protected-page`);
|
||||||
|
await expect(freshPage.getByLabel('Username')).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
||||||
|
// Intercept the login API response
|
||||||
|
let capturedRedirect: string | null = null;
|
||||||
|
await freshPage.route('**/api/forward-auth/login', async (route) => {
|
||||||
|
const response = await route.fetch();
|
||||||
|
const json = await response.json();
|
||||||
|
capturedRedirect = json.redirectTo ?? null;
|
||||||
|
await route.fulfill({ response });
|
||||||
|
});
|
||||||
|
|
||||||
|
await freshPage.getByLabel('Username').fill('testadmin');
|
||||||
|
await freshPage.getByLabel('Password').fill('TestPassword2026!');
|
||||||
|
await freshPage.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||||
|
|
||||||
|
const deadline = Date.now() + 15_000;
|
||||||
|
while (!capturedRedirect && Date.now() < deadline) {
|
||||||
|
await freshPage.waitForTimeout(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(capturedRedirect).toBeTruthy();
|
||||||
|
expect(capturedRedirect).toContain('/.cpm-auth/callback');
|
||||||
|
|
||||||
|
// Complete the callback
|
||||||
|
const callbackUrl = new URL(capturedRedirect!);
|
||||||
|
const callbackRes = await httpGet(DOMAIN, callbackUrl.pathname + callbackUrl.search);
|
||||||
|
expect(callbackRes.status).toBe(302);
|
||||||
|
const setCookie = String(callbackRes.headers['set-cookie'] ?? '');
|
||||||
|
expect(setCookie).toContain('_cpm_fa=');
|
||||||
|
|
||||||
|
// Verify authenticated access to non-excluded path
|
||||||
|
const match = setCookie.match(/_cpm_fa=([^;]+)/);
|
||||||
|
expect(match).toBeTruthy();
|
||||||
|
const sessionCookie = match![1];
|
||||||
|
const upstreamRes = await httpGet(DOMAIN, '/protected-page', {
|
||||||
|
Cookie: `_cpm_fa=${sessionCookie}`,
|
||||||
|
});
|
||||||
|
expect(upstreamRes.status).toBe(200);
|
||||||
|
expect(upstreamRes.body).toContain(ECHO_BODY);
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cleanup: delete proxy host', async ({ page }) => {
|
||||||
|
if (proxyHostId) {
|
||||||
|
const res = await page.request.delete(`${API}/proxy-hosts/${proxyHostId}`, {
|
||||||
|
headers: { 'Origin': BASE_URL },
|
||||||
|
});
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,7 @@ test.describe('Settings', () => {
|
|||||||
|
|
||||||
test('settings page renders content', async ({ page }) => {
|
test('settings page renders content', async ({ page }) => {
|
||||||
await page.goto('/settings');
|
await page.goto('/settings');
|
||||||
const hasContent = await page.locator('text=/settings|general|cloudflare|dns|logging/i').count() > 0;
|
const hasContent = await page.locator('text=/settings|general|dns provider|dns|logging/i').count() > 0;
|
||||||
expect(hasContent).toBe(true);
|
expect(hasContent).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,9 +30,9 @@ test.describe('Settings', () => {
|
|||||||
await expect(page.getByRole('button', { name: /save general settings/i })).toBeEnabled({ timeout: 10000 });
|
await expect(page.getByRole('button', { name: /save general settings/i })).toBeEnabled({ timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('settings page has Cloudflare and DNS sections', async ({ page }) => {
|
test('settings page has DNS Provider and DNS sections', async ({ page }) => {
|
||||||
await page.goto('/settings');
|
await page.goto('/settings');
|
||||||
await expect(page.getByRole('button', { name: /save cloudflare settings/i })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'DNS Providers' })).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: /save dns settings/i })).toBeVisible();
|
await expect(page.getByRole('button', { name: /save dns settings/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ describe('applySyncPayload', () => {
|
|||||||
settings: {
|
settings: {
|
||||||
general: null,
|
general: null,
|
||||||
cloudflare: null,
|
cloudflare: null,
|
||||||
|
dns_provider: null,
|
||||||
authentik: null,
|
authentik: null,
|
||||||
metrics: null,
|
metrics: null,
|
||||||
logging: null,
|
logging: null,
|
||||||
|
|||||||
@@ -217,6 +217,78 @@ describe('proxy-hosts boolean fields', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Authentik forward auth meta round-trip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('proxy-hosts authentik meta', () => {
|
||||||
|
it('stores and retrieves authentik config with excluded_paths', async () => {
|
||||||
|
const meta = {
|
||||||
|
authentik: {
|
||||||
|
enabled: true,
|
||||||
|
outpost_domain: 'outpost.goauthentik.io',
|
||||||
|
outpost_upstream: 'http://authentik:9000',
|
||||||
|
excluded_paths: ['/share/*', '/rest/*'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const host = await insertHost({ meta: JSON.stringify(meta) });
|
||||||
|
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
|
||||||
|
const parsed = JSON.parse(row!.meta!);
|
||||||
|
expect(parsed.authentik.enabled).toBe(true);
|
||||||
|
expect(parsed.authentik.excluded_paths).toEqual(['/share/*', '/rest/*']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores authentik config with protected_paths (no excluded_paths)', async () => {
|
||||||
|
const meta = {
|
||||||
|
authentik: {
|
||||||
|
enabled: true,
|
||||||
|
outpost_domain: 'outpost.goauthentik.io',
|
||||||
|
outpost_upstream: 'http://authentik:9000',
|
||||||
|
protected_paths: ['/admin/*', '/secret/*'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const host = await insertHost({ meta: JSON.stringify(meta) });
|
||||||
|
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
|
||||||
|
const parsed = JSON.parse(row!.meta!);
|
||||||
|
expect(parsed.authentik.protected_paths).toEqual(['/admin/*', '/secret/*']);
|
||||||
|
expect(parsed.authentik.excluded_paths).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CPM forward auth meta round-trip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('proxy-hosts CPM forward auth meta', () => {
|
||||||
|
it('stores and retrieves cpm_forward_auth config with excluded_paths', async () => {
|
||||||
|
const meta = {
|
||||||
|
cpm_forward_auth: {
|
||||||
|
enabled: true,
|
||||||
|
excluded_paths: ['/api/public/*', '/health'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const host = await insertHost({ meta: JSON.stringify(meta) });
|
||||||
|
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
|
||||||
|
const parsed = JSON.parse(row!.meta!);
|
||||||
|
expect(parsed.cpm_forward_auth.enabled).toBe(true);
|
||||||
|
expect(parsed.cpm_forward_auth.excluded_paths).toEqual(['/api/public/*', '/health']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores cpm_forward_auth config with protected_paths (no excluded_paths)', async () => {
|
||||||
|
const meta = {
|
||||||
|
cpm_forward_auth: {
|
||||||
|
enabled: true,
|
||||||
|
protected_paths: ['/admin/*'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const host = await insertHost({ meta: JSON.stringify(meta) });
|
||||||
|
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
|
||||||
|
const parsed = JSON.parse(row!.meta!);
|
||||||
|
expect(parsed.cpm_forward_auth.protected_paths).toEqual(['/admin/*']);
|
||||||
|
expect(parsed.cpm_forward_auth.excluded_paths).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Null meta field
|
// Null meta field
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ vi.mock('@/src/lib/settings', () => ({
|
|||||||
saveLoggingSettings: vi.fn(),
|
saveLoggingSettings: vi.fn(),
|
||||||
getDnsSettings: vi.fn(),
|
getDnsSettings: vi.fn(),
|
||||||
saveDnsSettings: vi.fn(),
|
saveDnsSettings: vi.fn(),
|
||||||
|
getDnsProviderSettings: vi.fn(),
|
||||||
|
saveDnsProviderSettings: vi.fn(),
|
||||||
getUpstreamDnsResolutionSettings: vi.fn(),
|
getUpstreamDnsResolutionSettings: vi.fn(),
|
||||||
saveUpstreamDnsResolutionSettings: vi.fn(),
|
saveUpstreamDnsResolutionSettings: vi.fn(),
|
||||||
getGeoBlockSettings: vi.fn(),
|
getGeoBlockSettings: vi.fn(),
|
||||||
|
|||||||
Reference in New Issue
Block a user