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>
This commit is contained in:
@@ -32,7 +32,7 @@ export function CpmForwardAuthFields({
|
||||
groups = [],
|
||||
currentAccess,
|
||||
}: {
|
||||
cpmForwardAuth?: ProxyHost["cpm_forward_auth"] | null;
|
||||
cpmForwardAuth?: ProxyHost["cpmForwardAuth"] | null;
|
||||
users?: UserEntry[];
|
||||
groups?: GroupEntry[];
|
||||
currentAccess?: ForwardAuthAccessData | null;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ProxyHost } from "@/lib/models/proxy-hosts";
|
||||
export function DnsResolverFields({
|
||||
dnsResolver
|
||||
}: {
|
||||
dnsResolver?: ProxyHost["dns_resolver"] | null;
|
||||
dnsResolver?: ProxyHost["dnsResolver"] | null;
|
||||
}) {
|
||||
const initial = dnsResolver ?? null;
|
||||
const [enabled, setEnabled] = useState(initial?.enabled ?? false);
|
||||
|
||||
@@ -87,8 +87,8 @@ export function CreateHostDialog({
|
||||
</Alert>
|
||||
)}
|
||||
<SettingsToggles
|
||||
hstsSubdomains={initialData?.hsts_subdomains}
|
||||
skipHttpsValidation={initialData?.skip_https_hostname_validation}
|
||||
hstsSubdomains={initialData?.hstsSubdomains}
|
||||
skipHttpsValidation={initialData?.skipHttpsHostnameValidation}
|
||||
enabled={true}
|
||||
/>
|
||||
<div>
|
||||
@@ -118,7 +118,7 @@ export function CreateHostDialog({
|
||||
<UpstreamInput defaultUpstreams={initialData?.upstreams} />
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Certificate</label>
|
||||
<Select name="certificate_id" defaultValue={String(initialData?.certificate_id ?? "__none__")}>
|
||||
<Select name="certificate_id" defaultValue={String(initialData?.certificateId ?? "__none__")}>
|
||||
<SelectTrigger aria-label="Certificate">
|
||||
<SelectValue placeholder="Managed by Caddy (Auto)" />
|
||||
</SelectTrigger>
|
||||
@@ -134,7 +134,7 @@ export function CreateHostDialog({
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Access List</label>
|
||||
<Select name="access_list_id" defaultValue={String(initialData?.access_list_id ?? "__none__")}>
|
||||
<Select name="access_list_id" defaultValue={String(initialData?.accessListId ?? "__none__")}>
|
||||
<SelectTrigger aria-label="Access List">
|
||||
<SelectValue placeholder="None" />
|
||||
</SelectTrigger>
|
||||
@@ -149,14 +149,14 @@ export function CreateHostDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<RedirectsFields initialData={initialData?.redirects} />
|
||||
<LocationRulesFields initialData={initialData?.location_rules} />
|
||||
<LocationRulesFields initialData={initialData?.locationRules} />
|
||||
<RewriteFields initialData={initialData?.rewrite} />
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Custom Pre-Handlers (JSON)</label>
|
||||
<Textarea
|
||||
name="custom_pre_handlers_json"
|
||||
placeholder='[{"handler": "headers", ...}]'
|
||||
defaultValue={initialData?.custom_pre_handlers_json ?? ""}
|
||||
defaultValue={initialData?.customPreHandlersJson ?? ""}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Optional JSON array of Caddy handlers</p>
|
||||
@@ -166,7 +166,7 @@ export function CreateHostDialog({
|
||||
<Textarea
|
||||
name="custom_reverse_proxy_json"
|
||||
placeholder='{"headers": {"request": {...}}}'
|
||||
defaultValue={initialData?.custom_reverse_proxy_json ?? ""}
|
||||
defaultValue={initialData?.customReverseProxyJson ?? ""}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
@@ -175,13 +175,13 @@ export function CreateHostDialog({
|
||||
</div>
|
||||
<AuthentikFields defaults={authentikDefaults} authentik={initialData?.authentik} />
|
||||
<CpmForwardAuthFields
|
||||
cpmForwardAuth={initialData?.cpm_forward_auth}
|
||||
cpmForwardAuth={initialData?.cpmForwardAuth}
|
||||
users={forwardAuthUsers}
|
||||
groups={forwardAuthGroups}
|
||||
/>
|
||||
<LoadBalancerFields loadBalancer={initialData?.load_balancer} />
|
||||
<DnsResolverFields dnsResolver={initialData?.dns_resolver} />
|
||||
<UpstreamDnsResolutionFields upstreamDnsResolution={initialData?.upstream_dns_resolution} />
|
||||
<LoadBalancerFields loadBalancer={initialData?.loadBalancer} />
|
||||
<DnsResolverFields dnsResolver={initialData?.dnsResolver} />
|
||||
<UpstreamDnsResolutionFields upstreamDnsResolution={initialData?.upstreamDnsResolution} />
|
||||
<GeoBlockFields />
|
||||
<WafFields value={initialData?.waf} />
|
||||
<MtlsFields
|
||||
@@ -246,8 +246,8 @@ export function EditHostDialog({
|
||||
</Alert>
|
||||
)}
|
||||
<SettingsToggles
|
||||
hstsSubdomains={host.hsts_subdomains}
|
||||
skipHttpsValidation={host.skip_https_hostname_validation}
|
||||
hstsSubdomains={host.hstsSubdomains}
|
||||
skipHttpsValidation={host.skipHttpsHostnameValidation}
|
||||
enabled={host.enabled}
|
||||
/>
|
||||
<div>
|
||||
@@ -269,7 +269,7 @@ export function EditHostDialog({
|
||||
<UpstreamInput defaultUpstreams={host.upstreams} />
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Certificate</label>
|
||||
<Select name="certificate_id" defaultValue={String(host.certificate_id ?? "__none__")}>
|
||||
<Select name="certificate_id" defaultValue={String(host.certificateId ?? "__none__")}>
|
||||
<SelectTrigger aria-label="Certificate">
|
||||
<SelectValue placeholder="Managed by Caddy (Auto)" />
|
||||
</SelectTrigger>
|
||||
@@ -285,7 +285,7 @@ export function EditHostDialog({
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Access List</label>
|
||||
<Select name="access_list_id" defaultValue={String(host.access_list_id ?? "__none__")}>
|
||||
<Select name="access_list_id" defaultValue={String(host.accessListId ?? "__none__")}>
|
||||
<SelectTrigger aria-label="Access List">
|
||||
<SelectValue placeholder="None" />
|
||||
</SelectTrigger>
|
||||
@@ -300,13 +300,13 @@ export function EditHostDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<RedirectsFields initialData={host.redirects} />
|
||||
<LocationRulesFields initialData={host.location_rules} />
|
||||
<LocationRulesFields initialData={host.locationRules} />
|
||||
<RewriteFields initialData={host.rewrite} />
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Custom Pre-Handlers (JSON)</label>
|
||||
<Textarea
|
||||
name="custom_pre_handlers_json"
|
||||
defaultValue={host.custom_pre_handlers_json ?? ""}
|
||||
defaultValue={host.customPreHandlersJson ?? ""}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Optional JSON array of Caddy handlers</p>
|
||||
@@ -315,7 +315,7 @@ export function EditHostDialog({
|
||||
<label className="text-sm font-medium mb-1 block">Custom Reverse Proxy (JSON)</label>
|
||||
<Textarea
|
||||
name="custom_reverse_proxy_json"
|
||||
defaultValue={host.custom_reverse_proxy_json ?? ""}
|
||||
defaultValue={host.customReverseProxyJson ?? ""}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
@@ -324,18 +324,18 @@ export function EditHostDialog({
|
||||
</div>
|
||||
<AuthentikFields authentik={host.authentik} />
|
||||
<CpmForwardAuthFields
|
||||
cpmForwardAuth={host.cpm_forward_auth}
|
||||
cpmForwardAuth={host.cpmForwardAuth}
|
||||
users={forwardAuthUsers}
|
||||
groups={forwardAuthGroups}
|
||||
currentAccess={forwardAuthAccess}
|
||||
/>
|
||||
<LoadBalancerFields loadBalancer={host.load_balancer} />
|
||||
<DnsResolverFields dnsResolver={host.dns_resolver} />
|
||||
<UpstreamDnsResolutionFields upstreamDnsResolution={host.upstream_dns_resolution} />
|
||||
<LoadBalancerFields loadBalancer={host.loadBalancer} />
|
||||
<DnsResolverFields dnsResolver={host.dnsResolver} />
|
||||
<UpstreamDnsResolutionFields upstreamDnsResolution={host.upstreamDnsResolution} />
|
||||
<GeoBlockFields
|
||||
initialValues={{
|
||||
geoblock: host.geoblock,
|
||||
geoblock_mode: host.geoblock_mode,
|
||||
geoblock_mode: host.geoblockMode,
|
||||
}}
|
||||
/>
|
||||
<WafFields value={host.waf} />
|
||||
|
||||
@@ -19,7 +19,7 @@ const LOAD_BALANCING_POLICIES = [
|
||||
export function LoadBalancerFields({
|
||||
loadBalancer
|
||||
}: {
|
||||
loadBalancer?: ProxyHost["load_balancer"] | null;
|
||||
loadBalancer?: ProxyHost["loadBalancer"] | null;
|
||||
}) {
|
||||
const initial = loadBalancer ?? null;
|
||||
const [enabled, setEnabled] = useState(initial?.enabled ?? false);
|
||||
|
||||
@@ -37,13 +37,13 @@ export function MtlsFields({ value, caCertificates, issuedClientCerts = [], prox
|
||||
const [editRule, setEditRule] = useState<MtlsAccessRule | null>(null);
|
||||
|
||||
const isEditMode = !!proxyHostId;
|
||||
const activeCerts = issuedClientCerts.filter(c => !c.revoked_at);
|
||||
const activeCerts = issuedClientCerts.filter(c => !c.revokedAt);
|
||||
|
||||
const certsByCA = new Map<number, IssuedClientCertificate[]>();
|
||||
for (const cert of activeCerts) {
|
||||
const list = certsByCA.get(cert.ca_certificate_id) ?? [];
|
||||
const list = certsByCA.get(cert.caCertificateId) ?? [];
|
||||
list.push(cert);
|
||||
certsByCA.set(cert.ca_certificate_id, list);
|
||||
certsByCA.set(cert.caCertificateId, list);
|
||||
}
|
||||
|
||||
const loadRules = useCallback(() => {
|
||||
@@ -143,7 +143,7 @@ export function MtlsFields({ value, caCertificates, issuedClientCerts = [], prox
|
||||
<span className="text-sm font-medium">{role.name}</span>
|
||||
{role.description && <span className="text-xs text-muted-foreground ml-2">— {role.description}</span>}
|
||||
</label>
|
||||
<Badge variant="outline" className="text-xs shrink-0">{role.certificate_count} certs</Badge>
|
||||
<Badge variant="outline" className="text-xs shrink-0">{role.certificateCount} certs</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -201,10 +201,10 @@ export function MtlsFields({ value, caCertificates, issuedClientCerts = [], prox
|
||||
className="ml-4"
|
||||
/>
|
||||
<label className="min-w-0 flex-1 cursor-pointer" onClick={() => toggleCert(cert.id)}>
|
||||
<span className="text-sm">{cert.common_name}</span>
|
||||
<span className="text-sm">{cert.commonName}</span>
|
||||
</label>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
expires {new Date(cert.valid_to).toLocaleDateString()}
|
||||
expires {new Date(cert.validTo).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -249,20 +249,20 @@ export function MtlsFields({ value, caCertificates, issuedClientCerts = [], prox
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{rules.map(rule => (
|
||||
<div key={rule.id} className="group flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm">
|
||||
<code className="shrink-0 text-xs bg-muted px-1.5 py-0.5 rounded font-mono">{rule.path_pattern}</code>
|
||||
{rule.deny_all ? (
|
||||
<code className="shrink-0 text-xs bg-muted px-1.5 py-0.5 rounded font-mono">{rule.pathPattern}</code>
|
||||
{rule.denyAll ? (
|
||||
<Badge variant="destructive" className="text-xs gap-1"><Ban className="h-3 w-3" /> Deny</Badge>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1 flex-1 min-w-0">
|
||||
{rule.allowed_role_ids.map(roleId => {
|
||||
{rule.allowedRoleIds.map(roleId => {
|
||||
const role = mtlsRoles.find(r => r.id === roleId);
|
||||
return <Badge key={`r-${roleId}`} variant="secondary" className="text-xs">{role?.name ?? `#${roleId}`}</Badge>;
|
||||
})}
|
||||
{rule.allowed_cert_ids.map(certId => {
|
||||
{rule.allowedCertIds.map(certId => {
|
||||
const cert = issuedClientCerts.find(c => c.id === certId);
|
||||
return <Badge key={`c-${certId}`} variant="outline" className="text-xs">{cert?.common_name ?? `#${certId}`}</Badge>;
|
||||
return <Badge key={`c-${certId}`} variant="outline" className="text-xs">{cert?.commonName ?? `#${certId}`}</Badge>;
|
||||
})}
|
||||
{rule.allowed_role_ids.length === 0 && rule.allowed_cert_ids.length === 0 && (
|
||||
{rule.allowedRoleIds.length === 0 && rule.allowedCertIds.length === 0 && (
|
||||
<span className="text-xs text-destructive italic">No roles/certs — effectively denied</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -297,12 +297,12 @@ function RuleDialog({ onClose, proxyHostId, roles, activeCerts, title, submitLab
|
||||
onClose: () => void; proxyHostId: number; roles: MtlsRole[]; activeCerts: IssuedClientCertificate[];
|
||||
title: string; submitLabel: string; existing?: MtlsAccessRule; onSaved: () => void;
|
||||
}) {
|
||||
const [pathPattern, setPathPattern] = useState(existing?.path_pattern ?? "*");
|
||||
const [pathPattern, setPathPattern] = useState(existing?.pathPattern ?? "*");
|
||||
const [priority, setPriority] = useState(String(existing?.priority ?? 0));
|
||||
const [description, setDescription] = useState(existing?.description ?? "");
|
||||
const [selectedRoleIds, setSelectedRoleIds] = useState<number[]>(existing?.allowed_role_ids ?? []);
|
||||
const [selectedCertIds, setSelectedCertIds] = useState<number[]>(existing?.allowed_cert_ids ?? []);
|
||||
const [denyAll, setDenyAll] = useState(existing?.deny_all ?? false);
|
||||
const [selectedRoleIds, setSelectedRoleIds] = useState<number[]>(existing?.allowedRoleIds ?? []);
|
||||
const [selectedCertIds, setSelectedCertIds] = useState<number[]>(existing?.allowedCertIds ?? []);
|
||||
const [denyAll, setDenyAll] = useState(existing?.denyAll ?? false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@@ -373,7 +373,7 @@ function RuleDialog({ onClose, proxyHostId, roles, activeCerts, title, submitLab
|
||||
{activeCerts.map(cert => (
|
||||
<div key={cert.id} className="flex items-center gap-2 py-1 rounded hover:bg-muted/50 px-1">
|
||||
<Checkbox checked={selectedCertIds.includes(cert.id)} onCheckedChange={() => setSelectedCertIds(prev => prev.includes(cert.id) ? prev.filter(i => i !== cert.id) : [...prev, cert.id])} />
|
||||
<label className="text-sm cursor-pointer flex-1" onClick={() => setSelectedCertIds(prev => prev.includes(cert.id) ? prev.filter(i => i !== cert.id) : [...prev, cert.id])}>{cert.common_name}</label>
|
||||
<label className="text-sm cursor-pointer flex-1" onClick={() => setSelectedCertIds(prev => prev.includes(cert.id) ? prev.filter(i => i !== cert.id) : [...prev, cert.id])}>{cert.commonName}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
type ToggleSetting = {
|
||||
name: "hsts_subdomains" | "skip_https_hostname_validation";
|
||||
name: "hstsSubdomains" | "skipHttpsHostnameValidation";
|
||||
label: string;
|
||||
description: string;
|
||||
defaultChecked: boolean;
|
||||
@@ -21,8 +21,8 @@ export function SettingsToggles({
|
||||
enabled = true
|
||||
}: SettingsTogglesProps) {
|
||||
const [values, setValues] = useState({
|
||||
hsts_subdomains: hstsSubdomains,
|
||||
skip_https_hostname_validation: skipHttpsValidation,
|
||||
hstsSubdomains: hstsSubdomains,
|
||||
skipHttpsHostnameValidation: skipHttpsValidation,
|
||||
enabled: enabled
|
||||
});
|
||||
|
||||
@@ -32,16 +32,16 @@ export function SettingsToggles({
|
||||
|
||||
const settings: ToggleSetting[] = [
|
||||
{
|
||||
name: "hsts_subdomains",
|
||||
name: "hstsSubdomains",
|
||||
label: "HSTS Subdomains",
|
||||
description: "Include subdomains in the Strict-Transport-Security header",
|
||||
defaultChecked: values.hsts_subdomains,
|
||||
defaultChecked: values.hstsSubdomains,
|
||||
},
|
||||
{
|
||||
name: "skip_https_hostname_validation",
|
||||
name: "skipHttpsHostnameValidation",
|
||||
label: "Skip HTTPS Validation",
|
||||
description: "Skip SSL certificate hostname verification for backend connections",
|
||||
defaultChecked: values.skip_https_hostname_validation,
|
||||
defaultChecked: values.skipHttpsHostnameValidation,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function toFamilyMode(family: "ipv6" | "ipv4" | "both" | null | undefined): Fami
|
||||
export function UpstreamDnsResolutionFields({
|
||||
upstreamDnsResolution
|
||||
}: {
|
||||
upstreamDnsResolution?: ProxyHost["upstream_dns_resolution"] | null;
|
||||
upstreamDnsResolution?: ProxyHost["upstreamDnsResolution"] | null;
|
||||
}) {
|
||||
const mode = toResolutionMode(upstreamDnsResolution?.enabled);
|
||||
const family = toFamilyMode(upstreamDnsResolution?.family);
|
||||
|
||||
Reference in New Issue
Block a user