Files
caddy-proxy-manager/app/(dashboard)/proxy-hosts/actions.ts
fuomag9 3a16d6e9b1 Replace next-auth with Better Auth, migrate DB columns to camelCase
- Replace next-auth v5 beta with better-auth v1.6.2 (stable releases)
- Add multi-provider OAuth support with admin UI configuration
- New oauthProviders table with encrypted secrets (AES-256-GCM)
- Env var bootstrap (OAUTH_*) syncs to DB, UI-created providers fully editable
- OAuth provider REST API: GET/POST/PUT/DELETE /api/v1/oauth-providers
- Settings page "Authentication Providers" section for admin management
- Account linking uses new accounts table (multi-provider per user)
- Username plugin for credentials sign-in (replaces email@localhost pattern)
- bcrypt password compatibility (existing hashes work)
- Database-backed sessions via Kysely adapter (bun:sqlite direct)
- Configurable rate limiting via AUTH_RATE_LIMIT_* env vars
- All DB columns migrated from snake_case to camelCase
- All TypeScript types/models migrated to camelCase properties
- Removed casing: "snake_case" from Drizzle config
- Callback URL format: {baseUrl}/api/auth/oauth2/callback/{providerId}
- package-lock.json removed and gitignored (using bun.lock)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:11:48 +02:00

