@qwickapps/server 1.5.1 → 1.6.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +41 -0
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/guards.d.ts.map +1 -1
  6. package/dist/core/guards.js +77 -0
  7. package/dist/core/guards.js.map +1 -1
  8. package/dist/core/health-manager.d.ts +4 -0
  9. package/dist/core/health-manager.d.ts.map +1 -1
  10. package/dist/core/health-manager.js +6 -1
  11. package/dist/core/health-manager.js.map +1 -1
  12. package/dist/core/plugin-registry.d.ts +55 -5
  13. package/dist/core/plugin-registry.d.ts.map +1 -1
  14. package/dist/core/plugin-registry.js +57 -19
  15. package/dist/core/plugin-registry.js.map +1 -1
  16. package/dist/core/types.d.ts +2 -0
  17. package/dist/core/types.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/plugins/api-keys/api-keys-plugin.d.ts +46 -0
  23. package/dist/plugins/api-keys/api-keys-plugin.d.ts.map +1 -0
  24. package/dist/plugins/api-keys/api-keys-plugin.js +329 -0
  25. package/dist/plugins/api-keys/api-keys-plugin.js.map +1 -0
  26. package/dist/plugins/api-keys/index.d.ts +14 -0
  27. package/dist/plugins/api-keys/index.d.ts.map +1 -0
  28. package/dist/plugins/api-keys/index.js +17 -0
  29. package/dist/plugins/api-keys/index.js.map +1 -0
  30. package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts +74 -0
  31. package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts.map +1 -0
  32. package/dist/plugins/api-keys/middleware/bearer-token-auth.js +201 -0
  33. package/dist/plugins/api-keys/middleware/bearer-token-auth.js.map +1 -0
  34. package/dist/plugins/api-keys/middleware/index.d.ts +7 -0
  35. package/dist/plugins/api-keys/middleware/index.d.ts.map +1 -0
  36. package/dist/plugins/api-keys/middleware/index.js +7 -0
  37. package/dist/plugins/api-keys/middleware/index.js.map +1 -0
  38. package/dist/plugins/api-keys/stores/index.d.ts +7 -0
  39. package/dist/plugins/api-keys/stores/index.d.ts.map +1 -0
  40. package/dist/plugins/api-keys/stores/index.js +7 -0
  41. package/dist/plugins/api-keys/stores/index.js.map +1 -0
  42. package/dist/plugins/api-keys/stores/postgres-store.d.ts +34 -0
  43. package/dist/plugins/api-keys/stores/postgres-store.d.ts.map +1 -0
  44. package/dist/plugins/api-keys/stores/postgres-store.js +360 -0
  45. package/dist/plugins/api-keys/stores/postgres-store.js.map +1 -0
  46. package/dist/plugins/api-keys/types.d.ts +268 -0
  47. package/dist/plugins/api-keys/types.d.ts.map +1 -0
  48. package/dist/plugins/api-keys/types.js +56 -0
  49. package/dist/plugins/api-keys/types.js.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -1
  51. package/dist/plugins/auth/auth-plugin.js +17 -1
  52. package/dist/plugins/auth/auth-plugin.js.map +1 -1
  53. package/dist/plugins/auth/auth-plugin.test.js +133 -0
  54. package/dist/plugins/auth/auth-plugin.test.js.map +1 -1
  55. package/dist/plugins/auth/env-config.d.ts.map +1 -1
  56. package/dist/plugins/auth/env-config.js +6 -2
  57. package/dist/plugins/auth/env-config.js.map +1 -1
  58. package/dist/plugins/auth/types.d.ts +10 -0
  59. package/dist/plugins/auth/types.d.ts.map +1 -1
  60. package/dist/plugins/auth/types.js.map +1 -1
  61. package/dist/plugins/devices/__tests__/token-utils.test.js +4 -2
  62. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -1
  63. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  64. package/dist/plugins/frontend-app-plugin.js +21 -4
  65. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  66. package/dist/plugins/index.d.ts +2 -0
  67. package/dist/plugins/index.d.ts.map +1 -1
  68. package/dist/plugins/index.js +2 -0
  69. package/dist/plugins/index.js.map +1 -1
  70. package/dist/plugins/qwickbrain/index.d.ts +25 -0
  71. package/dist/plugins/qwickbrain/index.d.ts.map +1 -0
  72. package/dist/plugins/qwickbrain/index.js +24 -0
  73. package/dist/plugins/qwickbrain/index.js.map +1 -0
  74. package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts +23 -0
  75. package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -0
  76. package/dist/plugins/qwickbrain/qwickbrain-plugin.js +528 -0
  77. package/dist/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -0
  78. package/dist/plugins/qwickbrain/types.d.ts +131 -0
  79. package/dist/plugins/qwickbrain/types.d.ts.map +1 -0
  80. package/dist/plugins/qwickbrain/types.js +9 -0
  81. package/dist/plugins/qwickbrain/types.js.map +1 -0
  82. package/dist/plugins/users/__tests__/postgres-store.test.js +1 -0
  83. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -1
  84. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  85. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  86. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  87. package/dist/plugins/users/stores/postgres-store.js +59 -1
  88. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  89. package/dist/plugins/users/types.d.ts +22 -0
  90. package/dist/plugins/users/types.d.ts.map +1 -1
  91. package/dist-ui/assets/index-5nX8fM1a.js +469 -0
  92. package/dist-ui/assets/index-5nX8fM1a.js.map +1 -0
  93. package/dist-ui/index.html +1 -1
  94. package/dist-ui-lib/api/controlPanelApi.d.ts +68 -0
  95. package/dist-ui-lib/components/index.d.ts +2 -1
  96. package/dist-ui-lib/index.js +2642 -2281
  97. package/dist-ui-lib/index.js.map +1 -1
  98. package/dist-ui-lib/pages/APIKeysPage.d.ts +13 -0
  99. package/dist-ui-lib/pages/AcceptInvitationPage.d.ts +28 -0
  100. package/package.json +3 -2
  101. package/src/core/control-panel.ts +47 -0
  102. package/src/core/guards.ts +89 -0
  103. package/src/core/health-manager.ts +6 -1
  104. package/src/core/plugin-registry.ts +123 -25
  105. package/src/core/types.ts +2 -0
  106. package/src/index.ts +11 -0
  107. package/src/plugins/api-keys/api-keys-plugin.ts +397 -0
  108. package/src/plugins/api-keys/index.ts +49 -0
  109. package/src/plugins/api-keys/middleware/bearer-token-auth.ts +250 -0
  110. package/src/plugins/api-keys/middleware/index.ts +12 -0
  111. package/src/plugins/api-keys/stores/index.ts +7 -0
  112. package/src/plugins/api-keys/stores/postgres-store.ts +487 -0
  113. package/src/plugins/api-keys/types.ts +243 -0
  114. package/src/plugins/auth/auth-plugin.test.ts +167 -0
  115. package/src/plugins/auth/auth-plugin.ts +17 -1
  116. package/src/plugins/auth/env-config.ts +6 -2
  117. package/src/plugins/auth/types.ts +10 -0
  118. package/src/plugins/devices/__tests__/token-utils.test.ts +4 -2
  119. package/src/plugins/frontend-app-plugin.ts +24 -4
  120. package/src/plugins/index.ts +15 -0
  121. package/src/plugins/qwickbrain/index.ts +33 -0
  122. package/src/plugins/qwickbrain/qwickbrain-plugin.ts +642 -0
  123. package/src/plugins/qwickbrain/types.ts +146 -0
  124. package/src/plugins/users/__tests__/postgres-store.test.ts +1 -0
  125. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  126. package/src/plugins/users/stores/postgres-store.ts +69 -0
  127. package/src/plugins/users/types.ts +25 -0
  128. package/ui/src/App.tsx +6 -1
  129. package/ui/src/api/controlPanelApi.ts +206 -37
  130. package/ui/src/components/index.ts +6 -0
  131. package/ui/src/pages/APIKeysPage.tsx +661 -0
  132. package/ui/src/pages/AcceptInvitationPage.tsx +169 -0
  133. package/ui/src/pages/UsersPage.tsx +225 -2
  134. package/dist-ui/assets/index-CynOqPkb.js +0 -469
  135. package/dist-ui/assets/index-CynOqPkb.js.map +0 -1
