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:
fuomag9
2026-04-12 21:11:48 +02:00
parent eb78b64c2f
commit 3a16d6e9b1
100 changed files with 3390 additions and 14495 deletions

View File

@@ -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">

View File

@@ -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
);

View File

@@ -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}