680 lines
25 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { requireAdmin } from "@/src/lib/auth";
import { actionError, actionSuccess, INITIAL_ACTION_STATE, type ActionState } from "@/src/lib/actions";
import {
createProxyHost,
deleteProxyHost,
updateProxyHost,
type ProxyHostAuthentikInput,
type LoadBalancerInput,
type LoadBalancingPolicy,
type DnsResolverInput,
type UpstreamDnsResolutionInput,
type GeoBlockMode,
type WafHostConfig,
type MtlsConfig,
type RedirectRule,
type RewriteConfig,
type CpmForwardAuthInput
} from "@/src/lib/models/proxy-hosts";
import { getCertificate } from "@/src/lib/models/certificates";
import { setForwardAuthAccess } from "@/src/lib/models/forward-auth";
import { getCloudflareSettings, type GeoBlockSettings } from "@/src/lib/settings";
import {
parseCsv,
parseUpstreams,
parseCheckbox,
parseOptionalText,
parseCertificateId,
parseAccessListId,
parseOptionalNumber,
} from "@/src/lib/form-parse";
async function validateAndSanitizeCertificateId(
certificateId: number | null,
cloudflareConfigured: boolean
): Promise<{ certificateId: number | null; warning?: string }> {
// null is valid (Caddy Auto)
if (certificateId === null) {
return { certificateId: null };
}
// Check if certificate exists
const certificate = await getCertificate(certificateId);
if (!certificate) {
// Build helpful warning message
let warning: string;
if (!cloudflareConfigured) {
warning = `Certificate ID ${certificateId} not found. Automatically using 'Managed by Caddy (Auto)'. Note: Without Cloudflare DNS integration, wildcard certificates require port 80 to be accessible for HTTP-01 challenges. Configure Cloudflare in Settings to enable DNS-01 challenges.`;
} else {
warning = `Certificate ID ${certificateId} not found. Automatically using 'Managed by Caddy (Auto)' which will provision certificates automatically using Caddy.`;
}
return { certificateId: null, warning };
}
return { certificateId };
}
function parseAuthentikConfig(formData: FormData): ProxyHostAuthentikInput | undefined {
if (!formData.has("authentik_present")) {
return undefined;
}
const enabledIndicator = formData.has("authentik_enabled_present");
const enabledValue = enabledIndicator
? formData.has("authentik_enabled")
? parseCheckbox(formData.get("authentik_enabled"))
: false
: undefined;
const outpostDomain = parseOptionalText(formData.get("authentik_outpost_domain"));
const outpostUpstream = parseOptionalText(formData.get("authentik_outpost_upstream"));
const authEndpoint = parseOptionalText(formData.get("authentik_auth_endpoint"));
const copyHeaders = parseCsv(formData.get("authentik_copy_headers"));
const trustedProxies = parseCsv(formData.get("authentik_trusted_proxies"));
const protectedPaths = parseCsv(formData.get("authentik_protected_paths"));
const setHostHeader = formData.has("authentik_set_host_header_present")
? parseCheckbox(formData.get("authentik_set_host_header"))
: undefined;
const result: ProxyHostAuthentikInput = {};
if (enabledValue !== undefined) {
result.enabled = enabledValue;
}
if (outpostDomain !== null) {
result.outpostDomain = outpostDomain;
}
if (outpostUpstream !== null) {
result.outpostUpstream = outpostUpstream;
}
if (authEndpoint !== null) {
result.authEndpoint = authEndpoint;
}
if (copyHeaders.length > 0 || formData.has("authentik_copy_headers")) {
result.copyHeaders = copyHeaders;
}
if (trustedProxies.length > 0 || formData.has("authentik_trusted_proxies")) {
result.trustedProxies = trustedProxies;
}
if (protectedPaths.length > 0 || formData.has("authentik_protected_paths")) {
result.protectedPaths = protectedPaths;
}
if (setHostHeader !== undefined) {
result.setOutpostHostHeader = setHostHeader;
}
return Object.keys(result).length > 0 ? result : undefined;
}
function parseCpmForwardAuthConfig(formData: FormData): CpmForwardAuthInput | undefined {
if (!formData.has("cpm_forward_auth_present")) {
return undefined;
}
const enabledIndicator = formData.has("cpm_forward_auth_enabled_present");
const enabledValue = enabledIndicator
? formData.has("cpm_forward_auth_enabled")
? parseCheckbox(formData.get("cpm_forward_auth_enabled"))
: false
: undefined;
const protectedPaths = parseCsv(formData.get("cpm_forward_auth_protected_paths"));
const result: CpmForwardAuthInput = {};
if (enabledValue !== undefined) {
result.enabled = enabledValue;
}
if (protectedPaths.length > 0 || formData.has("cpm_forward_auth_protected_paths")) {
result.protected_paths = protectedPaths.length > 0 ? protectedPaths : null;
}
return Object.keys(result).length > 0 ? result : undefined;
}
function parseRedirectUrl(raw: FormDataEntryValue | null): string {
if (!raw || typeof raw !== "string") return "";
const trimmed = raw.trim();
if (!trimmed) return "";
try {
const url = new URL(trimmed);
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
return trimmed;
} catch {
return "";
}
}
const VALID_LB_POLICIES: LoadBalancingPolicy[] = ["random", "round_robin", "least_conn", "ip_hash", "first", "header", "cookie", "uri_hash"];
const VALID_UPSTREAM_DNS_FAMILIES = ["ipv6", "ipv4", "both"] as const;
function parseLoadBalancerConfig(formData: FormData): LoadBalancerInput | undefined {
if (!formData.has("lb_present")) {
return undefined;
}
const enabledIndicator = formData.has("lb_enabled_present");
const enabledValue = enabledIndicator
? formData.has("lb_enabled")
? parseCheckbox(formData.get("lb_enabled"))
: false
: undefined;
const policyRaw = parseOptionalText(formData.get("lb_policy"));
const policy = policyRaw && VALID_LB_POLICIES.includes(policyRaw as LoadBalancingPolicy)
? (policyRaw as LoadBalancingPolicy)
: undefined;
const policyHeaderField = parseOptionalText(formData.get("lb_policy_header_field"));
const policyCookieName = parseOptionalText(formData.get("lb_policy_cookie_name"));
const policyCookieSecret = parseOptionalText(formData.get("lb_policy_cookie_secret"));
const tryDuration = parseOptionalText(formData.get("lb_try_duration"));
const tryInterval = parseOptionalText(formData.get("lb_try_interval"));
const retries = parseOptionalNumber(formData.get("lb_retries"));
// Active health check
const activeHealthEnabled = formData.has("lb_active_health_enabled_present")
? formData.has("lb_active_health_enabled")
? parseCheckbox(formData.get("lb_active_health_enabled"))
: false
: undefined;
let activeHealthCheck: LoadBalancerInput["activeHealthCheck"] = undefined;
if (activeHealthEnabled !== undefined || formData.has("lb_active_health_uri")) {
activeHealthCheck = {
enabled: activeHealthEnabled,
uri: parseOptionalText(formData.get("lb_active_health_uri")),
port: parseOptionalNumber(formData.get("lb_active_health_port")),
interval: parseOptionalText(formData.get("lb_active_health_interval")),
timeout: parseOptionalText(formData.get("lb_active_health_timeout")),
status: parseOptionalNumber(formData.get("lb_active_health_status")),
body: parseOptionalText(formData.get("lb_active_health_body"))
};
}
// Passive health check
const passiveHealthEnabled = formData.has("lb_passive_health_enabled_present")
? formData.has("lb_passive_health_enabled")
? parseCheckbox(formData.get("lb_passive_health_enabled"))
: false
: undefined;
let passiveHealthCheck: LoadBalancerInput["passiveHealthCheck"] = undefined;
if (passiveHealthEnabled !== undefined || formData.has("lb_passive_health_fail_duration")) {
// Parse unhealthy status codes from comma-separated input
const unhealthyStatusRaw = parseOptionalText(formData.get("lb_passive_health_unhealthy_status"));
let unhealthyStatus: number[] | null = null;
if (unhealthyStatusRaw) {
unhealthyStatus = unhealthyStatusRaw
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => Number.isFinite(n) && n >= 100);
if (unhealthyStatus.length === 0) {
unhealthyStatus = null;
}
}
passiveHealthCheck = {
enabled: passiveHealthEnabled,
failDuration: parseOptionalText(formData.get("lb_passive_health_fail_duration")),
maxFails: parseOptionalNumber(formData.get("lb_passive_health_max_fails")),
unhealthyStatus,
unhealthyLatency: parseOptionalText(formData.get("lb_passive_health_unhealthy_latency"))
};
}
const result: LoadBalancerInput = {};
if (enabledValue !== undefined) {
result.enabled = enabledValue;
}
if (policy !== undefined) {
result.policy = policy;
}
if (policyHeaderField !== null) {
result.policyHeaderField = policyHeaderField;
}
if (policyCookieName !== null) {
result.policyCookieName = policyCookieName;
}
if (policyCookieSecret !== null) {
result.policyCookieSecret = policyCookieSecret;
}
if (tryDuration !== null) {
result.tryDuration = tryDuration;
}
if (tryInterval !== null) {
result.tryInterval = tryInterval;
}
if (retries !== null) {
result.retries = retries;
}
if (activeHealthCheck !== undefined) {
result.activeHealthCheck = activeHealthCheck;
}
if (passiveHealthCheck !== undefined) {
result.passiveHealthCheck = passiveHealthCheck;
}
return Object.keys(result).length > 0 ? result : undefined;
}
function parseGeoBlockConfig(formData: FormData): {
geoblock: GeoBlockSettings | null;
geoblock_mode: GeoBlockMode;
} {
if (!formData.has("geoblock_present")) {
return { geoblock: null, geoblock_mode: "merge" };
}
const enabled = parseCheckbox(formData.get("geoblock_enabled"));
const rawMode = formData.get("geoblock_mode");
const mode: GeoBlockMode = rawMode === "override" ? "override" : "merge";
// Helper to parse a comma-separated string field into a string array
const parseStringList = (key: string): string[] => {
const val = formData.get(key);
if (!val || typeof val !== "string") return [];
return val.split(",").map(s => s.trim()).filter(Boolean);
};
// Helper to parse a comma-separated string field into a number array
const parseNumberList = (key: string): number[] => {
return parseStringList(key)
.map(s => parseInt(s, 10))
.filter(n => !isNaN(n));
};
const config: GeoBlockSettings = {
enabled,
block_countries: parseStringList("geoblock_block_countries"),
block_continents: parseStringList("geoblock_block_continents"),
block_asns: parseNumberList("geoblock_block_asns"),
block_cidrs: parseStringList("geoblock_block_cidrs"),
block_ips: parseStringList("geoblock_block_ips"),
allow_countries: parseStringList("geoblock_allow_countries"),
allow_continents: parseStringList("geoblock_allow_continents"),
allow_asns: parseNumberList("geoblock_allow_asns"),
allow_cidrs: parseStringList("geoblock_allow_cidrs"),
allow_ips: parseStringList("geoblock_allow_ips"),
trusted_proxies: parseStringList("geoblock_trusted_proxies"),
fail_closed: formData.get("geoblock_fail_closed") === "on",
response_status: (() => {
const s = parseOptionalNumber(formData.get("geoblock_response_status")) ?? 403;
return s >= 100 && s <= 599 ? s : 403;
})(),
response_body: parseOptionalText(formData.get("geoblock_response_body")) ?? "Forbidden",
response_headers: parseResponseHeaders(formData),
redirect_url: parseRedirectUrl(formData.get("geoblock_redirect_url")),
};
return { geoblock: config, geoblock_mode: mode };
}
// Helper: parse response headers from geoblock_response_headers_keys[] and geoblock_response_headers_values[]
function parseResponseHeaders(formData: FormData): Record<string, string> {
const keys = formData.getAll("geoblock_response_headers_keys[]") as string[];
const values = formData.getAll("geoblock_response_headers_values[]") as string[];
const headers: Record<string, string> = {};
keys.forEach((key, i) => {
const trimmed = key.trim();
if (trimmed && /^[a-zA-Z0-9\-_]+$/.test(trimmed)) {
headers[trimmed] = (values[i] ?? "").trim();
}
});
return headers;
}
function parseWafConfig(formData: FormData): { waf?: WafHostConfig | null } {
if (!formData.has("waf_present")) return {};
const enabled = parseCheckbox(formData.get("waf_enabled"));
const rawMode = formData.get("waf_mode");
const wafMode: WafHostConfig["waf_mode"] = rawMode === "override" ? "override" : "merge";
const rawEngineMode = formData.get("waf_engine_mode");
const engineMode: WafHostConfig["mode"] =
rawEngineMode === "On" ? "On" : rawEngineMode === "Off" ? "Off" : undefined;
const loadCrs = parseCheckbox(formData.get("waf_load_owasp_crs"));
const customDirectives = typeof formData.get("waf_custom_directives") === "string"
? (formData.get("waf_custom_directives") as string).trim()
: "";
const rawExcl = formData.get("waf_excluded_rule_ids");
const excluded_rule_ids: number[] = rawExcl
? (JSON.parse(rawExcl as string) as unknown[]).filter((x): x is number => Number.isInteger(x) && (x as number) > 0)
: [];
if (!enabled) {
return { waf: { enabled: false, waf_mode: wafMode } };
}
return {
waf: {
enabled: true,
mode: engineMode,
load_owasp_crs: loadCrs,
custom_directives: customDirectives,
excluded_rule_ids,
waf_mode: wafMode,
}
};
}
function parseDnsResolverConfig(formData: FormData): DnsResolverInput | undefined {
if (!formData.has("dns_present")) {
return undefined;
}
const enabledIndicator = formData.has("dns_enabled_present");
const enabledValue = enabledIndicator
? formData.has("dns_enabled")
? parseCheckbox(formData.get("dns_enabled"))
: false
: undefined;
// Parse resolvers from newline-separated input
const resolversRaw = parseOptionalText(formData.get("dns_resolvers"));
let resolvers: string[] | undefined = undefined;
if (resolversRaw || formData.has("dns_resolvers")) {
resolvers = resolversRaw
? resolversRaw
.split(/[\n,]/)
.map((s) => s.trim())
.filter((s) => s.length > 0)
: [];
}
// Parse fallbacks from newline-separated input
const fallbacksRaw = parseOptionalText(formData.get("dns_fallbacks"));
let fallbacks: string[] | null = null;
if (fallbacksRaw) {
fallbacks = fallbacksRaw
.split(/[\n,]/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
if (fallbacks.length === 0) {
fallbacks = null;
}
}
const timeout = parseOptionalText(formData.get("dns_timeout"));
const result: DnsResolverInput = {};
if (enabledValue !== undefined) {
result.enabled = enabledValue;
}
if (resolvers !== undefined) {
result.resolvers = resolvers;
}
if (fallbacks !== null) {
result.fallbacks = fallbacks;
}
if (timeout !== null) {
result.timeout = timeout;
}
return Object.keys(result).length > 0 ? result : undefined;
}
function parseMtlsConfig(formData: FormData): MtlsConfig | null {
if (!formData.has("mtls_present")) return null;
const enabled = formData.get("mtls_enabled") === "true";
if (!enabled) return null;
const certIds = formData.getAll("mtls_cert_id").map(Number).filter(n => Number.isFinite(n) && n > 0);
const roleIds = formData.getAll("mtls_role_id").map(Number).filter(n => Number.isFinite(n) && n > 0);
return { enabled, trusted_client_cert_ids: certIds, trusted_role_ids: roleIds };
}
function parseRedirectsConfig(formData: FormData): RedirectRule[] | null {
const raw = formData.get("redirects_json");
if (!raw || typeof raw !== "string") return null;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return null;
return parsed.filter(
(r) =>
r &&
typeof r.from === "string" &&
typeof r.to === "string" &&
[301, 302, 307, 308].includes(r.status)
) as RedirectRule[];
} catch {
return null;
}
}
function parseLocationRulesConfig(formData: FormData): import("@/src/lib/models/proxy-hosts").LocationRule[] | null {
const raw = formData.get("location_rules_json");
if (!raw || typeof raw !== "string") return null;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return null;
return parsed;
} catch {
return null;
}
}
function parseRewriteConfig(formData: FormData): RewriteConfig | null {
const prefix = formData.get("rewrite_path_prefix");
if (!prefix || typeof prefix !== "string" || !prefix.trim()) return null;
return { path_prefix: prefix.trim() };
}
function parseUpstreamDnsResolutionConfig(formData: FormData): UpstreamDnsResolutionInput | undefined {
if (!formData.has("upstream_dns_resolution_present")) {
return undefined;
}
const modeRaw = parseOptionalText(formData.get("upstream_dns_resolution_mode")) ?? "inherit";
const familyRaw = parseOptionalText(formData.get("upstream_dns_resolution_family")) ?? "inherit";
const result: UpstreamDnsResolutionInput = {};
if (modeRaw === "enabled") {
result.enabled = true;
} else if (modeRaw === "disabled") {
result.enabled = false;
} else if (modeRaw === "inherit") {
result.enabled = null;
}
if (familyRaw === "inherit") {
result.family = null;
} else if (VALID_UPSTREAM_DNS_FAMILIES.includes(familyRaw as typeof VALID_UPSTREAM_DNS_FAMILIES[number])) {
result.family = familyRaw as "ipv6" | "ipv4" | "both";
}
return Object.keys(result).length > 0 ? result : undefined;
}
export async function createProxyHostAction(
_prevState: ActionState = INITIAL_ACTION_STATE,
formData: FormData
): Promise<ActionState> {
void _prevState;
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
// Parse certificateId safely
const parsedCertificateId = parseCertificateId(formData.get("certificate_id"));
// Validate certificate exists and get sanitized value
const cloudflareSettings = await getCloudflareSettings();
const cloudflareConfigured = !!(cloudflareSettings?.apiToken);
const { certificateId, warning } = await validateAndSanitizeCertificateId(parsedCertificateId, cloudflareConfigured);
// Log warning if certificate was auto-fallback
if (warning) {
console.warn(`[createProxyHostAction] ${warning}`);
}
const host = await createProxyHost(
{
name: String(formData.get("name") ?? "Untitled"),
domains: parseCsv(formData.get("domains")),
upstreams: parseUpstreams(formData.get("upstreams")),
certificateId: certificateId,
accessListId: parseAccessListId(formData.get("access_list_id")),
sslForced: formData.has("ssl_forced_present") ? parseCheckbox(formData.get("ssl_forced")) : undefined,
hstsSubdomains: parseCheckbox(formData.get("hsts_subdomains")),
skipHttpsHostnameValidation: parseCheckbox(formData.get("skip_https_hostname_validation")),
enabled: parseCheckbox(formData.get("enabled")),
customPreHandlersJson: parseOptionalText(formData.get("custom_pre_handlers_json")),
customReverseProxyJson: parseOptionalText(formData.get("custom_reverse_proxy_json")),
authentik: parseAuthentikConfig(formData),
cpmForwardAuth: parseCpmForwardAuthConfig(formData),
loadBalancer: parseLoadBalancerConfig(formData),
dnsResolver: parseDnsResolverConfig(formData),
upstreamDnsResolution: parseUpstreamDnsResolutionConfig(formData),
...parseGeoBlockConfig(formData),
...parseWafConfig(formData),
mtls: parseMtlsConfig(formData),
redirects: parseRedirectsConfig(formData),
rewrite: parseRewriteConfig(formData),
locationRules: parseLocationRulesConfig(formData),
},
userId
);
// Save forward auth access if CPM forward auth is enabled
const faUserIds = formData.getAll("cpm_fa_user_id").map((v) => Number(v)).filter((n) => n > 0);
const faGroupIds = formData.getAll("cpm_fa_group_id").map((v) => Number(v)).filter((n) => n > 0);
if (host.cpmForwardAuth?.enabled && (faUserIds.length > 0 || faGroupIds.length > 0)) {
await setForwardAuthAccess(host.id, { userIds: faUserIds, groupIds: faGroupIds }, userId);
}
revalidatePath("/proxy-hosts");
// Return success with warning if applicable
if (warning) {
return actionSuccess(`Proxy host created using Caddy Auto certificate management. ${warning}`);
}
return actionSuccess("Proxy host created and queued for Caddy reload.");
} catch (error) {
console.error("Failed to create proxy host:", error);
return actionError(error, "Failed to create proxy host. Please check the logs for details.");
}
}
export async function updateProxyHostAction(
id: number,
_prevState: ActionState = INITIAL_ACTION_STATE,
formData: FormData
): Promise<ActionState> {
void _prevState;
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
const boolField = (key: string) => (formData.has(`${key}_present`) ? parseCheckbox(formData.get(key)) : undefined);
// Parse and validate certificate_id if present
let certificateId: number | null | undefined = undefined;
let warning: string | undefined;
if (formData.has("certificate_id")) {
const parsedCertificateId = parseCertificateId(formData.get("certificate_id"));
// Validate certificate exists and get sanitized value
const cloudflareSettings = await getCloudflareSettings();
const cloudflareConfigured = !!(cloudflareSettings?.apiToken);
const validation = await validateAndSanitizeCertificateId(parsedCertificateId, cloudflareConfigured);
certificateId = validation.certificateId;
warning = validation.warning;
// Log warning if certificate was auto-fallback
if (warning) {
console.warn(`[updateProxyHostAction] ${warning}`);
}
}
await updateProxyHost(
id,
{
name: formData.get("name") ? String(formData.get("name")) : undefined,
domains: formData.get("domains") ? parseCsv(formData.get("domains")) : undefined,
upstreams: formData.get("upstreams") ? parseUpstreams(formData.get("upstreams")) : undefined,
certificateId: certificateId,
accessListId: formData.has("access_list_id")
? parseAccessListId(formData.get("access_list_id"))
: undefined,
hstsSubdomains: boolField("hsts_subdomains"),
skipHttpsHostnameValidation: boolField("skip_https_hostname_validation"),
enabled: boolField("enabled"),
customPreHandlersJson: formData.has("custom_pre_handlers_json")
? parseOptionalText(formData.get("custom_pre_handlers_json"))
: undefined,
customReverseProxyJson: formData.has("custom_reverse_proxy_json")
? parseOptionalText(formData.get("custom_reverse_proxy_json"))
: undefined,
authentik: parseAuthentikConfig(formData),
cpmForwardAuth: parseCpmForwardAuthConfig(formData),
loadBalancer: parseLoadBalancerConfig(formData),
dnsResolver: parseDnsResolverConfig(formData),
upstreamDnsResolution: parseUpstreamDnsResolutionConfig(formData),
...parseGeoBlockConfig(formData),
...parseWafConfig(formData),
mtls: formData.has("mtls_present") ? parseMtlsConfig(formData) : undefined,
redirects: formData.has("redirects_json") ? parseRedirectsConfig(formData) : undefined,
rewrite: formData.has("rewrite_path_prefix") ? parseRewriteConfig(formData) : undefined,
locationRules: formData.has("location_rules_json") ? parseLocationRulesConfig(formData) : undefined,
},
userId
);
// Save forward auth access if the section is present in the form
if (formData.has("cpm_forward_auth_present")) {
const faUserIds = formData.getAll("cpm_fa_user_id").map((v) => Number(v)).filter((n) => n > 0);
const faGroupIds = formData.getAll("cpm_fa_group_id").map((v) => Number(v)).filter((n) => n > 0);
await setForwardAuthAccess(id, { userIds: faUserIds, groupIds: faGroupIds }, userId);
}
revalidatePath("/proxy-hosts");
// Return success with warning if applicable
if (warning) {
return actionSuccess(`Proxy host updated using Caddy Auto certificate management. ${warning}`);
}
return actionSuccess("Proxy host updated.");
} catch (error) {
console.error(`Failed to update proxy host ${id}:`, error);
return actionError(error, "Failed to update proxy host. Please check the logs for details.");
}
}
export async function deleteProxyHostAction(
id: number,
_prevState: ActionState = INITIAL_ACTION_STATE
): Promise<ActionState> {
void _prevState;
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
await deleteProxyHost(id, userId);
revalidatePath("/proxy-hosts");
return actionSuccess("Proxy host deleted.");
} catch (error) {
console.error(`Failed to delete proxy host ${id}:`, error);
return actionError(error, "Failed to delete proxy host. Please check the logs for details.");
}
}
export async function toggleProxyHostAction(
id: number,
enabled: boolean
): Promise<ActionState> {
try {
const session = await requireAdmin();
const userId = Number(session.user.id);
await updateProxyHost(id, { enabled }, userId);
revalidatePath("/proxy-hosts");
return actionSuccess(`Proxy host ${enabled ? "enabled" : "disabled"}.`);
} catch (error) {
console.error(`Failed to toggle proxy host ${id}:`, error);
return actionError(error, "Failed to toggle proxy host. Please check the logs for details.");
}
}