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:
@@ -133,10 +133,10 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
label: "Features",
|
||||
render: (host: ProxyHost) => {
|
||||
const badges = [
|
||||
host.certificate_id && (
|
||||
host.certificateId && (
|
||||
<Badge key="tls" variant="info" className="text-[10px] px-1.5 py-0">TLS</Badge>
|
||||
),
|
||||
host.access_list_id && (
|
||||
host.accessListId && (
|
||||
<Badge key="auth" variant="warning" className="text-[10px] px-1.5 py-0">
|
||||
<Shield className="h-2.5 w-2.5 mr-0.5" />Auth
|
||||
</Badge>
|
||||
@@ -156,7 +156,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
<MapPin className="h-2.5 w-2.5 mr-0.5" />Geo
|
||||
</Badge>
|
||||
),
|
||||
host.load_balancer?.enabled && (
|
||||
host.loadBalancer?.enabled && (
|
||||
<Badge key="lb" variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||
<Scale className="h-2.5 w-2.5 mr-0.5" />LB
|
||||
</Badge>
|
||||
@@ -244,7 +244,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, caC
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<StatusChip status={host.enabled ? "active" : "inactive"} />
|
||||
{host.certificate_id && <Badge variant="info" className="text-[10px] px-1.5 py-0">TLS</Badge>}
|
||||
{host.certificateId && <Badge variant="info" className="text-[10px] px-1.5 py-0">TLS</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
|
||||
@@ -497,7 +497,7 @@ export async function createProxyHostAction(
|
||||
const session = await requireAdmin();
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
// Parse certificate_id safely
|
||||
// Parse certificateId safely
|
||||
const parsedCertificateId = parseCertificateId(formData.get("certificate_id"));
|
||||
|
||||
// Validate certificate exists and get sanitized value
|
||||
@@ -516,25 +516,25 @@ export async function createProxyHostAction(
|
||||
name: String(formData.get("name") ?? "Untitled"),
|
||||
domains: parseCsv(formData.get("domains")),
|
||||
upstreams: parseUpstreams(formData.get("upstreams")),
|
||||
certificate_id: certificateId,
|
||||
access_list_id: parseAccessListId(formData.get("access_list_id")),
|
||||
ssl_forced: formData.has("ssl_forced_present") ? parseCheckbox(formData.get("ssl_forced")) : undefined,
|
||||
hsts_subdomains: parseCheckbox(formData.get("hsts_subdomains")),
|
||||
skip_https_hostname_validation: parseCheckbox(formData.get("skip_https_hostname_validation")),
|
||||
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")),
|
||||
custom_pre_handlers_json: parseOptionalText(formData.get("custom_pre_handlers_json")),
|
||||
custom_reverse_proxy_json: parseOptionalText(formData.get("custom_reverse_proxy_json")),
|
||||
customPreHandlersJson: parseOptionalText(formData.get("custom_pre_handlers_json")),
|
||||
customReverseProxyJson: parseOptionalText(formData.get("custom_reverse_proxy_json")),
|
||||
authentik: parseAuthentikConfig(formData),
|
||||
cpm_forward_auth: parseCpmForwardAuthConfig(formData),
|
||||
load_balancer: parseLoadBalancerConfig(formData),
|
||||
dns_resolver: parseDnsResolverConfig(formData),
|
||||
upstream_dns_resolution: parseUpstreamDnsResolutionConfig(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),
|
||||
location_rules: parseLocationRulesConfig(formData),
|
||||
locationRules: parseLocationRulesConfig(formData),
|
||||
},
|
||||
userId
|
||||
);
|
||||
@@ -542,7 +542,7 @@ export async function createProxyHostAction(
|
||||
// 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.cpm_forward_auth?.enabled && (faUserIds.length > 0 || faGroupIds.length > 0)) {
|
||||
if (host.cpmForwardAuth?.enabled && (faUserIds.length > 0 || faGroupIds.length > 0)) {
|
||||
await setForwardAuthAccess(host.id, { userIds: faUserIds, groupIds: faGroupIds }, userId);
|
||||
}
|
||||
|
||||
@@ -597,30 +597,30 @@ export async function updateProxyHostAction(
|
||||
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,
|
||||
certificate_id: certificateId,
|
||||
access_list_id: formData.has("access_list_id")
|
||||
certificateId: certificateId,
|
||||
accessListId: formData.has("access_list_id")
|
||||
? parseAccessListId(formData.get("access_list_id"))
|
||||
: undefined,
|
||||
hsts_subdomains: boolField("hsts_subdomains"),
|
||||
skip_https_hostname_validation: boolField("skip_https_hostname_validation"),
|
||||
hstsSubdomains: boolField("hsts_subdomains"),
|
||||
skipHttpsHostnameValidation: boolField("skip_https_hostname_validation"),
|
||||
enabled: boolField("enabled"),
|
||||
custom_pre_handlers_json: formData.has("custom_pre_handlers_json")
|
||||
customPreHandlersJson: formData.has("custom_pre_handlers_json")
|
||||
? parseOptionalText(formData.get("custom_pre_handlers_json"))
|
||||
: undefined,
|
||||
custom_reverse_proxy_json: formData.has("custom_reverse_proxy_json")
|
||||
customReverseProxyJson: formData.has("custom_reverse_proxy_json")
|
||||
? parseOptionalText(formData.get("custom_reverse_proxy_json"))
|
||||
: undefined,
|
||||
authentik: parseAuthentikConfig(formData),
|
||||
cpm_forward_auth: parseCpmForwardAuthConfig(formData),
|
||||
load_balancer: parseLoadBalancerConfig(formData),
|
||||
dns_resolver: parseDnsResolverConfig(formData),
|
||||
upstream_dns_resolution: parseUpstreamDnsResolutionConfig(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,
|
||||
location_rules: formData.has("location_rules_json") ? parseLocationRulesConfig(formData) : undefined,
|
||||
locationRules: formData.has("location_rules_json") ? parseLocationRulesConfig(formData) : undefined,
|
||||
},
|
||||
userId
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
|
||||
]);
|
||||
|
||||
// Build forward auth access map for hosts that have CPM forward auth enabled
|
||||
const faHosts = hosts.filter((h) => h.cpm_forward_auth?.enabled);
|
||||
const faHosts = hosts.filter((h) => h.cpmForwardAuth?.enabled);
|
||||
const faAccessEntries = await Promise.all(
|
||||
faHosts.map((h) => getForwardAuthAccessForHost(h.id).catch(() => []))
|
||||
);
|
||||
@@ -51,8 +51,8 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
|
||||
faHosts.forEach((h, i) => {
|
||||
const entries = faAccessEntries[i];
|
||||
forwardAuthAccessMap[h.id] = {
|
||||
userIds: entries.filter((e) => e.user_id !== null).map((e) => e.user_id!),
|
||||
groupIds: entries.filter((e) => e.group_id !== null).map((e) => e.group_id!),
|
||||
userIds: entries.filter((e) => e.userId !== null).map((e) => e.userId!),
|
||||
groupIds: entries.filter((e) => e.groupId !== null).map((e) => e.groupId!),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
|
||||
authentikDefaults={authentikDefaults}
|
||||
pagination={{ total, page, perPage: PER_PAGE }}
|
||||
initialSearch={search ?? ""}
|
||||
initialSort={{ sortBy: sortBy ?? "created_at", sortDir }}
|
||||
initialSort={{ sortBy: sortBy ?? "createdAt", sortDir }}
|
||||
mtlsRoles={mtlsRoles}
|
||||
issuedClientCerts={issuedClientCerts}
|
||||
forwardAuthUsers={forwardAuthUsers}
|
||||
|
||||
Reference in New Issue
Block a user