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

@@ -68,7 +68,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: '',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Name is required');
@@ -78,7 +78,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'sctp' as any,
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("Protocol must be 'tcp' or 'udp'");
@@ -88,7 +88,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: '',
listenAddress: '',
upstreams: ['10.0.0.1:5432'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Listen address is required');
@@ -98,7 +98,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: '10.0.0.1',
listenAddress: '10.0.0.1',
upstreams: ['10.0.0.1:5432'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("Listen address must be in format ':PORT' or 'HOST:PORT'");
@@ -108,7 +108,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':0',
listenAddress: ':0',
upstreams: ['10.0.0.1:5432'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Port must be between 1 and 65535');
@@ -118,7 +118,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':99999',
listenAddress: ':99999',
upstreams: ['10.0.0.1:5432'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Port must be between 1 and 65535');
@@ -128,7 +128,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: [],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('At least one upstream must be specified');
@@ -138,7 +138,7 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1'],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("must be in 'host:port' format");
@@ -148,9 +148,9 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'udp',
listen_address: ':5353',
listenAddress: ':5353',
upstreams: ['8.8.8.8:53'],
tls_termination: true,
tlsTermination: true,
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('TLS termination is only supported with TCP');
});
@@ -159,10 +159,10 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432'],
matcher_type: 'tls_sni',
matcher_value: [],
matcherType: 'tls_sni',
matcherValue: [],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Matcher value is required');
});
@@ -171,10 +171,10 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':8080',
listenAddress: ':8080',
upstreams: ['10.0.0.1:8080'],
matcher_type: 'http_host',
matcher_value: [],
matcherType: 'http_host',
matcherValue: [],
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Matcher value is required');
});
@@ -183,9 +183,9 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432'],
proxy_protocol_version: 'v3' as any,
proxyProtocolVersion: 'v3' as any,
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow("Proxy protocol version must be 'v1' or 'v2'");
});
@@ -194,9 +194,9 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Test',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432'],
matcher_type: 'invalid' as any,
matcherType: 'invalid' as any,
};
await expect(createL4ProxyHost(input, 1)).rejects.toThrow('Matcher type must be one of');
});
@@ -205,33 +205,33 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Full Featured',
protocol: 'tcp',
listen_address: ':993',
listenAddress: ':993',
upstreams: ['localhost:143'],
matcher_type: 'tls_sni',
matcher_value: ['mail.example.com'],
tls_termination: true,
proxy_protocol_version: 'v1',
proxy_protocol_receive: true,
matcherType: 'tls_sni',
matcherValue: ['mail.example.com'],
tlsTermination: true,
proxyProtocolVersion: 'v1',
proxyProtocolReceive: true,
enabled: true,
};
const result = await createL4ProxyHost(input, 1);
expect(result).toBeDefined();
expect(result.name).toBe('Full Featured');
expect(result.protocol).toBe('tcp');
expect(result.listen_address).toBe(':993');
expect(result.listenAddress).toBe(':993');
expect(result.upstreams).toEqual(['localhost:143']);
expect(result.matcher_type).toBe('tls_sni');
expect(result.matcher_value).toEqual(['mail.example.com']);
expect(result.tls_termination).toBe(true);
expect(result.proxy_protocol_version).toBe('v1');
expect(result.proxy_protocol_receive).toBe(true);
expect(result.matcherType).toBe('tls_sni');
expect(result.matcherValue).toEqual(['mail.example.com']);
expect(result.tlsTermination).toBe(true);
expect(result.proxyProtocolVersion).toBe('v1');
expect(result.proxyProtocolReceive).toBe(true);
});
it('accepts valid UDP proxy', async () => {
const input: L4ProxyHostInput = {
name: 'DNS',
protocol: 'udp',
listen_address: ':5353',
listenAddress: ':5353',
upstreams: ['8.8.8.8:53'],
};
const result = await createL4ProxyHost(input, 1);
@@ -243,54 +243,54 @@ describe('L4 proxy host create validation', () => {
const input: L4ProxyHostInput = {
name: 'Bound',
protocol: 'tcp',
listen_address: '0.0.0.0:5432',
listenAddress: '0.0.0.0:5432',
upstreams: ['10.0.0.1:5432'],
};
const result = await createL4ProxyHost(input, 1);
expect(result.listen_address).toBe('0.0.0.0:5432');
expect(result.listenAddress).toBe('0.0.0.0:5432');
});
it('accepts none matcher without matcher_value', async () => {
it('accepts none matcher without matcherValue', async () => {
const input: L4ProxyHostInput = {
name: 'Catch All',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432'],
matcher_type: 'none',
matcherType: 'none',
};
const result = await createL4ProxyHost(input, 1);
expect(result.matcher_type).toBe('none');
expect(result.matcherType).toBe('none');
});
it('accepts proxy_protocol matcher without matcher_value', async () => {
it('accepts proxy_protocol matcher without matcherValue', async () => {
const input: L4ProxyHostInput = {
name: 'PP Detect',
protocol: 'tcp',
listen_address: ':8443',
listenAddress: ':8443',
upstreams: ['10.0.0.1:443'],
matcher_type: 'proxy_protocol',
matcherType: 'proxy_protocol',
};
const result = await createL4ProxyHost(input, 1);
expect(result.matcher_type).toBe('proxy_protocol');
expect(result.matcherType).toBe('proxy_protocol');
});
it('trims whitespace from name and listen_address', async () => {
it('trims whitespace from name and listenAddress', async () => {
const input: L4ProxyHostInput = {
name: ' Spacey Name ',
protocol: 'tcp',
listen_address: ' :5432 ',
listenAddress: ' :5432 ',
upstreams: ['10.0.0.1:5432'],
};
const result = await createL4ProxyHost(input, 1);
expect(result.name).toBe('Spacey Name');
expect(result.listen_address).toBe(':5432');
expect(result.listenAddress).toBe(':5432');
});
it('deduplicates upstreams', async () => {
const input: L4ProxyHostInput = {
name: 'Dedup',
protocol: 'tcp',
listen_address: ':5432',
listenAddress: ':5432',
upstreams: ['10.0.0.1:5432', '10.0.0.1:5432', '10.0.0.2:5432'],
};
const result = await createL4ProxyHost(input, 1);