@qwickapps/server 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +114 -0
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
- package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.js +166 -0
- package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
- package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
- package/dist/plugins/auth/config-store.d.ts +11 -0
- package/dist/plugins/auth/config-store.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.js +232 -0
- package/dist/plugins/auth/config-store.js.map +1 -0
- package/dist/plugins/auth/config-store.test.d.ts +7 -0
- package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.test.js +299 -0
- package/dist/plugins/auth/config-store.test.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +51 -1
- package/dist/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/plugins/auth/env-config.js +640 -7
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/auth/index.d.ts +6 -2
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +5 -1
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/types.d.ts +106 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
- package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
- package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
- package/dist/plugins/rate-limit/cleanup.js +72 -0
- package/dist/plugins/rate-limit/cleanup.js.map +1 -0
- package/dist/plugins/rate-limit/env-config.d.ts +91 -0
- package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
- package/dist/plugins/rate-limit/env-config.js +318 -0
- package/dist/plugins/rate-limit/env-config.js.map +1 -0
- package/dist/plugins/rate-limit/index.d.ts +76 -0
- package/dist/plugins/rate-limit/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/index.js +79 -0
- package/dist/plugins/rate-limit/index.js.map +1 -0
- package/dist/plugins/rate-limit/middleware.d.ts +40 -0
- package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
- package/dist/plugins/rate-limit/middleware.js +169 -0
- package/dist/plugins/rate-limit/middleware.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
- package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
- package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
- package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
- package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/index.js +8 -0
- package/dist/plugins/rate-limit/stores/index.js.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.js +27 -0
- package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
- package/dist/plugins/rate-limit/types.d.ts +265 -0
- package/dist/plugins/rate-limit/types.d.ts.map +1 -0
- package/dist/plugins/rate-limit/types.js +9 -0
- package/dist/plugins/rate-limit/types.js.map +1 -0
- package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
- package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +141 -0
- package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
- package/dist-ui-lib/index.js +3332 -2343
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
- package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/control-panel.ts +128 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +38 -0
- package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
- package/src/plugins/auth/adapter-wrapper.ts +205 -0
- package/src/plugins/auth/config-store.test.ts +417 -0
- package/src/plugins/auth/config-store.ts +305 -0
- package/src/plugins/auth/env-config.ts +714 -7
- package/src/plugins/auth/index.ts +22 -1
- package/src/plugins/auth/types.ts +138 -0
- package/src/plugins/index.ts +49 -0
- package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
- package/src/plugins/rate-limit/cleanup.ts +117 -0
- package/src/plugins/rate-limit/env-config.ts +400 -0
- package/src/plugins/rate-limit/index.ts +128 -0
- package/src/plugins/rate-limit/middleware.ts +212 -0
- package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
- package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
- package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
- package/src/plugins/rate-limit/stores/index.ts +8 -0
- package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
- package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
- package/src/plugins/rate-limit/strategies/index.ts +30 -0
- package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
- package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
- package/src/plugins/rate-limit/types.ts +338 -0
- package/ui/src/App.tsx +32 -14
- package/ui/src/api/controlPanelApi.ts +226 -0
- package/ui/src/dashboard/builtInWidgets.tsx +5 -1
- package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
- package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
- package/ui/src/dashboard/widgets/index.ts +2 -0
- package/ui/src/pages/AuthPage.tsx +986 -142
- package/ui/src/pages/IntegrationsPage.tsx +288 -0
- package/ui/src/pages/RateLimitPage.tsx +292 -0
- package/dist-ui/assets/index-BY8OxNgO.js +0 -465
- package/dist-ui/assets/index-BY8OxNgO.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Adapter Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps an auth adapter to enable hot-reload without server restart.
|
|
5
|
+
* All method calls are delegated to the underlying adapter, which can
|
|
6
|
+
* be swapped at runtime via setAdapter().
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Request, Response, RequestHandler } from 'express';
|
|
12
|
+
import type { AuthAdapter, AuthenticatedUser } from './types.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extended adapter interface with hot-reload capabilities
|
|
16
|
+
*/
|
|
17
|
+
export interface AdapterWrapper extends AuthAdapter {
|
|
18
|
+
/**
|
|
19
|
+
* Replace the underlying adapter (for hot-reload)
|
|
20
|
+
* Calls shutdown() on the old adapter before swapping
|
|
21
|
+
*/
|
|
22
|
+
setAdapter(adapter: AuthAdapter): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get information about the current adapter
|
|
26
|
+
*/
|
|
27
|
+
getAdapterInfo(): { name: string; initialized: boolean };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if an adapter is currently set
|
|
31
|
+
*/
|
|
32
|
+
hasAdapter(): boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a no-op adapter for when auth is disabled
|
|
37
|
+
*/
|
|
38
|
+
function createNoopAdapter(): AuthAdapter {
|
|
39
|
+
return {
|
|
40
|
+
name: 'none',
|
|
41
|
+
initialize: () => ((_req, _res, next) => next()) as RequestHandler,
|
|
42
|
+
isAuthenticated: () => false,
|
|
43
|
+
getUser: () => null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create an adapter wrapper for hot-reload support
|
|
49
|
+
*
|
|
50
|
+
* @param initialAdapter Optional initial adapter (defaults to no-op)
|
|
51
|
+
* @returns AdapterWrapper that delegates to the underlying adapter
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const wrapper = createAdapterWrapper(supertokensAdapter(config));
|
|
56
|
+
*
|
|
57
|
+
* // Later, swap the adapter without restart:
|
|
58
|
+
* await wrapper.setAdapter(auth0Adapter(newConfig));
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function createAdapterWrapper(initialAdapter?: AuthAdapter): AdapterWrapper {
|
|
62
|
+
let currentAdapter: AuthAdapter = initialAdapter || createNoopAdapter();
|
|
63
|
+
let isInitialized = false;
|
|
64
|
+
let isSwapping = false; // Prevent concurrent adapter swaps
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
// Delegate name to current adapter
|
|
68
|
+
get name(): string {
|
|
69
|
+
return currentAdapter.name;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize returns the middleware
|
|
74
|
+
* Note: This is called once at startup. The middleware itself
|
|
75
|
+
* will delegate to currentAdapter, which can be swapped.
|
|
76
|
+
*/
|
|
77
|
+
initialize(): RequestHandler | RequestHandler[] {
|
|
78
|
+
isInitialized = true;
|
|
79
|
+
|
|
80
|
+
// Get the initial middleware
|
|
81
|
+
const initialMiddleware = currentAdapter.initialize();
|
|
82
|
+
|
|
83
|
+
// Return a wrapper middleware that delegates to current adapter's middleware
|
|
84
|
+
// This allows hot-reload to work - new adapter's middleware will be used
|
|
85
|
+
// Note: For simplicity, we return the initial middleware. Full hot-reload
|
|
86
|
+
// of middleware would require more complex Express route manipulation.
|
|
87
|
+
// The key hot-reload capability is in isAuthenticated/getUser which ARE
|
|
88
|
+
// delegated dynamically.
|
|
89
|
+
return initialMiddleware;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Delegate isAuthenticated to current adapter
|
|
94
|
+
*/
|
|
95
|
+
isAuthenticated(req: Request): boolean {
|
|
96
|
+
return currentAdapter.isAuthenticated(req);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Delegate getUser to current adapter
|
|
101
|
+
*/
|
|
102
|
+
getUser(req: Request): AuthenticatedUser | null | Promise<AuthenticatedUser | null> {
|
|
103
|
+
return currentAdapter.getUser(req);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Delegate hasRoles to current adapter (if available)
|
|
108
|
+
*/
|
|
109
|
+
hasRoles(req: Request, roles: string[]): boolean {
|
|
110
|
+
if (currentAdapter.hasRoles) {
|
|
111
|
+
return currentAdapter.hasRoles(req, roles);
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Delegate getAccessToken to current adapter (if available)
|
|
118
|
+
*/
|
|
119
|
+
getAccessToken(req: Request): string | null {
|
|
120
|
+
if (currentAdapter.getAccessToken) {
|
|
121
|
+
return currentAdapter.getAccessToken(req);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Delegate onUnauthorized to current adapter (if available)
|
|
128
|
+
*/
|
|
129
|
+
onUnauthorized(req: Request, res: Response): void {
|
|
130
|
+
if (currentAdapter.onUnauthorized) {
|
|
131
|
+
currentAdapter.onUnauthorized(req, res);
|
|
132
|
+
} else {
|
|
133
|
+
res.status(401).json({
|
|
134
|
+
error: 'Unauthorized',
|
|
135
|
+
message: 'Authentication required',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Shutdown the current adapter
|
|
142
|
+
*/
|
|
143
|
+
async shutdown(): Promise<void> {
|
|
144
|
+
if (currentAdapter.shutdown) {
|
|
145
|
+
await currentAdapter.shutdown();
|
|
146
|
+
}
|
|
147
|
+
isInitialized = false;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Hot-reload: Replace the underlying adapter
|
|
152
|
+
* @throws Error if another adapter swap is already in progress
|
|
153
|
+
*/
|
|
154
|
+
async setAdapter(adapter: AuthAdapter): Promise<void> {
|
|
155
|
+
// Prevent concurrent adapter swaps
|
|
156
|
+
if (isSwapping) {
|
|
157
|
+
throw new Error('Adapter swap already in progress');
|
|
158
|
+
}
|
|
159
|
+
isSwapping = true;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const oldAdapter = currentAdapter;
|
|
163
|
+
|
|
164
|
+
// Shutdown old adapter
|
|
165
|
+
if (oldAdapter.shutdown) {
|
|
166
|
+
try {
|
|
167
|
+
await oldAdapter.shutdown();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error('[AdapterWrapper] Error shutting down old adapter:', err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Set new adapter
|
|
174
|
+
currentAdapter = adapter;
|
|
175
|
+
|
|
176
|
+
// Initialize new adapter if we're already running
|
|
177
|
+
if (isInitialized) {
|
|
178
|
+
// Note: We initialize the new adapter but can't easily swap Express middleware
|
|
179
|
+
// The new adapter's isAuthenticated/getUser will be used immediately
|
|
180
|
+
// Full middleware hot-reload would require server restart
|
|
181
|
+
currentAdapter.initialize();
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
isSwapping = false;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get info about the current adapter
|
|
190
|
+
*/
|
|
191
|
+
getAdapterInfo(): { name: string; initialized: boolean } {
|
|
192
|
+
return {
|
|
193
|
+
name: currentAdapter.name,
|
|
194
|
+
initialized: isInitialized,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if a real adapter is set (not the no-op)
|
|
200
|
+
*/
|
|
201
|
+
hasAdapter(): boolean {
|
|
202
|
+
return currentAdapter.name !== 'none';
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Config Store Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for PostgreSQL-backed auth configuration store.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { postgresAuthConfigStore } from './config-store.js';
|
|
9
|
+
import type { RuntimeAuthConfig } from './types.js';
|
|
10
|
+
|
|
11
|
+
// Mock pg pool
|
|
12
|
+
interface MockPoolClient {
|
|
13
|
+
query: ReturnType<typeof vi.fn>;
|
|
14
|
+
on: ReturnType<typeof vi.fn>;
|
|
15
|
+
release: ReturnType<typeof vi.fn>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface MockPool {
|
|
19
|
+
query: ReturnType<typeof vi.fn>;
|
|
20
|
+
connect: ReturnType<typeof vi.fn>;
|
|
21
|
+
on: ReturnType<typeof vi.fn>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createMockPool(): MockPool {
|
|
25
|
+
const mockClient: MockPoolClient = {
|
|
26
|
+
query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
|
|
27
|
+
on: vi.fn(),
|
|
28
|
+
release: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
|
|
33
|
+
connect: vi.fn().mockResolvedValue(mockClient),
|
|
34
|
+
on: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('postgresAuthConfigStore', () => {
|
|
39
|
+
let mockPool: MockPool;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
mockPool = createMockPool();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('identifier validation', () => {
|
|
50
|
+
it('should reject invalid table names', () => {
|
|
51
|
+
expect(() =>
|
|
52
|
+
postgresAuthConfigStore({
|
|
53
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
54
|
+
tableName: 'DROP TABLE users;--',
|
|
55
|
+
})
|
|
56
|
+
).toThrow('Invalid table name');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reject table names with spaces', () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
postgresAuthConfigStore({
|
|
62
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
63
|
+
tableName: 'my table',
|
|
64
|
+
})
|
|
65
|
+
).toThrow('Invalid table name');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should reject table names starting with numbers', () => {
|
|
69
|
+
expect(() =>
|
|
70
|
+
postgresAuthConfigStore({
|
|
71
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
72
|
+
tableName: '123table',
|
|
73
|
+
})
|
|
74
|
+
).toThrow('Invalid table name');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should reject table names longer than 63 characters', () => {
|
|
78
|
+
const longName = 'a'.repeat(64);
|
|
79
|
+
expect(() =>
|
|
80
|
+
postgresAuthConfigStore({
|
|
81
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
82
|
+
tableName: longName,
|
|
83
|
+
})
|
|
84
|
+
).toThrow('Invalid table name');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should accept valid table names', () => {
|
|
88
|
+
expect(() =>
|
|
89
|
+
postgresAuthConfigStore({
|
|
90
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
91
|
+
tableName: 'auth_config',
|
|
92
|
+
})
|
|
93
|
+
).not.toThrow();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should accept table names with underscores', () => {
|
|
97
|
+
expect(() =>
|
|
98
|
+
postgresAuthConfigStore({
|
|
99
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
100
|
+
tableName: '_my_auth_config_table',
|
|
101
|
+
})
|
|
102
|
+
).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should reject invalid schema names', () => {
|
|
106
|
+
expect(() =>
|
|
107
|
+
postgresAuthConfigStore({
|
|
108
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
109
|
+
schema: 'public; DROP TABLE users;--',
|
|
110
|
+
})
|
|
111
|
+
).toThrow('Invalid schema name');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should reject invalid notify channel names', () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
postgresAuthConfigStore({
|
|
117
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
118
|
+
notifyChannel: 'channel-with-dashes',
|
|
119
|
+
})
|
|
120
|
+
).toThrow('Invalid notify channel');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('initialize', () => {
|
|
125
|
+
it('should create table if autoCreateTable is true', async () => {
|
|
126
|
+
const store = postgresAuthConfigStore({
|
|
127
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
128
|
+
autoCreateTable: true,
|
|
129
|
+
enableNotify: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await store.initialize();
|
|
133
|
+
|
|
134
|
+
expect(mockPool.query).toHaveBeenCalledWith(
|
|
135
|
+
expect.stringContaining('CREATE TABLE IF NOT EXISTS')
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should not create table if autoCreateTable is false', async () => {
|
|
140
|
+
const store = postgresAuthConfigStore({
|
|
141
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
142
|
+
autoCreateTable: false,
|
|
143
|
+
enableNotify: false,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await store.initialize();
|
|
147
|
+
|
|
148
|
+
expect(mockPool.query).not.toHaveBeenCalledWith(
|
|
149
|
+
expect.stringContaining('CREATE TABLE IF NOT EXISTS')
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should use custom table name', async () => {
|
|
154
|
+
const store = postgresAuthConfigStore({
|
|
155
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
156
|
+
tableName: 'custom_auth_config',
|
|
157
|
+
autoCreateTable: true,
|
|
158
|
+
enableNotify: false,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await store.initialize();
|
|
162
|
+
|
|
163
|
+
expect(mockPool.query).toHaveBeenCalledWith(
|
|
164
|
+
expect.stringContaining('"public"."custom_auth_config"')
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should use custom schema name', async () => {
|
|
169
|
+
const store = postgresAuthConfigStore({
|
|
170
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
171
|
+
schema: 'myschema',
|
|
172
|
+
autoCreateTable: true,
|
|
173
|
+
enableNotify: false,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await store.initialize();
|
|
177
|
+
|
|
178
|
+
expect(mockPool.query).toHaveBeenCalledWith(
|
|
179
|
+
expect.stringContaining('"myschema"."auth_config"')
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('load', () => {
|
|
185
|
+
it('should return null when no config exists', async () => {
|
|
186
|
+
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
|
187
|
+
|
|
188
|
+
const store = postgresAuthConfigStore({
|
|
189
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
190
|
+
autoCreateTable: false,
|
|
191
|
+
enableNotify: false,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await store.load();
|
|
195
|
+
|
|
196
|
+
expect(result).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return config when it exists', async () => {
|
|
200
|
+
const mockConfig = {
|
|
201
|
+
adapter: 'basic',
|
|
202
|
+
config: { basic: { username: 'admin', password: 'secret' } },
|
|
203
|
+
settings: {},
|
|
204
|
+
updated_at: new Date('2025-01-15T12:00:00Z'),
|
|
205
|
+
updated_by: 'admin@example.com',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
mockPool.query.mockResolvedValueOnce({ rows: [mockConfig], rowCount: 1 });
|
|
209
|
+
|
|
210
|
+
const store = postgresAuthConfigStore({
|
|
211
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
212
|
+
autoCreateTable: false,
|
|
213
|
+
enableNotify: false,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const result = await store.load();
|
|
217
|
+
|
|
218
|
+
expect(result).toEqual({
|
|
219
|
+
adapter: 'basic',
|
|
220
|
+
config: { basic: { username: 'admin', password: 'secret' } },
|
|
221
|
+
settings: {},
|
|
222
|
+
updatedAt: '2025-01-15T12:00:00.000Z',
|
|
223
|
+
updatedBy: 'admin@example.com',
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('save', () => {
|
|
229
|
+
it('should upsert config to database', async () => {
|
|
230
|
+
const store = postgresAuthConfigStore({
|
|
231
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
232
|
+
autoCreateTable: false,
|
|
233
|
+
enableNotify: false,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const config: RuntimeAuthConfig = {
|
|
237
|
+
adapter: 'basic',
|
|
238
|
+
config: { basic: { username: 'admin', password: 'secret', realm: 'Test' } },
|
|
239
|
+
settings: {},
|
|
240
|
+
updatedAt: new Date().toISOString(),
|
|
241
|
+
updatedBy: 'admin@example.com',
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
await store.save(config);
|
|
245
|
+
|
|
246
|
+
expect(mockPool.query).toHaveBeenCalledWith(
|
|
247
|
+
expect.stringContaining('INSERT INTO'),
|
|
248
|
+
expect.arrayContaining(['basic', expect.any(String), expect.any(String), 'admin@example.com'])
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should send NOTIFY when enableNotify is true', async () => {
|
|
253
|
+
const store = postgresAuthConfigStore({
|
|
254
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
255
|
+
autoCreateTable: false,
|
|
256
|
+
enableNotify: true,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const config: RuntimeAuthConfig = {
|
|
260
|
+
adapter: 'basic',
|
|
261
|
+
config: { basic: { username: 'admin', password: 'secret', realm: 'Test' } },
|
|
262
|
+
settings: {},
|
|
263
|
+
updatedAt: new Date().toISOString(),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
await store.save(config);
|
|
267
|
+
|
|
268
|
+
expect(mockPool.query).toHaveBeenCalledWith('NOTIFY auth_config_changed');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should not send NOTIFY when enableNotify is false', async () => {
|
|
272
|
+
const store = postgresAuthConfigStore({
|
|
273
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
274
|
+
autoCreateTable: false,
|
|
275
|
+
enableNotify: false,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const config: RuntimeAuthConfig = {
|
|
279
|
+
adapter: 'basic',
|
|
280
|
+
config: { basic: { username: 'admin', password: 'secret', realm: 'Test' } },
|
|
281
|
+
settings: {},
|
|
282
|
+
updatedAt: new Date().toISOString(),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await store.save(config);
|
|
286
|
+
|
|
287
|
+
expect(mockPool.query).not.toHaveBeenCalledWith('NOTIFY auth_config_changed');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('delete', () => {
|
|
292
|
+
it('should delete config from database', async () => {
|
|
293
|
+
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 1 });
|
|
294
|
+
|
|
295
|
+
const store = postgresAuthConfigStore({
|
|
296
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
297
|
+
autoCreateTable: false,
|
|
298
|
+
enableNotify: false,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const result = await store.delete();
|
|
302
|
+
|
|
303
|
+
expect(mockPool.query).toHaveBeenCalledWith(
|
|
304
|
+
expect.stringContaining('DELETE FROM')
|
|
305
|
+
);
|
|
306
|
+
expect(result).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should return false when no config to delete', async () => {
|
|
310
|
+
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
|
311
|
+
|
|
312
|
+
const store = postgresAuthConfigStore({
|
|
313
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
314
|
+
autoCreateTable: false,
|
|
315
|
+
enableNotify: false,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = await store.delete();
|
|
319
|
+
|
|
320
|
+
expect(result).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should send NOTIFY after delete when enableNotify is true', async () => {
|
|
324
|
+
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 1 });
|
|
325
|
+
|
|
326
|
+
const store = postgresAuthConfigStore({
|
|
327
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
328
|
+
autoCreateTable: false,
|
|
329
|
+
enableNotify: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
await store.delete();
|
|
333
|
+
|
|
334
|
+
expect(mockPool.query).toHaveBeenCalledWith('NOTIFY auth_config_changed');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('onChange', () => {
|
|
339
|
+
it('should register listener and return unsubscribe function', () => {
|
|
340
|
+
const store = postgresAuthConfigStore({
|
|
341
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
342
|
+
autoCreateTable: false,
|
|
343
|
+
enableNotify: false,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const listener = vi.fn();
|
|
347
|
+
const unsubscribe = store.onChange(listener);
|
|
348
|
+
|
|
349
|
+
expect(typeof unsubscribe).toBe('function');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should unsubscribe listener when called', () => {
|
|
353
|
+
const store = postgresAuthConfigStore({
|
|
354
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
355
|
+
autoCreateTable: false,
|
|
356
|
+
enableNotify: false,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const listener = vi.fn();
|
|
360
|
+
const unsubscribe = store.onChange(listener);
|
|
361
|
+
|
|
362
|
+
// Unsubscribe
|
|
363
|
+
unsubscribe();
|
|
364
|
+
|
|
365
|
+
// Listener should not be called anymore
|
|
366
|
+
// (We can't easily test this without exposing internals, but the function exists)
|
|
367
|
+
expect(unsubscribe).toBeDefined();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('lazy pool initialization', () => {
|
|
372
|
+
it('should accept a function that returns the pool', async () => {
|
|
373
|
+
const poolFn = vi.fn().mockReturnValue(mockPool);
|
|
374
|
+
|
|
375
|
+
const store = postgresAuthConfigStore({
|
|
376
|
+
pool: poolFn as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
377
|
+
autoCreateTable: false,
|
|
378
|
+
enableNotify: false,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
|
382
|
+
await store.load();
|
|
383
|
+
|
|
384
|
+
expect(poolFn).toHaveBeenCalled();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should throw if pool is invalid', async () => {
|
|
388
|
+
const store = postgresAuthConfigStore({
|
|
389
|
+
pool: (() => ({})) as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
390
|
+
autoCreateTable: false,
|
|
391
|
+
enableNotify: false,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await expect(store.load()).rejects.toThrow('Invalid pool');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('shutdown', () => {
|
|
399
|
+
it('should release listener client and clear listeners', async () => {
|
|
400
|
+
const store = postgresAuthConfigStore({
|
|
401
|
+
pool: mockPool as unknown as Parameters<typeof postgresAuthConfigStore>[0]['pool'],
|
|
402
|
+
autoCreateTable: false,
|
|
403
|
+
enableNotify: false,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Add a listener
|
|
407
|
+
const listener = vi.fn();
|
|
408
|
+
store.onChange(listener);
|
|
409
|
+
|
|
410
|
+
// Shutdown
|
|
411
|
+
await store.shutdown();
|
|
412
|
+
|
|
413
|
+
// Should complete without error
|
|
414
|
+
expect(true).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
});
|