@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,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';
|