@qwickapps/server 1.5.2 → 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.
- package/CHANGELOG.md +22 -0
- package/dist/core/control-panel.js +8 -8
- package/dist/core/control-panel.js.map +1 -1
- package/dist/plugins/api-keys/api-keys-plugin.d.ts +46 -0
- package/dist/plugins/api-keys/api-keys-plugin.d.ts.map +1 -0
- package/dist/plugins/api-keys/api-keys-plugin.js +329 -0
- package/dist/plugins/api-keys/api-keys-plugin.js.map +1 -0
- package/dist/plugins/api-keys/index.d.ts +14 -0
- package/dist/plugins/api-keys/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/index.js +17 -0
- package/dist/plugins/api-keys/index.js.map +1 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts +74 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts.map +1 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.js +201 -0
- package/dist/plugins/api-keys/middleware/bearer-token-auth.js.map +1 -0
- package/dist/plugins/api-keys/middleware/index.d.ts +7 -0
- package/dist/plugins/api-keys/middleware/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/middleware/index.js +7 -0
- package/dist/plugins/api-keys/middleware/index.js.map +1 -0
- package/dist/plugins/api-keys/stores/index.d.ts +7 -0
- package/dist/plugins/api-keys/stores/index.d.ts.map +1 -0
- package/dist/plugins/api-keys/stores/index.js +7 -0
- package/dist/plugins/api-keys/stores/index.js.map +1 -0
- package/dist/plugins/api-keys/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/api-keys/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/api-keys/stores/postgres-store.js +360 -0
- package/dist/plugins/api-keys/stores/postgres-store.js.map +1 -0
- package/dist/plugins/api-keys/types.d.ts +268 -0
- package/dist/plugins/api-keys/types.d.ts.map +1 -0
- package/dist/plugins/api-keys/types.js +56 -0
- package/dist/plugins/api-keys/types.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +1 -1
- package/dist/plugins/auth/auth-plugin.js.map +1 -1
- package/dist/plugins/auth/env-config.js +2 -2
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +5 -2
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/users/__tests__/postgres-store.test.js +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -1
- package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
- package/dist/plugins/users/stores/postgres-store.js +59 -1
- package/dist/plugins/users/stores/postgres-store.js.map +1 -1
- package/dist/plugins/users/types.d.ts +22 -0
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist-ui/assets/index-5nX8fM1a.js +469 -0
- package/dist-ui/assets/index-5nX8fM1a.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +62 -0
- package/dist-ui-lib/components/index.d.ts +2 -1
- package/dist-ui-lib/index.js +2588 -2238
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/APIKeysPage.d.ts +13 -0
- package/dist-ui-lib/pages/AcceptInvitationPage.d.ts +28 -0
- package/package.json +3 -2
- package/src/core/control-panel.ts +8 -8
- package/src/plugins/api-keys/api-keys-plugin.ts +397 -0
- package/src/plugins/api-keys/index.ts +49 -0
- package/src/plugins/api-keys/middleware/bearer-token-auth.ts +250 -0
- package/src/plugins/api-keys/middleware/index.ts +12 -0
- package/src/plugins/api-keys/stores/index.ts +7 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +487 -0
- package/src/plugins/api-keys/types.ts +243 -0
- package/src/plugins/auth/auth-plugin.ts +1 -1
- package/src/plugins/auth/env-config.ts +2 -2
- package/src/plugins/frontend-app-plugin.ts +7 -2
- package/src/plugins/users/__tests__/postgres-store.test.ts +1 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
- package/src/plugins/users/stores/postgres-store.ts +69 -0
- package/src/plugins/users/types.ts +25 -0
- package/ui/src/App.tsx +6 -1
- package/ui/src/api/controlPanelApi.ts +157 -0
- package/ui/src/components/index.ts +6 -0
- package/ui/src/pages/APIKeysPage.tsx +661 -0
- package/ui/src/pages/AcceptInvitationPage.tsx +169 -0
- package/ui/src/pages/UsersPage.tsx +225 -2
- package/dist-ui/assets/index-BfC7mG5L.js +0 -469
- package/dist-ui/assets/index-BfC7mG5L.js.map +0 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APIKeysPage Component
|
|
3
|
+
*
|
|
4
|
+
* API key management page for authentication and authorization.
|
|
5
|
+
* Allows users to create, view, and manage API keys with scopes.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
export interface APIKeysPageProps {
|
|
10
|
+
title?: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function APIKeysPage({ title, subtitle, }: APIKeysPageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AcceptInvitationPage Component
|
|
3
|
+
*
|
|
4
|
+
* Standalone page for users to accept invitations and activate their accounts.
|
|
5
|
+
* Can be used in control panel or frontend applications.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
import { type User } from '../api/controlPanelApi';
|
|
10
|
+
export interface AcceptInvitationPageProps {
|
|
11
|
+
/** Invitation token (if not provided, will extract from URL) */
|
|
12
|
+
token?: string;
|
|
13
|
+
/** Title text */
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Subtitle text */
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
/** Success message */
|
|
18
|
+
successMessage?: string;
|
|
19
|
+
/** URL to redirect to after successful activation (optional) */
|
|
20
|
+
redirectUrl?: string;
|
|
21
|
+
/** Label for the redirect button */
|
|
22
|
+
redirectLabel?: string;
|
|
23
|
+
/** Callback when invitation is accepted successfully */
|
|
24
|
+
onSuccess?: (user: User) => void;
|
|
25
|
+
/** Callback when invitation acceptance fails */
|
|
26
|
+
onError?: (error: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function AcceptInvitationPage({ token: tokenProp, title, subtitle, successMessage, redirectUrl, redirectLabel, onSuccess, onError, }: AcceptInvitationPageProps): import("react/jsx-runtime").JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qwickapps/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"cors": "^2.8.5",
|
|
55
55
|
"express": "^4.18.2",
|
|
56
56
|
"helmet": "^7.1.0",
|
|
57
|
-
"http-proxy-middleware": "^3.0.3"
|
|
57
|
+
"http-proxy-middleware": "^3.0.3",
|
|
58
|
+
"zod": "^3.22.4"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@emotion/react": "^11.14.0",
|
|
@@ -392,7 +392,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
392
392
|
return b.path.length - a.path.length;
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
-
// Register routes with Express
|
|
395
|
+
// Register routes with Express router (mounted at apiBasePath)
|
|
396
396
|
logger.debug(`Registering ${routes.length} routes in priority order:`);
|
|
397
397
|
for (const route of routes) {
|
|
398
398
|
// TODO: Add auth middleware if route.auth?.required
|
|
@@ -403,26 +403,26 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
403
403
|
|
|
404
404
|
switch (route.method) {
|
|
405
405
|
case 'get':
|
|
406
|
-
|
|
406
|
+
router.get(route.path, ...handlers);
|
|
407
407
|
break;
|
|
408
408
|
case 'post':
|
|
409
|
-
|
|
409
|
+
router.post(route.path, ...handlers);
|
|
410
410
|
break;
|
|
411
411
|
case 'put':
|
|
412
|
-
|
|
412
|
+
router.put(route.path, ...handlers);
|
|
413
413
|
break;
|
|
414
414
|
case 'delete':
|
|
415
|
-
|
|
415
|
+
router.delete(route.path, ...handlers);
|
|
416
416
|
break;
|
|
417
417
|
case 'patch':
|
|
418
|
-
|
|
418
|
+
router.patch(route.path, ...handlers);
|
|
419
419
|
break;
|
|
420
420
|
case 'use':
|
|
421
|
-
|
|
421
|
+
router.use(route.path, ...handlers);
|
|
422
422
|
break;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
logger.debug(` ${route.method.toUpperCase()} ${route.path}`);
|
|
425
|
+
logger.debug(` ${route.method.toUpperCase()} ${apiBasePath}${route.path}`);
|
|
426
426
|
}
|
|
427
427
|
|
|
428
428
|
return new Promise((resolve) => {
|
|
@@ -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';
|