@@ -0,0 +1,397 @@
1
+ /**
2
+ * API Keys Plugin
3
+ *
4
+ * API key authentication and management plugin for @qwickapps/server.
5
+ * Provides API key generation, storage, and verification with PostgreSQL RLS.
6
+ *
7
+ * This plugin depends on the Users Plugin for user identity.
8
+ *
9
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
10
+ */
11
+
12
+ import type { Request, Response } from 'express';
13
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
14
+ import type {
15
+ ApiKeysPluginConfig,
16
+ ApiKeyStore,
17
+ CreateApiKeyParams,
18
+ UpdateApiKeyParams,
19
+ ApiKey,
20
+ } from './types.js';
21
+ import type { AuthenticatedRequest } from '../auth/types.js';
22
+ import {
23
+ CreateApiKeySchema,
24
+ UpdateApiKeySchema,
25
+ } from './types.js';
26
+
27
+ // Store instance for helper access
28
+ let currentStore: ApiKeyStore | null = null;
29
+
30
+ /**
31
+ * Create the API Keys plugin
32
+ */
33
+ export function createApiKeysPlugin(config: ApiKeysPluginConfig): Plugin {
34
+ const debug = config.debug || false;
35
+ // Routes are mounted under /api by the control panel, so don't include /api in prefix
36
+ const apiPrefix = config.api?.prefix || '/api-keys';
37
+ const apiEnabled = config.api?.enabled !== false;
38
+
39
+ function log(message: string, data?: Record<string, unknown>) {
40
+ if (debug) {
41
+ console.log(`[ApiKeysPlugin] ${message}`, data || '');
42
+ }
43
+ }
44
+
45
+ return {
46
+ id: 'api-keys',
47
+ name: 'API Keys',
48
+ version: '1.0.0',
49
+
50
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
51
+ log('Starting API keys plugin');
52
+
53
+ // Check for users plugin dependency
54
+ if (!registry.hasPlugin('users')) {
55
+ throw new Error('API Keys plugin requires Users plugin to be loaded first');
56
+ }
57
+
58
+ // Initialize the store (creates tables and RLS policies if needed)
59
+ await config.store.initialize();
60
+ log('API keys plugin migrations complete');
61
+
62
+ // Store reference for helper access
63
+ currentStore = config.store;
64
+
65
+ // Register health check
66
+ registry.registerHealthCheck({
67
+ name: 'api-keys-store',
68
+ type: 'custom',
69
+ check: async () => {
70
+ try {
71
+ // Simple health check - store is accessible
72
+ return { healthy: currentStore !== null };
73
+ } catch {
74
+ return { healthy: false };
75
+ }
76
+ },
77
+ });
78
+
79
+ // Add API routes if enabled
80
+ if (apiEnabled) {
81
+ // POST /api-keys - Create a new API key
82
+ registry.addRoute({
83
+ method: 'post',
84
+ path: apiPrefix,
85
+ pluginId: 'api-keys',
86
+ handler: async (req: Request, res: Response) => {
87
+ try {
88
+ const authReq = req as AuthenticatedRequest;
89
+ const userId = authReq.auth?.user?.id;
90
+
91
+ if (!userId) {
92
+ return res.status(401).json({ error: 'Authentication required' });
93
+ }
94
+
95
+ // Validate request body
96
+ const validation = CreateApiKeySchema.safeParse(req.body);
97
+ if (!validation.success) {
98
+ return res.status(400).json({
99
+ error: 'Invalid request',
100
+ });
101
+ }
102
+
103
+ const params: CreateApiKeyParams = {
104
+ user_id: userId,
105
+ ...validation.data,
106
+ };
107
+
108
+ // Create the API key
109
+ const apiKey = await config.store.create(params);
110
+
111
+ // Return the key with plaintext (ONLY time plaintext is accessible)
112
+ res.status(201).json({
113
+ id: apiKey.id,
114
+ name: apiKey.name,
115
+ key: apiKey.plaintext_key, // Client must save this - won't be shown again
116
+ key_prefix: apiKey.key_prefix,
117
+ key_type: apiKey.key_type,
118
+ scopes: apiKey.scopes,
119
+ expires_at: apiKey.expires_at,
120
+ is_active: apiKey.is_active,
121
+ created_at: apiKey.created_at,
122
+ });
123
+ } catch (error) {
124
+ console.error('[ApiKeysPlugin] Create API key error:', error);
125
+ res.status(500).json({ error: 'Failed to create API key' });
126
+ }
127
+ },
128
+ });
129
+
130
+ // GET /api-keys - List current user's API keys
131
+ registry.addRoute({
132
+ method: 'get',
133
+ path: apiPrefix,
134
+ pluginId: 'api-keys',
135
+ handler: async (req: Request, res: Response) => {
136
+ try {
137
+ const authReq = req as AuthenticatedRequest;
138
+ const userId = authReq.auth?.user?.id;
139
+
140
+ if (!userId) {
141
+ return res.status(401).json({ error: 'Authentication required' });
142
+ }
143
+
144
+ const keys = await config.store.list(userId);
145
+
146
+ // Remove sensitive fields from response
147
+ const sanitized = keys.map(key => ({
148
+ id: key.id,
149
+ name: key.name,
150
+ key_prefix: key.key_prefix,
151
+ key_type: key.key_type,
152
+ scopes: key.scopes,
153
+ last_used_at: key.last_used_at,
154
+ expires_at: key.expires_at,
155
+ is_active: key.is_active,
156
+ created_at: key.created_at,
157
+ updated_at: key.updated_at,
158
+ }));
159
+
160
+ res.json({ keys: sanitized });
161
+ } catch (error) {
162
+ console.error('[ApiKeysPlugin] List API keys error:', error);
163
+ res.status(500).json({ error: 'Failed to list API keys' });
164
+ }
165
+ },
166
+ });
167
+
168
+ // GET /api-keys/:id - Get a specific API key
169
+ registry.addRoute({
170
+ method: 'get',
171
+ path: `${apiPrefix}/:id`,
172
+ pluginId: 'api-keys',
173
+ handler: async (req: Request, res: Response) => {
174
+ try {
175
+ const authReq = req as AuthenticatedRequest;
176
+ const userId = authReq.auth?.user?.id;
177
+
178
+ if (!userId) {
179
+ return res.status(401).json({ error: 'Authentication required' });
180
+ }
181
+
182
+ const { id } = req.params;
183
+ const key = await config.store.get(userId, id);
184
+
185
+ if (!key) {
186
+ return res.status(404).json({ error: 'API key not found' });
187
+ }
188
+
189
+ // Remove sensitive fields from response
190
+ const sanitized = {
191
+ id: key.id,
192
+ name: key.name,
193
+ key_prefix: key.key_prefix,
194
+ key_type: key.key_type,
195
+ scopes: key.scopes,
196
+ last_used_at: key.last_used_at,
197
+ expires_at: key.expires_at,
198
+ is_active: key.is_active,
199
+ created_at: key.created_at,
200
+ updated_at: key.updated_at,
201
+ };
202
+
203
+ res.json(sanitized);
204
+ } catch (error) {
205
+ console.error('[ApiKeysPlugin] Get API key error:', error);
206
+ res.status(500).json({ error: 'Failed to get API key' });
207
+ }
208
+ },
209
+ });
210
+
211
+ // PUT /api-keys/:id - Update an API key
212
+ registry.addRoute({
213
+ method: 'put',
214
+ path: `${apiPrefix}/:id`,
215
+ pluginId: 'api-keys',
216
+ handler: async (req: Request, res: Response) => {
217
+ try {
218
+ const authReq = req as AuthenticatedRequest;
219
+ const userId = authReq.auth?.user?.id;
220
+
221
+ if (!userId) {
222
+ return res.status(401).json({ error: 'Authentication required' });
223
+ }
224
+
225
+ // Validate request body
226
+ const validation = UpdateApiKeySchema.safeParse(req.body);
227
+ if (!validation.success) {
228
+ return res.status(400).json({
229
+ error: 'Invalid request',
230
+ });
231
+ }
232
+
233
+ const { id } = req.params;
234
+ const params: UpdateApiKeyParams = validation.data;
235
+
236
+ const updated = await config.store.update(userId, id, params);
237
+
238
+ if (!updated) {
239
+ return res.status(404).json({ error: 'API key not found' });
240
+ }
241
+
242
+ // Remove sensitive fields from response
243
+ const sanitized = {
244
+ id: updated.id,
245
+ name: updated.name,
246
+ key_prefix: updated.key_prefix,
247
+ key_type: updated.key_type,
248
+ scopes: updated.scopes,
249
+ last_used_at: updated.last_used_at,
250
+ expires_at: updated.expires_at,
251
+ is_active: updated.is_active,
252
+ created_at: updated.created_at,
253
+ updated_at: updated.updated_at,
254
+ };
255
+
256
+ res.json(sanitized);
257
+ } catch (error) {
258
+ console.error('[ApiKeysPlugin] Update API key error:', error);
259
+ res.status(500).json({ error: 'Failed to update API key' });
260
+ }
261
+ },
262
+ });
263
+
264
+ // DELETE /api-keys/:id - Delete an API key
265
+ registry.addRoute({
266
+ method: 'delete',
267
+ path: `${apiPrefix}/:id`,
268
+ pluginId: 'api-keys',
269
+ handler: async (req: Request, res: Response) => {
270
+ try {
271
+ const authReq = req as AuthenticatedRequest;
272
+ const userId = authReq.auth?.user?.id;
273
+
274
+ if (!userId) {
275
+ return res.status(401).json({ error: 'Authentication required' });
276
+ }
277
+
278
+ const { id } = req.params;
279
+ const deleted = await config.store.delete(userId, id);
280
+
281
+ if (!deleted) {
282
+ return res.status(404).json({ error: 'API key not found' });
283
+ }
284
+
285
+ res.status(204).send();
286
+ } catch (error) {
287
+ console.error('[ApiKeysPlugin] Delete API key error:', error);
288
+ res.status(500).json({ error: 'Failed to delete API key' });
289
+ }
290
+ },
291
+ });
292
+ }
293
+
294
+ log('API keys plugin started');
295
+ },
296
+
297
+ async onStop(): Promise<void> {
298
+ log('Stopping API keys plugin');
299
+ if (currentStore) {
300
+ await currentStore.shutdown();
301
+ }
302
+ currentStore = null;
303
+ log('API keys plugin stopped');
304
+ },
305
+ };
306
+ }
307
+
308
+ // ========================================
309
+ // Helper Functions
310
+ // ========================================
311
+
312
+ /**
313
+ * Get the current API keys store instance
314
+ */
315
+ export function getApiKeysStore(): ApiKeyStore | null {
316
+ return currentStore;
317
+ }
318
+
319
+ /**
320
+ * Verify an API key and return the associated key record
321
+ * Returns null if key is invalid, expired, or inactive
322
+ */
323
+ export async function verifyApiKey(plaintextKey: string): Promise<ApiKey | null> {
324
+ if (!currentStore) {
325
+ throw new Error('API Keys plugin not initialized');
326
+ }
327
+
328
+ const key = await currentStore.verify(plaintextKey);
329
+
330
+ // Update last_used_at timestamp if key is valid
331
+ if (key) {
332
+ await currentStore.recordUsage(key.id).catch(err => {
333
+ console.error('[ApiKeysPlugin] Failed to record key usage:', err);
334
+ });
335
+ }
336
+
337
+ return key;
338
+ }
339
+
340
+ /**
341
+ * Create an API key for a user
342
+ */
343
+ export async function createApiKey(params: CreateApiKeyParams): Promise<ApiKey> {
344
+ if (!currentStore) {
345
+ throw new Error('API Keys plugin not initialized');
346
+ }
347
+
348
+ return currentStore.create(params);
349
+ }
350
+
351
+ /**
352
+ * List all API keys for a user
353
+ */
354
+ export async function listApiKeys(userId: string): Promise<ApiKey[]> {
355
+ if (!currentStore) {
356
+ throw new Error('API Keys plugin not initialized');
357
+ }
358
+
359
+ return currentStore.list(userId);
360
+ }
361
+
362
+ /**
363
+ * Get a specific API key
364
+ */
365
+ export async function getApiKey(userId: string, keyId: string): Promise<ApiKey | null> {
366
+ if (!currentStore) {
367
+ throw new Error('API Keys plugin not initialized');
368
+ }
369
+
370
+ return currentStore.get(userId, keyId);
371
+ }
372
+
373
+ /**
374
+ * Update an API key
375
+ */
376
+ export async function updateApiKey(
377
+ userId: string,
378
+ keyId: string,
379
+ params: UpdateApiKeyParams
380
+ ): Promise<ApiKey | null> {
381
+ if (!currentStore) {
382
+ throw new Error('API Keys plugin not initialized');
383
+ }
384
+
385
+ return currentStore.update(userId, keyId, params);
386
+ }
387
+
388
+ /**
389
+ * Delete an API key
390
+ */
391
+ export async function deleteApiKey(userId: string, keyId: string): Promise<boolean> {
392
+ if (!currentStore) {
393
+ throw new Error('API Keys plugin not initialized');
394
+ }
395
+
396
+ return currentStore.delete(userId, keyId);
397
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * API Keys Plugin Index
3
+ *
4
+ * API key authentication and management plugin with PostgreSQL RLS.
5
+ * Depends on the Users Plugin for user identity.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ // Main plugin
11
+ export {
12
+ createApiKeysPlugin,
13
+ getApiKeysStore,
14
+ verifyApiKey,
15
+ createApiKey,
16
+ listApiKeys,
17
+ getApiKey,
18
+ updateApiKey,
19
+ deleteApiKey,
20
+ } from './api-keys-plugin.js';
21
+
22
+ // Types
23
+ export type {
24
+ ApiKeysPluginConfig,
25
+ ApiKeyStore,
26
+ ApiKey,
27
+ ApiKeyWithPlaintext,
28
+ ApiKeyScope,
29
+ ApiKeyType,
30
+ CreateApiKeyParams,
31
+ UpdateApiKeyParams,
32
+ PostgresApiKeyStoreConfig,
33
+ ApiKeysApiConfig,
34
+ } from './types.js';
35
+
36
+ // Zod schemas
37
+ export {
38
+ ApiKeyScopeSchema,
39
+ ApiKeyTypeSchema,
40
+ CreateApiKeySchema,
41
+ UpdateApiKeySchema,
42
+ ApiKeySchema,
43
+ } from './types.js';
44
+
45
+ // Stores
46
+ export { postgresApiKeyStore } from './stores/index.js';
47
+
48
+ // Middleware
49
+ export { bearerTokenAuth } from './middleware/index.js';
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Bearer Token Authentication Middleware
3
+ *
4
+ * Middleware for authenticating API requests using Bearer tokens (API keys).
5
+ * Verifies the token, checks expiration and active status, and attaches
6
+ * the authenticated key info to the request.
7
+ *
8
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
9
+ */
10
+
11
+ import type { Request, Response, NextFunction } from 'express';
12
+ import type { ApiKey, ApiKeyScope } from '../types.js';
13
+ import { getApiKeysStore } from '../api-keys-plugin.js';
14
+ import { incrementLimit, isLimited } from '../../rate-limit/rate-limit-service.js';
15
+
16
+ /**
17
+ * Extended Express Request with API key authentication info
18
+ */
19
+ export interface ApiKeyAuthenticatedRequest extends Request {
20
+ apiKey?: {
21
+ id: string;
22
+ user_id: string;
23
+ scopes: ApiKeyScope[];
24
+ key_type: 'm2m' | 'pat';
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Options for bearer token authentication middleware
30
+ */
31
+ export interface BearerTokenAuthOptions {
32
+ /** Required scopes (all must be present) */
33
+ requiredScopes?: ApiKeyScope[];
34
+ /** Allow only specific key types */
35
+ allowedKeyTypes?: ('m2m' | 'pat')[];
36
+ /** Custom error handler */
37
+ onUnauthorized?: (req: Request, res: Response, reason: string) => void;
38
+ }
39
+
40
+ /**
41
+ * Extract Bearer token from Authorization header
42
+ *
43
+ * @param req Express request
44
+ * @returns Bearer token or null if not found
45
+ */
46
+ function extractBearerToken(req: Request): string | null {
47
+ const authHeader = req.headers.authorization;
48
+
49
+ if (!authHeader) {
50
+ return null;
51
+ }
52
+
53
+ // Check for "Bearer <token>" format
54
+ const parts = authHeader.split(' ');
55
+ if (parts.length !== 2 || parts[0] !== 'Bearer') {
56
+ return null;
57
+ }
58
+
59
+ return parts[1];
60
+ }
61
+
62
+ /**
63
+ * Check if API key has all required scopes
64
+ *
65
+ * @param keyScopes Scopes granted to the API key
66
+ * @param requiredScopes Scopes required for the endpoint
67
+ * @returns True if key has all required scopes
68
+ */
69
+ function hasRequiredScopes(keyScopes: ApiKeyScope[], requiredScopes: ApiKeyScope[]): boolean {
70
+ return requiredScopes.every(required => keyScopes.includes(required));
71
+ }
72
+
73
+ /**
74
+ * Default unauthorized handler
75
+ */
76
+ function defaultUnauthorizedHandler(req: Request, res: Response, reason: string): void {
77
+ res.status(401).json({
78
+ error: 'Unauthorized',
79
+ message: reason,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Get rate limit identifier for API key authentication
85
+ * Uses IP address as the identifier
86
+ */
87
+ function getRateLimitIdentifier(req: Request): string {
88
+ const ip = req.ip ||
89
+ req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ||
90
+ req.socket?.remoteAddress ||
91
+ 'unknown';
92
+ return `api-key-auth:${ip}`;
93
+ }
94
+
95
+ /**
96
+ * Check if rate limit has been exceeded
97
+ * Uses the existing rate-limit plugin with specific limits for API key auth
98
+ */
99
+ async function checkRateLimit(identifier: string): Promise<boolean> {
100
+ try {
101
+ // Check rate limit: 100 requests per 15 minutes
102
+ return await isLimited(identifier, {
103
+ maxRequests: 100,
104
+ windowMs: 15 * 60 * 1000, // 15 minutes
105
+ strategy: 'sliding-window',
106
+ });
107
+ } catch (error) {
108
+ // If rate limit service not available, allow the request
109
+ // (fail open - let other security measures handle it)
110
+ console.warn('[bearerTokenAuth] Rate limit service unavailable:', error);
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Record a failed authentication attempt
117
+ * Increments the rate limit counter
118
+ */
119
+ async function recordFailedAttempt(identifier: string): Promise<void> {
120
+ try {
121
+ await incrementLimit(identifier, {
122
+ maxRequests: 100,
123
+ windowMs: 15 * 60 * 1000, // 15 minutes
124
+ strategy: 'sliding-window',
125
+ });
126
+ } catch (error) {
127
+ // Non-critical - log and continue
128
+ console.warn('[bearerTokenAuth] Failed to record attempt:', error);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Bearer Token Authentication Middleware
134
+ *
135
+ * Validates API keys sent as Bearer tokens in the Authorization header.
136
+ * Attaches authenticated key info to the request for downstream handlers.
137
+ *
138
+ * @param options Configuration options
139
+ * @returns Express middleware function
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { bearerTokenAuth } from '@qwickapps/server';
144
+ *
145
+ * // Require authentication
146
+ * app.get('/api/data', bearerTokenAuth(), (req, res) => {
147
+ * const { apiKey } = req as ApiKeyAuthenticatedRequest;
148
+ * res.json({ user_id: apiKey?.user_id });
149
+ * });
150
+ *
151
+ * // Require specific scopes
152
+ * app.post('/api/data', bearerTokenAuth({
153
+ * requiredScopes: ['write'],
154
+ * }), (req, res) => {
155
+ * // Handler code
156
+ * });
157
+ *
158
+ * // Allow only M2M keys
159
+ * app.post('/api/admin', bearerTokenAuth({
160
+ * allowedKeyTypes: ['m2m'],
161
+ * requiredScopes: ['admin'],
162
+ * }), (req, res) => {
163
+ * // Handler code
164
+ * });
165
+ * ```
166
+ */
167
+ export function bearerTokenAuth(options: BearerTokenAuthOptions = {}) {
168
+ const {
169
+ requiredScopes = [],
170
+ allowedKeyTypes,
171
+ onUnauthorized = defaultUnauthorizedHandler,
172
+ } = options;
173
+
174
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
175
+ try {
176
+ // Check rate limit first
177
+ const rateLimitId = getRateLimitIdentifier(req);
178
+ if (await checkRateLimit(rateLimitId)) {
179
+ res.status(429).json({
180
+ error: 'Too Many Requests',
181
+ message: 'Too many authentication attempts. Please try again later.',
182
+ });
183
+ return;
184
+ }
185
+
186
+ // Extract token from Authorization header
187
+ const token = extractBearerToken(req);
188
+
189
+ if (!token) {
190
+ recordFailedAttempt(rateLimitId);
191
+ onUnauthorized(req, res, 'Missing or invalid Authorization header');
192
+ return;
193
+ }
194
+
195
+ // Get store instance
196
+ const store = getApiKeysStore();
197
+ if (!store) {
198
+ console.error('[bearerTokenAuth] API Keys plugin not initialized');
199
+ res.status(500).json({ error: 'Authentication service unavailable' });
200
+ return;
201
+ }
202
+
203
+ // Verify the token
204
+ const apiKey = await store.verify(token);
205
+
206
+ if (!apiKey) {
207
+ recordFailedAttempt(rateLimitId);
208
+ onUnauthorized(req, res, 'Invalid, expired, or inactive API key');
209
+ return;
210
+ }
211
+
212
+ // Check key type if restrictions apply
213
+ if (allowedKeyTypes && !allowedKeyTypes.includes(apiKey.key_type)) {
214
+ onUnauthorized(req, res, `This endpoint requires ${allowedKeyTypes.join(' or ')} keys`);
215
+ return;
216
+ }
217
+
218
+ // Check scopes if required
219
+ if (requiredScopes.length > 0 && !hasRequiredScopes(apiKey.scopes, requiredScopes)) {
220
+ onUnauthorized(req, res, `Insufficient scopes. Required: ${requiredScopes.join(', ')}`);
221
+ return;
222
+ }
223
+
224
+ // Record usage (non-blocking)
225
+ store.recordUsage(apiKey.id).catch(err => {
226
+ console.error('[bearerTokenAuth] Failed to record key usage:', err);
227
+ });
228
+
229
+ // Attach authenticated key info to request
230
+ (req as ApiKeyAuthenticatedRequest).apiKey = {
231
+ id: apiKey.id,
232
+ user_id: apiKey.user_id,
233
+ scopes: apiKey.scopes,
234
+ key_type: apiKey.key_type,
235
+ };
236
+
237
+ next();
238
+ } catch (error) {
239
+ console.error('[bearerTokenAuth] Authentication error:', error);
240
+ res.status(500).json({ error: 'Authentication failed' });
241
+ }
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Helper type guard for API key authenticated requests
247
+ */
248
+ export function isApiKeyAuthenticated(req: Request): req is ApiKeyAuthenticatedRequest {
249
+ return 'apiKey' in req && (req as ApiKeyAuthenticatedRequest).apiKey !== undefined;
250
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * API Keys Middleware Index
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ export {
8
+ bearerTokenAuth,
9
+ isApiKeyAuthenticated,
10
+ type ApiKeyAuthenticatedRequest,
11
+ type BearerTokenAuthOptions,
12
+ } from './bearer-token-auth.js';