@qwickapps/server 1.3.0 → 1.3.1
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 +154 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +30 -2
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +36 -0
- package/dist/core/plugin-registry.d.ts.map +1 -1
- package/dist/core/plugin-registry.js +26 -0
- package/dist/core/plugin-registry.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/index.d.ts +1 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/index.js +1 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
- package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +88 -0
- package/dist/plugins/auth/env-config.d.ts.map +1 -0
- package/dist/plugins/auth/env-config.js +489 -0
- package/dist/plugins/auth/env-config.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +3 -1
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +3 -0
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
- package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
- package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
- package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +70 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/auth/types.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +3 -0
- package/dist/plugins/cache-plugin.test.js.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/postgres-plugin.test.js +3 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
- package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
- package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
- package/dist/plugins/preferences/index.d.ts +12 -0
- package/dist/plugins/preferences/index.d.ts.map +1 -0
- package/dist/plugins/preferences/index.js +13 -0
- package/dist/plugins/preferences/index.js.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
- package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
- package/dist/plugins/preferences/preferences-plugin.js +226 -0
- package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
- package/dist/plugins/preferences/stores/index.d.ts +9 -0
- package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/index.js +9 -0
- package/dist/plugins/preferences/stores/index.js.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
- package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/preferences/stores/postgres-store.js +181 -0
- package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
- package/dist/plugins/preferences/types.d.ts +91 -0
- package/dist/plugins/preferences/types.d.ts.map +1 -0
- package/dist/plugins/preferences/types.js +10 -0
- package/dist/plugins/preferences/types.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
- package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
- package/dist/plugins/users/index.d.ts +2 -2
- package/dist/plugins/users/index.d.ts.map +1 -1
- package/dist/plugins/users/index.js +1 -1
- package/dist/plugins/users/index.js.map +1 -1
- package/dist/plugins/users/types.d.ts +36 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +8 -2
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +122 -5
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/{index-Bsp2ntcw.js → index-BY8OxNgO.js} +112 -112
- package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +53 -7
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
- package/dist-ui-lib/index.js +2382 -3651
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
- package/package.json +7 -2
- package/src/core/control-panel.ts +33 -2
- package/src/core/plugin-registry.ts +63 -0
- package/src/index.ts +7 -0
- package/src/plugins/auth/adapters/index.ts +1 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
- package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
- package/src/plugins/auth/env-config.ts +572 -0
- package/src/plugins/auth/index.ts +9 -0
- package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
- package/src/plugins/auth/types.ts +80 -0
- package/src/plugins/cache-plugin.test.ts +3 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/postgres-plugin.test.ts +3 -0
- package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
- package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
- package/src/plugins/preferences/index.ts +30 -0
- package/src/plugins/preferences/preferences-plugin.ts +270 -0
- package/src/plugins/preferences/stores/index.ts +9 -0
- package/src/plugins/preferences/stores/postgres-store.ts +252 -0
- package/src/plugins/preferences/types.ts +100 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
- package/src/plugins/users/index.ts +3 -0
- package/src/plugins/users/types.ts +38 -0
- package/src/plugins/users/users-plugin.ts +142 -5
- package/ui/src/App.tsx +4 -1
- package/ui/src/api/controlPanelApi.ts +100 -1
- package/ui/src/components/ControlPanelApp.tsx +3 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
- package/ui/src/dashboard/builtInWidgets.tsx +8 -2
- package/ui/src/pages/AuthPage.tsx +259 -0
- package/ui/src/pages/PluginsPage.tsx +394 -0
- package/ui/vite.lib.config.ts +5 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supertokens Adapter Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the Supertokens authentication adapter.
|
|
5
|
+
* Tests against supertokens-node v20+ API.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
11
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
12
|
+
import { supertokensAdapter } from './adapters/supertokens-adapter.js';
|
|
13
|
+
import type { SupertokensAdapterConfig, AuthenticatedUser } from './types.js';
|
|
14
|
+
|
|
15
|
+
// Type for extended request with our custom properties
|
|
16
|
+
interface SupertokensExtendedRequest extends Request {
|
|
17
|
+
_supertokensUser?: AuthenticatedUser;
|
|
18
|
+
_supertokensRes?: Response;
|
|
19
|
+
_supertokensSession?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mock Supertokens modules
|
|
23
|
+
vi.mock('supertokens-node', () => ({
|
|
24
|
+
default: {
|
|
25
|
+
init: vi.fn(),
|
|
26
|
+
getUser: vi.fn(),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('supertokens-node/recipe/session', () => ({
|
|
31
|
+
default: {
|
|
32
|
+
init: vi.fn(),
|
|
33
|
+
getSession: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('supertokens-node/recipe/emailpassword', () => ({
|
|
38
|
+
default: {
|
|
39
|
+
init: vi.fn(),
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('supertokens-node/recipe/thirdparty', () => ({
|
|
44
|
+
default: {
|
|
45
|
+
init: vi.fn(),
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
vi.mock('supertokens-node/framework/express', () => ({
|
|
50
|
+
middleware: vi.fn(() => (_req: Request, _res: Response, next: NextFunction) => next()),
|
|
51
|
+
errorHandler: vi.fn(() => (_req: Request, _res: Response, next: NextFunction) => next()),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Mock request/response helpers
|
|
55
|
+
function createMockRequest(overrides: Partial<Request> = {}): Request {
|
|
56
|
+
return {
|
|
57
|
+
headers: {},
|
|
58
|
+
cookies: {},
|
|
59
|
+
path: '/',
|
|
60
|
+
originalUrl: '/',
|
|
61
|
+
...overrides,
|
|
62
|
+
} as unknown as Request;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createMockResponse(): Response {
|
|
66
|
+
const res = {
|
|
67
|
+
status: vi.fn().mockReturnThis(),
|
|
68
|
+
json: vi.fn().mockReturnThis(),
|
|
69
|
+
setHeader: vi.fn().mockReturnThis(),
|
|
70
|
+
redirect: vi.fn().mockReturnThis(),
|
|
71
|
+
};
|
|
72
|
+
return res as unknown as Response;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('supertokensAdapter', () => {
|
|
76
|
+
const validConfig: SupertokensAdapterConfig = {
|
|
77
|
+
connectionUri: 'http://localhost:3567',
|
|
78
|
+
appName: 'Test App',
|
|
79
|
+
apiDomain: 'http://localhost:3000',
|
|
80
|
+
websiteDomain: 'http://localhost:3000',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let adapter: ReturnType<typeof supertokensAdapter>;
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
vi.clearAllMocks();
|
|
87
|
+
adapter = supertokensAdapter(validConfig);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
vi.resetModules();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('name', () => {
|
|
95
|
+
it('should return "supertokens"', () => {
|
|
96
|
+
expect(adapter.name).toBe('supertokens');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('initialize', () => {
|
|
101
|
+
it('should return an array of middlewares', () => {
|
|
102
|
+
const middlewares = adapter.initialize();
|
|
103
|
+
expect(Array.isArray(middlewares)).toBe(true);
|
|
104
|
+
expect(middlewares).toHaveLength(2); // init middleware + supertokens middleware
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should store response on request for later use', async () => {
|
|
108
|
+
const middlewares = adapter.initialize() as RequestHandler[];
|
|
109
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
110
|
+
const res = createMockResponse();
|
|
111
|
+
const next = vi.fn();
|
|
112
|
+
|
|
113
|
+
// Apply first middleware (init)
|
|
114
|
+
await middlewares[0](req, res, next);
|
|
115
|
+
|
|
116
|
+
expect(req._supertokensRes).toBe(res);
|
|
117
|
+
expect(next).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should initialize Supertokens SDK on first request', async () => {
|
|
121
|
+
const supertokens = await import('supertokens-node');
|
|
122
|
+
const middlewares = adapter.initialize() as RequestHandler[];
|
|
123
|
+
const req = createMockRequest();
|
|
124
|
+
const res = createMockResponse();
|
|
125
|
+
const next = vi.fn();
|
|
126
|
+
|
|
127
|
+
await middlewares[0](req, res, next);
|
|
128
|
+
|
|
129
|
+
expect(supertokens.default.init).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
framework: 'express',
|
|
132
|
+
appInfo: expect.objectContaining({
|
|
133
|
+
appName: 'Test App',
|
|
134
|
+
apiDomain: 'http://localhost:3000',
|
|
135
|
+
websiteDomain: 'http://localhost:3000',
|
|
136
|
+
apiBasePath: '/auth',
|
|
137
|
+
websiteBasePath: '/auth',
|
|
138
|
+
}),
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should use custom apiBasePath when provided', async () => {
|
|
144
|
+
const supertokens = await import('supertokens-node');
|
|
145
|
+
const customAdapter = supertokensAdapter({
|
|
146
|
+
...validConfig,
|
|
147
|
+
apiBasePath: '/api/auth',
|
|
148
|
+
websiteBasePath: '/auth-ui',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const middlewares = customAdapter.initialize() as RequestHandler[];
|
|
152
|
+
const req = createMockRequest();
|
|
153
|
+
const res = createMockResponse();
|
|
154
|
+
const next = vi.fn();
|
|
155
|
+
|
|
156
|
+
await middlewares[0](req, res, next);
|
|
157
|
+
|
|
158
|
+
expect(supertokens.default.init).toHaveBeenCalledWith(
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
appInfo: expect.objectContaining({
|
|
161
|
+
apiBasePath: '/api/auth',
|
|
162
|
+
websiteBasePath: '/auth-ui',
|
|
163
|
+
}),
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should initialize EmailPassword recipe by default', async () => {
|
|
169
|
+
const EmailPassword = await import('supertokens-node/recipe/emailpassword');
|
|
170
|
+
|
|
171
|
+
const middlewares = adapter.initialize() as RequestHandler[];
|
|
172
|
+
const req = createMockRequest();
|
|
173
|
+
const res = createMockResponse();
|
|
174
|
+
const next = vi.fn();
|
|
175
|
+
|
|
176
|
+
await middlewares[0](req, res, next);
|
|
177
|
+
|
|
178
|
+
expect(EmailPassword.default.init).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should initialize ThirdParty recipe when social providers configured', async () => {
|
|
182
|
+
const ThirdParty = await import('supertokens-node/recipe/thirdparty');
|
|
183
|
+
|
|
184
|
+
const adapterWithProviders = supertokensAdapter({
|
|
185
|
+
...validConfig,
|
|
186
|
+
socialProviders: {
|
|
187
|
+
google: { clientId: 'google-id', clientSecret: 'google-secret' },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const middlewares = adapterWithProviders.initialize() as RequestHandler[];
|
|
192
|
+
const req = createMockRequest();
|
|
193
|
+
const res = createMockResponse();
|
|
194
|
+
const next = vi.fn();
|
|
195
|
+
|
|
196
|
+
await middlewares[0](req, res, next);
|
|
197
|
+
|
|
198
|
+
expect(ThirdParty.default.init).toHaveBeenCalledWith(
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
signInAndUpFeature: expect.objectContaining({
|
|
201
|
+
providers: expect.arrayContaining([
|
|
202
|
+
expect.objectContaining({
|
|
203
|
+
config: expect.objectContaining({
|
|
204
|
+
thirdPartyId: 'google',
|
|
205
|
+
}),
|
|
206
|
+
}),
|
|
207
|
+
]),
|
|
208
|
+
}),
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('isAuthenticated', () => {
|
|
215
|
+
it('should return true when session cookie exists', () => {
|
|
216
|
+
const req = createMockRequest({
|
|
217
|
+
cookies: { sAccessToken: 'some-token' },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(adapter.isAuthenticated(req)).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should return true when refresh token exists', () => {
|
|
224
|
+
const req = createMockRequest({
|
|
225
|
+
cookies: { sRefreshToken: 'refresh-token' },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(adapter.isAuthenticated(req)).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return true when Authorization header exists', () => {
|
|
232
|
+
const req = createMockRequest({
|
|
233
|
+
headers: { authorization: 'Bearer some-token' },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(adapter.isAuthenticated(req)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return false when no auth credentials exist', () => {
|
|
240
|
+
const req = createMockRequest();
|
|
241
|
+
|
|
242
|
+
expect(adapter.isAuthenticated(req)).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should return true when user is cached on request', () => {
|
|
246
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
247
|
+
req._supertokensUser = { id: 'user-123', email: 'test@example.com' };
|
|
248
|
+
|
|
249
|
+
expect(adapter.isAuthenticated(req)).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should return true when session is cached on request', () => {
|
|
253
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
254
|
+
req._supertokensSession = { getUserId: () => 'user-123' };
|
|
255
|
+
|
|
256
|
+
expect(adapter.isAuthenticated(req)).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('getUser', () => {
|
|
261
|
+
beforeEach(async () => {
|
|
262
|
+
// Initialize adapter to set initialized = true
|
|
263
|
+
const middlewares = adapter.initialize() as RequestHandler[];
|
|
264
|
+
const req = createMockRequest();
|
|
265
|
+
const res = createMockResponse();
|
|
266
|
+
const next = vi.fn();
|
|
267
|
+
await middlewares[0](req, res, next);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should return cached user if available', async () => {
|
|
271
|
+
const cachedUser: AuthenticatedUser = {
|
|
272
|
+
id: 'user-123',
|
|
273
|
+
email: 'test@example.com',
|
|
274
|
+
name: 'Test User',
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
278
|
+
req._supertokensUser = cachedUser;
|
|
279
|
+
req._supertokensRes = createMockResponse();
|
|
280
|
+
|
|
281
|
+
const user = await adapter.getUser(req);
|
|
282
|
+
expect(user).toEqual(cachedUser);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should return null when response is not stored on request', async () => {
|
|
286
|
+
const req = createMockRequest();
|
|
287
|
+
const user = await adapter.getUser(req);
|
|
288
|
+
expect(user).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should return null when no session exists', async () => {
|
|
292
|
+
const Session = await import('supertokens-node/recipe/session');
|
|
293
|
+
vi.mocked(Session.default.getSession).mockResolvedValue(null as never);
|
|
294
|
+
|
|
295
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
296
|
+
req._supertokensRes = createMockResponse();
|
|
297
|
+
|
|
298
|
+
const user = await adapter.getUser(req);
|
|
299
|
+
expect(user).toBeNull();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should return AuthenticatedUser when session is valid', async () => {
|
|
303
|
+
const Session = await import('supertokens-node/recipe/session');
|
|
304
|
+
const supertokens = await import('supertokens-node');
|
|
305
|
+
|
|
306
|
+
const mockSession = {
|
|
307
|
+
getUserId: vi.fn().mockReturnValue('user-123'),
|
|
308
|
+
getHandle: vi.fn().mockReturnValue('session-handle'),
|
|
309
|
+
getAccessTokenPayload: vi.fn().mockReturnValue({ roles: ['admin'] }),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const mockUserInfo = {
|
|
313
|
+
emails: ['test@example.com'],
|
|
314
|
+
thirdParty: [],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
vi.mocked(Session.default.getSession).mockResolvedValue(mockSession as never);
|
|
318
|
+
vi.mocked(supertokens.default.getUser).mockResolvedValue(mockUserInfo as never);
|
|
319
|
+
|
|
320
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
321
|
+
req._supertokensRes = createMockResponse();
|
|
322
|
+
|
|
323
|
+
const user = await adapter.getUser(req);
|
|
324
|
+
|
|
325
|
+
expect(user).toEqual({
|
|
326
|
+
id: 'user-123',
|
|
327
|
+
email: 'test@example.com',
|
|
328
|
+
name: 'test',
|
|
329
|
+
picture: undefined,
|
|
330
|
+
emailVerified: true,
|
|
331
|
+
roles: ['admin'],
|
|
332
|
+
raw: expect.objectContaining({
|
|
333
|
+
sessionHandle: 'session-handle',
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should cache user on request object', async () => {
|
|
339
|
+
const Session = await import('supertokens-node/recipe/session');
|
|
340
|
+
const supertokens = await import('supertokens-node');
|
|
341
|
+
|
|
342
|
+
const mockSession = {
|
|
343
|
+
getUserId: vi.fn().mockReturnValue('user-123'),
|
|
344
|
+
getHandle: vi.fn().mockReturnValue('session-handle'),
|
|
345
|
+
getAccessTokenPayload: vi.fn().mockReturnValue({}),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const mockUserInfo = {
|
|
349
|
+
emails: ['test@example.com'],
|
|
350
|
+
thirdParty: [],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
vi.mocked(Session.default.getSession).mockResolvedValue(mockSession as never);
|
|
354
|
+
vi.mocked(supertokens.default.getUser).mockResolvedValue(mockUserInfo as never);
|
|
355
|
+
|
|
356
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
357
|
+
req._supertokensRes = createMockResponse();
|
|
358
|
+
|
|
359
|
+
await adapter.getUser(req);
|
|
360
|
+
|
|
361
|
+
expect(req._supertokensUser).toBeDefined();
|
|
362
|
+
expect(req._supertokensUser?.id).toBe('user-123');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should return null and handle errors gracefully', async () => {
|
|
366
|
+
const Session = await import('supertokens-node/recipe/session');
|
|
367
|
+
vi.mocked(Session.default.getSession).mockRejectedValue(new Error('Session error'));
|
|
368
|
+
|
|
369
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
370
|
+
|
|
371
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
372
|
+
req._supertokensRes = createMockResponse();
|
|
373
|
+
|
|
374
|
+
const user = await adapter.getUser(req);
|
|
375
|
+
|
|
376
|
+
expect(user).toBeNull();
|
|
377
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
378
|
+
|
|
379
|
+
consoleSpy.mockRestore();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('hasRoles', () => {
|
|
384
|
+
it('should return true if user has the role', () => {
|
|
385
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
386
|
+
req._supertokensUser = {
|
|
387
|
+
id: 'user-123',
|
|
388
|
+
email: 'test@example.com',
|
|
389
|
+
roles: ['admin', 'user'],
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
expect(adapter.hasRoles!(req, ['admin'])).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should return true if user has all required roles', () => {
|
|
396
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
397
|
+
req._supertokensUser = {
|
|
398
|
+
id: 'user-123',
|
|
399
|
+
email: 'test@example.com',
|
|
400
|
+
roles: ['admin', 'user', 'editor'],
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
expect(adapter.hasRoles!(req, ['admin', 'editor'])).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should return false if user does not have the role', () => {
|
|
407
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
408
|
+
req._supertokensUser = {
|
|
409
|
+
id: 'user-123',
|
|
410
|
+
email: 'test@example.com',
|
|
411
|
+
roles: ['user'],
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
expect(adapter.hasRoles!(req, ['admin'])).toBe(false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should return false if user has no roles', () => {
|
|
418
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
419
|
+
req._supertokensUser = {
|
|
420
|
+
id: 'user-123',
|
|
421
|
+
email: 'test@example.com',
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
expect(adapter.hasRoles!(req, ['admin'])).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should return false if no user on request', () => {
|
|
428
|
+
const req = createMockRequest();
|
|
429
|
+
expect(adapter.hasRoles!(req, ['admin'])).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe('getAccessToken', () => {
|
|
434
|
+
it('should return null (session-based auth)', () => {
|
|
435
|
+
const req = createMockRequest({
|
|
436
|
+
cookies: { sAccessToken: 'some-token' },
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(adapter.getAccessToken!(req)).toBeNull();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('onUnauthorized', () => {
|
|
444
|
+
it('should return 401 with JSON response', () => {
|
|
445
|
+
const req = createMockRequest();
|
|
446
|
+
const res = createMockResponse();
|
|
447
|
+
|
|
448
|
+
adapter.onUnauthorized!(req, res);
|
|
449
|
+
|
|
450
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
451
|
+
expect(res.json).toHaveBeenCalledWith({
|
|
452
|
+
error: 'Unauthorized',
|
|
453
|
+
message: 'Authentication required. Please sign in.',
|
|
454
|
+
hint: 'Use the /auth endpoints to authenticate',
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe('shutdown', () => {
|
|
460
|
+
it('should resolve without error', async () => {
|
|
461
|
+
await expect(adapter.shutdown!()).resolves.toBeUndefined();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should reset initialization state', async () => {
|
|
465
|
+
// First initialize
|
|
466
|
+
const middlewares = adapter.initialize() as RequestHandler[];
|
|
467
|
+
const req = createMockRequest();
|
|
468
|
+
const res = createMockResponse();
|
|
469
|
+
const next = vi.fn();
|
|
470
|
+
await middlewares[0](req, res, next);
|
|
471
|
+
|
|
472
|
+
// Shutdown
|
|
473
|
+
await adapter.shutdown!();
|
|
474
|
+
|
|
475
|
+
// After shutdown, getUser should return null (not initialized)
|
|
476
|
+
const user = await adapter.getUser(createMockRequest());
|
|
477
|
+
expect(user).toBeNull();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe('configuration', () => {
|
|
482
|
+
it('should pass apiKey to Supertokens when provided', async () => {
|
|
483
|
+
const supertokens = await import('supertokens-node');
|
|
484
|
+
const adapterWithApiKey = supertokensAdapter({
|
|
485
|
+
...validConfig,
|
|
486
|
+
apiKey: 'my-api-key',
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const middlewares = adapterWithApiKey.initialize() as RequestHandler[];
|
|
490
|
+
const req = createMockRequest();
|
|
491
|
+
const res = createMockResponse();
|
|
492
|
+
const next = vi.fn();
|
|
493
|
+
|
|
494
|
+
await middlewares[0](req, res, next);
|
|
495
|
+
|
|
496
|
+
expect(supertokens.default.init).toHaveBeenCalledWith(
|
|
497
|
+
expect.objectContaining({
|
|
498
|
+
supertokens: expect.objectContaining({
|
|
499
|
+
apiKey: 'my-api-key',
|
|
500
|
+
}),
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should not initialize EmailPassword when disabled', async () => {
|
|
506
|
+
const EmailPassword = await import('supertokens-node/recipe/emailpassword');
|
|
507
|
+
|
|
508
|
+
const adapterNoEmail = supertokensAdapter({
|
|
509
|
+
...validConfig,
|
|
510
|
+
enableEmailPassword: false,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const middlewares = adapterNoEmail.initialize() as RequestHandler[];
|
|
514
|
+
const req = createMockRequest();
|
|
515
|
+
const res = createMockResponse();
|
|
516
|
+
const next = vi.fn();
|
|
517
|
+
|
|
518
|
+
await middlewares[0](req, res, next);
|
|
519
|
+
|
|
520
|
+
expect(EmailPassword.default.init).not.toHaveBeenCalled();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should configure multiple social providers', async () => {
|
|
524
|
+
const ThirdParty = await import('supertokens-node/recipe/thirdparty');
|
|
525
|
+
|
|
526
|
+
const adapterWithProviders = supertokensAdapter({
|
|
527
|
+
...validConfig,
|
|
528
|
+
socialProviders: {
|
|
529
|
+
google: { clientId: 'google-id', clientSecret: 'google-secret' },
|
|
530
|
+
github: { clientId: 'github-id', clientSecret: 'github-secret' },
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const middlewares = adapterWithProviders.initialize() as RequestHandler[];
|
|
535
|
+
const req = createMockRequest();
|
|
536
|
+
const res = createMockResponse();
|
|
537
|
+
const next = vi.fn();
|
|
538
|
+
|
|
539
|
+
await middlewares[0](req, res, next);
|
|
540
|
+
|
|
541
|
+
expect(ThirdParty.default.init).toHaveBeenCalledWith(
|
|
542
|
+
expect.objectContaining({
|
|
543
|
+
signInAndUpFeature: expect.objectContaining({
|
|
544
|
+
providers: expect.arrayContaining([
|
|
545
|
+
expect.objectContaining({
|
|
546
|
+
config: expect.objectContaining({
|
|
547
|
+
thirdPartyId: 'google',
|
|
548
|
+
}),
|
|
549
|
+
}),
|
|
550
|
+
expect.objectContaining({
|
|
551
|
+
config: expect.objectContaining({
|
|
552
|
+
thirdPartyId: 'github',
|
|
553
|
+
}),
|
|
554
|
+
}),
|
|
555
|
+
]),
|
|
556
|
+
}),
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe('supertokensAdapter edge cases', () => {
|
|
564
|
+
it('should handle missing cookies object', () => {
|
|
565
|
+
const adapter = supertokensAdapter({
|
|
566
|
+
connectionUri: 'http://localhost:3567',
|
|
567
|
+
appName: 'Test',
|
|
568
|
+
apiDomain: 'http://localhost:3000',
|
|
569
|
+
websiteDomain: 'http://localhost:3000',
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const req = {
|
|
573
|
+
headers: {},
|
|
574
|
+
path: '/',
|
|
575
|
+
originalUrl: '/',
|
|
576
|
+
} as unknown as Request;
|
|
577
|
+
|
|
578
|
+
expect(adapter.isAuthenticated(req)).toBe(false);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should handle empty user emails array', async () => {
|
|
582
|
+
const adapter = supertokensAdapter({
|
|
583
|
+
connectionUri: 'http://localhost:3567',
|
|
584
|
+
appName: 'Test',
|
|
585
|
+
apiDomain: 'http://localhost:3000',
|
|
586
|
+
websiteDomain: 'http://localhost:3000',
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Initialize first
|
|
590
|
+
const middlewares = adapter.initialize() as RequestHandler[];
|
|
591
|
+
const initReq = createMockRequest();
|
|
592
|
+
const initRes = createMockResponse();
|
|
593
|
+
await middlewares[0](initReq, initRes, vi.fn());
|
|
594
|
+
|
|
595
|
+
const Session = await import('supertokens-node/recipe/session');
|
|
596
|
+
const supertokens = await import('supertokens-node');
|
|
597
|
+
|
|
598
|
+
const mockSession = {
|
|
599
|
+
getUserId: vi.fn().mockReturnValue('user-123'),
|
|
600
|
+
getHandle: vi.fn().mockReturnValue('session-handle'),
|
|
601
|
+
getAccessTokenPayload: vi.fn().mockReturnValue({}),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const mockUserInfo = {
|
|
605
|
+
emails: [],
|
|
606
|
+
thirdParty: [{ userId: 'google-user-id' }],
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
vi.mocked(Session.default.getSession).mockResolvedValue(mockSession as never);
|
|
610
|
+
vi.mocked(supertokens.default.getUser).mockResolvedValue(mockUserInfo as never);
|
|
611
|
+
|
|
612
|
+
const req = createMockRequest() as SupertokensExtendedRequest;
|
|
613
|
+
req._supertokensRes = createMockResponse();
|
|
614
|
+
|
|
615
|
+
const user = await adapter.getUser(req);
|
|
616
|
+
|
|
617
|
+
expect(user?.email).toBe('');
|
|
618
|
+
expect(user?.name).toBe('google-user-id');
|
|
619
|
+
expect(user?.emailVerified).toBe(false);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
@@ -127,6 +127,47 @@ export interface BasicAdapterConfig {
|
|
|
127
127
|
realm?: string;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Supertokens adapter configuration
|
|
132
|
+
*/
|
|
133
|
+
export interface SupertokensAdapterConfig {
|
|
134
|
+
/** Supertokens connection URI (e.g., 'http://localhost:3567') */
|
|
135
|
+
connectionUri: string;
|
|
136
|
+
|
|
137
|
+
/** Supertokens API key (for managed service) */
|
|
138
|
+
apiKey?: string;
|
|
139
|
+
|
|
140
|
+
/** App name for branding */
|
|
141
|
+
appName: string;
|
|
142
|
+
|
|
143
|
+
/** API domain (e.g., 'http://localhost:3000') */
|
|
144
|
+
apiDomain: string;
|
|
145
|
+
|
|
146
|
+
/** Website domain (e.g., 'http://localhost:3000') */
|
|
147
|
+
websiteDomain: string;
|
|
148
|
+
|
|
149
|
+
/** API base path (default: '/auth') */
|
|
150
|
+
apiBasePath?: string;
|
|
151
|
+
|
|
152
|
+
/** Website base path (default: '/auth') */
|
|
153
|
+
websiteBasePath?: string;
|
|
154
|
+
|
|
155
|
+
/** Enable email/password auth (default: true) */
|
|
156
|
+
enableEmailPassword?: boolean;
|
|
157
|
+
|
|
158
|
+
/** Social login providers */
|
|
159
|
+
socialProviders?: {
|
|
160
|
+
google?: { clientId: string; clientSecret: string };
|
|
161
|
+
apple?: {
|
|
162
|
+
clientId: string;
|
|
163
|
+
clientSecret: string;
|
|
164
|
+
keyId: string;
|
|
165
|
+
teamId: string;
|
|
166
|
+
};
|
|
167
|
+
github?: { clientId: string; clientSecret: string };
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
130
171
|
/**
|
|
131
172
|
* Auth plugin configuration
|
|
132
173
|
*/
|
|
@@ -163,3 +204,42 @@ export interface AuthenticatedRequest extends Request {
|
|
|
163
204
|
export function isAuthenticatedRequest(req: Request): req is AuthenticatedRequest {
|
|
164
205
|
return 'auth' in req && (req as AuthenticatedRequest).auth?.isAuthenticated === true;
|
|
165
206
|
}
|
|
207
|
+
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
+
// Environment Configuration Types
|
|
210
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Auth plugin state
|
|
214
|
+
*/
|
|
215
|
+
export type AuthPluginState = 'disabled' | 'enabled' | 'error';
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Options for createAuthPluginFromEnv (overrides only)
|
|
219
|
+
*/
|
|
220
|
+
export interface AuthEnvPluginOptions {
|
|
221
|
+
/** Paths to exclude from authentication (can also use AUTH_EXCLUDE_PATHS env var) */
|
|
222
|
+
excludePaths?: string[];
|
|
223
|
+
/** Whether auth is required (can also use AUTH_REQUIRED env var, default: true) */
|
|
224
|
+
authRequired?: boolean;
|
|
225
|
+
/** Enable debug logging (can also use AUTH_DEBUG env var) */
|
|
226
|
+
debug?: boolean;
|
|
227
|
+
/** Custom unauthorized handler */
|
|
228
|
+
onUnauthorized?: (req: Request, res: Response) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Auth configuration status returned by getAuthStatus()
|
|
233
|
+
*/
|
|
234
|
+
export interface AuthConfigStatus {
|
|
235
|
+
/** Current plugin state */
|
|
236
|
+
state: AuthPluginState;
|
|
237
|
+
/** Active adapter name (null if disabled or error) */
|
|
238
|
+
adapter: string | null;
|
|
239
|
+
/** Error message if state is 'error' */
|
|
240
|
+
error?: string;
|
|
241
|
+
/** List of missing environment variables if state is 'error' */
|
|
242
|
+
missingVars?: string[];
|
|
243
|
+
/** Current configuration with secrets masked */
|
|
244
|
+
config?: Record<string, string>;
|
|
245
|
+
}
|