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:
@@ -58,65 +58,65 @@ describe('mtls-access-rules CRUD', () => {
|
||||
it('createMtlsAccessRule creates a rule', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id,
|
||||
path_pattern: '/admin/*',
|
||||
allowed_role_ids: [1, 2],
|
||||
allowed_cert_ids: [10],
|
||||
proxyHostId: host.id,
|
||||
pathPattern: '/admin/*',
|
||||
allowedRoleIds: [1, 2],
|
||||
allowedCertIds: [10],
|
||||
priority: 5,
|
||||
description: 'admin only',
|
||||
}, userId);
|
||||
|
||||
expect(rule.proxy_host_id).toBe(host.id);
|
||||
expect(rule.path_pattern).toBe('/admin/*');
|
||||
expect(rule.allowed_role_ids).toEqual([1, 2]);
|
||||
expect(rule.allowed_cert_ids).toEqual([10]);
|
||||
expect(rule.proxyHostId).toBe(host.id);
|
||||
expect(rule.pathPattern).toBe('/admin/*');
|
||||
expect(rule.allowedRoleIds).toEqual([1, 2]);
|
||||
expect(rule.allowedCertIds).toEqual([10]);
|
||||
expect(rule.priority).toBe(5);
|
||||
expect(rule.description).toBe('admin only');
|
||||
expect(rule.deny_all).toBe(false);
|
||||
expect(rule.denyAll).toBe(false);
|
||||
});
|
||||
|
||||
it('createMtlsAccessRule trims path_pattern', async () => {
|
||||
it('createMtlsAccessRule trims pathPattern', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id,
|
||||
path_pattern: ' /api/* ',
|
||||
proxyHostId: host.id,
|
||||
pathPattern: ' /api/* ',
|
||||
}, userId);
|
||||
expect(rule.path_pattern).toBe('/api/*');
|
||||
expect(rule.pathPattern).toBe('/api/*');
|
||||
});
|
||||
|
||||
it('createMtlsAccessRule defaults arrays to empty', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id,
|
||||
path_pattern: '*',
|
||||
proxyHostId: host.id,
|
||||
pathPattern: '*',
|
||||
}, userId);
|
||||
expect(rule.allowed_role_ids).toEqual([]);
|
||||
expect(rule.allowed_cert_ids).toEqual([]);
|
||||
expect(rule.deny_all).toBe(false);
|
||||
expect(rule.allowedRoleIds).toEqual([]);
|
||||
expect(rule.allowedCertIds).toEqual([]);
|
||||
expect(rule.denyAll).toBe(false);
|
||||
expect(rule.priority).toBe(0);
|
||||
});
|
||||
|
||||
it('createMtlsAccessRule with deny_all', async () => {
|
||||
it('createMtlsAccessRule with denyAll', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id,
|
||||
path_pattern: '/blocked/*',
|
||||
deny_all: true,
|
||||
proxyHostId: host.id,
|
||||
pathPattern: '/blocked/*',
|
||||
denyAll: true,
|
||||
}, userId);
|
||||
expect(rule.deny_all).toBe(true);
|
||||
expect(rule.denyAll).toBe(true);
|
||||
});
|
||||
|
||||
it('listMtlsAccessRules returns rules ordered by priority desc then path asc', async () => {
|
||||
const host = await insertHost();
|
||||
await createMtlsAccessRule({ proxy_host_id: host.id, path_pattern: '/b', priority: 1 }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: host.id, path_pattern: '/a', priority: 10 }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: host.id, path_pattern: '/c', priority: 1 }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: host.id, pathPattern: '/b', priority: 1 }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: host.id, pathPattern: '/a', priority: 10 }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: host.id, pathPattern: '/c', priority: 1 }, userId);
|
||||
|
||||
const rules = await listMtlsAccessRules(host.id);
|
||||
expect(rules).toHaveLength(3);
|
||||
expect(rules[0].path_pattern).toBe('/a'); // priority 10 (highest)
|
||||
expect(rules[1].path_pattern).toBe('/b'); // priority 1, path /b
|
||||
expect(rules[2].path_pattern).toBe('/c'); // priority 1, path /c
|
||||
expect(rules[0].pathPattern).toBe('/a'); // priority 10 (highest)
|
||||
expect(rules[1].pathPattern).toBe('/b'); // priority 1, path /b
|
||||
expect(rules[2].pathPattern).toBe('/c'); // priority 1, path /c
|
||||
});
|
||||
|
||||
it('listMtlsAccessRules returns empty array for host with no rules', async () => {
|
||||
@@ -128,24 +128,24 @@ describe('mtls-access-rules CRUD', () => {
|
||||
it('listMtlsAccessRules only returns rules for the specified host', async () => {
|
||||
const host1 = await insertHost('h1');
|
||||
const host2 = await insertHost('h2');
|
||||
await createMtlsAccessRule({ proxy_host_id: host1.id, path_pattern: '/h1' }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: host2.id, path_pattern: '/h2' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: host1.id, pathPattern: '/h1' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: host2.id, pathPattern: '/h2' }, userId);
|
||||
|
||||
const rules = await listMtlsAccessRules(host1.id);
|
||||
expect(rules).toHaveLength(1);
|
||||
expect(rules[0].path_pattern).toBe('/h1');
|
||||
expect(rules[0].pathPattern).toBe('/h1');
|
||||
});
|
||||
|
||||
it('getMtlsAccessRule returns a single rule', async () => {
|
||||
const host = await insertHost();
|
||||
const created = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id, path_pattern: '/test',
|
||||
proxyHostId: host.id, pathPattern: '/test',
|
||||
}, userId);
|
||||
|
||||
const fetched = await getMtlsAccessRule(created.id);
|
||||
expect(fetched).not.toBeNull();
|
||||
expect(fetched!.id).toBe(created.id);
|
||||
expect(fetched!.path_pattern).toBe('/test');
|
||||
expect(fetched!.pathPattern).toBe('/test');
|
||||
});
|
||||
|
||||
it('getMtlsAccessRule returns null for non-existent rule', async () => {
|
||||
@@ -155,34 +155,34 @@ describe('mtls-access-rules CRUD', () => {
|
||||
it('updateMtlsAccessRule updates fields', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id, path_pattern: '/old', priority: 0,
|
||||
proxyHostId: host.id, pathPattern: '/old', priority: 0,
|
||||
}, userId);
|
||||
|
||||
const updated = await updateMtlsAccessRule(rule.id, {
|
||||
path_pattern: '/new',
|
||||
pathPattern: '/new',
|
||||
priority: 99,
|
||||
allowed_role_ids: [5],
|
||||
deny_all: true,
|
||||
allowedRoleIds: [5],
|
||||
denyAll: true,
|
||||
description: 'updated',
|
||||
}, userId);
|
||||
|
||||
expect(updated.path_pattern).toBe('/new');
|
||||
expect(updated.pathPattern).toBe('/new');
|
||||
expect(updated.priority).toBe(99);
|
||||
expect(updated.allowed_role_ids).toEqual([5]);
|
||||
expect(updated.deny_all).toBe(true);
|
||||
expect(updated.allowedRoleIds).toEqual([5]);
|
||||
expect(updated.denyAll).toBe(true);
|
||||
expect(updated.description).toBe('updated');
|
||||
});
|
||||
|
||||
it('updateMtlsAccessRule partial update leaves other fields unchanged', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id, path_pattern: '/test',
|
||||
allowed_role_ids: [1], priority: 5, description: 'original',
|
||||
proxyHostId: host.id, pathPattern: '/test',
|
||||
allowedRoleIds: [1], priority: 5, description: 'original',
|
||||
}, userId);
|
||||
|
||||
const updated = await updateMtlsAccessRule(rule.id, { priority: 10 }, userId);
|
||||
expect(updated.path_pattern).toBe('/test');
|
||||
expect(updated.allowed_role_ids).toEqual([1]);
|
||||
expect(updated.pathPattern).toBe('/test');
|
||||
expect(updated.allowedRoleIds).toEqual([1]);
|
||||
expect(updated.description).toBe('original');
|
||||
expect(updated.priority).toBe(10);
|
||||
});
|
||||
@@ -194,7 +194,7 @@ describe('mtls-access-rules CRUD', () => {
|
||||
it('deleteMtlsAccessRule removes the rule', async () => {
|
||||
const host = await insertHost();
|
||||
const rule = await createMtlsAccessRule({
|
||||
proxy_host_id: host.id, path_pattern: '/test',
|
||||
proxyHostId: host.id, pathPattern: '/test',
|
||||
}, userId);
|
||||
|
||||
await deleteMtlsAccessRule(rule.id, 1);
|
||||
@@ -221,9 +221,9 @@ describe('getAccessRulesForHosts (bulk query)', () => {
|
||||
it('groups rules by proxy host ID', async () => {
|
||||
const h1 = await insertHost('h1');
|
||||
const h2 = await insertHost('h2');
|
||||
await createMtlsAccessRule({ proxy_host_id: h1.id, path_pattern: '/a' }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: h1.id, path_pattern: '/b' }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: h2.id, path_pattern: '/c' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h1.id, pathPattern: '/a' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h1.id, pathPattern: '/b' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h2.id, pathPattern: '/c' }, userId);
|
||||
|
||||
const map = await getAccessRulesForHosts([h1.id, h2.id]);
|
||||
expect(map.get(h1.id)).toHaveLength(2);
|
||||
@@ -233,8 +233,8 @@ describe('getAccessRulesForHosts (bulk query)', () => {
|
||||
it('excludes hosts not in the query list', async () => {
|
||||
const h1 = await insertHost('h1');
|
||||
const h2 = await insertHost('h2');
|
||||
await createMtlsAccessRule({ proxy_host_id: h1.id, path_pattern: '/a' }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: h2.id, path_pattern: '/b' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h1.id, pathPattern: '/a' }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h2.id, pathPattern: '/b' }, userId);
|
||||
|
||||
const map = await getAccessRulesForHosts([h1.id]);
|
||||
expect(map.has(h1.id)).toBe(true);
|
||||
@@ -243,20 +243,20 @@ describe('getAccessRulesForHosts (bulk query)', () => {
|
||||
|
||||
it('rules within a host are ordered by priority desc, path asc', async () => {
|
||||
const h = await insertHost();
|
||||
await createMtlsAccessRule({ proxy_host_id: h.id, path_pattern: '/z', priority: 10 }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: h.id, path_pattern: '/a', priority: 1 }, userId);
|
||||
await createMtlsAccessRule({ proxy_host_id: h.id, path_pattern: '/m', priority: 10 }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h.id, pathPattern: '/z', priority: 10 }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h.id, pathPattern: '/a', priority: 1 }, userId);
|
||||
await createMtlsAccessRule({ proxyHostId: h.id, pathPattern: '/m', priority: 10 }, userId);
|
||||
|
||||
const map = await getAccessRulesForHosts([h.id]);
|
||||
const rules = map.get(h.id)!;
|
||||
expect(rules[0].path_pattern).toBe('/m'); // priority 10, path /m
|
||||
expect(rules[1].path_pattern).toBe('/z'); // priority 10, path /z
|
||||
expect(rules[2].path_pattern).toBe('/a'); // priority 1
|
||||
expect(rules[0].pathPattern).toBe('/m'); // priority 10, path /m
|
||||
expect(rules[1].pathPattern).toBe('/z'); // priority 10, path /z
|
||||
expect(rules[2].pathPattern).toBe('/a'); // priority 1
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON parsing edge cases in access rules', () => {
|
||||
it('handles malformed allowed_role_ids JSON gracefully', async () => {
|
||||
it('handles malformed allowedRoleIds JSON gracefully', async () => {
|
||||
const host = await insertHost();
|
||||
const now = nowIso();
|
||||
// Insert directly with bad JSON
|
||||
@@ -267,7 +267,7 @@ describe('JSON parsing edge cases in access rules', () => {
|
||||
});
|
||||
|
||||
const rules = await listMtlsAccessRules(host.id);
|
||||
expect(rules[0].allowed_role_ids).toEqual([]);
|
||||
expect(rules[0].allowedRoleIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters non-numeric values from JSON arrays', async () => {
|
||||
@@ -280,7 +280,7 @@ describe('JSON parsing edge cases in access rules', () => {
|
||||
});
|
||||
|
||||
const rules = await listMtlsAccessRules(host.id);
|
||||
expect(rules[0].allowed_role_ids).toEqual([1, 3]);
|
||||
expect(rules[0].allowedRoleIds).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
it('handles non-array JSON', async () => {
|
||||
@@ -293,7 +293,7 @@ describe('JSON parsing edge cases in access rules', () => {
|
||||
});
|
||||
|
||||
const rules = await listMtlsAccessRules(host.id);
|
||||
expect(rules[0].allowed_role_ids).toEqual([]);
|
||||
expect(rules[0].allowed_cert_ids).toEqual([]);
|
||||
expect(rules[0].allowedRoleIds).toEqual([]);
|
||||
expect(rules[0].allowedCertIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user