@zintrust/core 0.1.34 → 0.1.36
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/package.json
CHANGED
package/src/boot/bootstrap.js
CHANGED
|
@@ -103,9 +103,15 @@ const gracefulShutdown = async (signal) => {
|
|
|
103
103
|
// Shutdown worker management system FIRST (before database closes)
|
|
104
104
|
if (appConfig.detectRuntime() === 'nodejs' || appConfig.detectRuntime() === 'lambda') {
|
|
105
105
|
try {
|
|
106
|
-
const {
|
|
106
|
+
const { createRequire } = await import('../node-singletons/module.js');
|
|
107
|
+
const require = createRequire(import.meta.url);
|
|
108
|
+
const workers = require('@zintrust/workers');
|
|
107
109
|
const workerBudgetMs = Math.min(15000, remainingMs());
|
|
108
|
-
await withTimeout(WorkerShutdown.shutdown({
|
|
110
|
+
await withTimeout(workers.WorkerShutdown.shutdown({
|
|
111
|
+
signal,
|
|
112
|
+
timeout: workerBudgetMs,
|
|
113
|
+
forceExit: false,
|
|
114
|
+
}), workerBudgetMs, 'Worker shutdown timed out');
|
|
109
115
|
}
|
|
110
116
|
catch (error) {
|
|
111
117
|
Logger.warn('Worker shutdown failed (continuing with app shutdown)', error);
|
|
@@ -136,9 +142,11 @@ async function useWorkerStarter() {
|
|
|
136
142
|
// Initialize worker management system
|
|
137
143
|
let workerInit = null;
|
|
138
144
|
try {
|
|
139
|
-
const {
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
const { createRequire } = await import('../node-singletons/module.js');
|
|
146
|
+
const require = createRequire(import.meta.url);
|
|
147
|
+
const workers = require('@zintrust/workers');
|
|
148
|
+
workerInit = workers.WorkerInit;
|
|
149
|
+
await workers.WorkerInit.initialize({
|
|
142
150
|
enableResourceMonitoring: true,
|
|
143
151
|
enableHealthMonitoring: true,
|
|
144
152
|
enableAutoScaling: false, // Disabled by default, enable via config
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zintrust/core v0.1.
|
|
2
|
+
* @zintrust/core v0.1.36
|
|
3
3
|
*
|
|
4
4
|
* ZinTrust Framework - Production-Grade TypeScript Backend
|
|
5
5
|
* Built for performance, type safety, and exceptional developer experience
|
|
6
6
|
*
|
|
7
7
|
* Build Information:
|
|
8
|
-
* Built: 2026-01-29T10:
|
|
8
|
+
* Built: 2026-01-29T10:45:55.711Z
|
|
9
9
|
* Node: >=20.0.0
|
|
10
10
|
* License: MIT
|
|
11
11
|
*
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Available at runtime for debugging and health checks
|
|
22
22
|
*/
|
|
23
23
|
export const ZINTRUST_VERSION = '0.1.23';
|
|
24
|
-
export const ZINTRUST_BUILD_DATE = '2026-01-29T10:
|
|
24
|
+
export const ZINTRUST_BUILD_DATE = '2026-01-29T10:45:55.674Z'; // Replaced during build
|
|
25
25
|
import { Application } from './boot/Application.js';
|
|
26
26
|
import { AwsSigV4 } from './common/index.js';
|
|
27
27
|
import { SignedRequest } from './security/SignedRequest.js';
|
package/src/orm/QueryBuilder.js
CHANGED
|
@@ -562,7 +562,7 @@ function attachReadExecutionMethods(builder, state, db) {
|
|
|
562
562
|
}
|
|
563
563
|
const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
|
|
564
564
|
const isKeyValue = (value) => typeof value === 'string' || typeof value === 'number';
|
|
565
|
-
const getModelIds = (models, key) => models.map((model) => model.getAttribute(key)).filter(isKeyValue);
|
|
565
|
+
const getModelIds = (models, key) => models.map((model) => model.getAttribute(key)).filter((element) => isKeyValue(element));
|
|
566
566
|
const applyConstraint = (query, constraint) => {
|
|
567
567
|
if (typeof constraint === 'function') {
|
|
568
568
|
return constraint(query) ?? query;
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User QueryBuilder Controller
|
|
3
|
+
* QueryBuilder-backed controller for the users resource.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IUserController, JsonRecord, ValidationErrorLike } from '@app/Types/controller';
|
|
7
|
+
import type { IRequest, IResponse, SanitizerError } from '@zintrust/core';
|
|
8
|
+
import {
|
|
9
|
+
Logger,
|
|
10
|
+
QueryBuilder,
|
|
11
|
+
Sanitizer,
|
|
12
|
+
Schema,
|
|
13
|
+
Validator,
|
|
14
|
+
getValidatedBody,
|
|
15
|
+
nowIso,
|
|
16
|
+
randomBytes,
|
|
17
|
+
useDatabase,
|
|
18
|
+
} from '@zintrust/core';
|
|
19
|
+
|
|
20
|
+
const isValidationError = (error: unknown): error is ValidationErrorLike => {
|
|
21
|
+
if (typeof error !== 'object' || error === null) return false;
|
|
22
|
+
const maybe = error as ValidationErrorLike;
|
|
23
|
+
return maybe.name === 'ValidationError' && typeof maybe.toObject === 'function';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const isSanitizerError = (error: unknown): error is SanitizerError => {
|
|
27
|
+
if (typeof error !== 'object' || error === null) return false;
|
|
28
|
+
const maybe = error as SanitizerError;
|
|
29
|
+
return maybe.name === 'SanitizerError';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const toJsonRecord = (value: unknown): JsonRecord => {
|
|
33
|
+
if (typeof value !== 'object' || value === null) return {};
|
|
34
|
+
if (Array.isArray(value)) return {};
|
|
35
|
+
return value as JsonRecord;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveBody = (req: IRequest): JsonRecord => {
|
|
39
|
+
return toJsonRecord(getValidatedBody(req) ?? req.body ?? {});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getParamCompat = (req: IRequest, name: string): unknown => {
|
|
43
|
+
try {
|
|
44
|
+
const anyReq = req as unknown as { getParam?: (key: string) => unknown };
|
|
45
|
+
if (typeof anyReq.getParam === 'function') return anyReq.getParam(name);
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const anyReq = req as unknown as { params?: Record<string, unknown> };
|
|
51
|
+
const params = anyReq.params;
|
|
52
|
+
if (typeof params === 'object' && params !== null) return params[name];
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const requireSelf = (
|
|
57
|
+
req: IRequest,
|
|
58
|
+
res: IResponse,
|
|
59
|
+
userId: string | undefined
|
|
60
|
+
): userId is string => {
|
|
61
|
+
if (typeof userId !== 'string' || userId.length === 0) {
|
|
62
|
+
res.status(400).json({ error: 'Missing user id' });
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const subject = typeof req.user?.sub === 'string' ? req.user.sub : undefined;
|
|
67
|
+
if (subject === undefined || subject.length === 0) {
|
|
68
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (subject !== userId) {
|
|
72
|
+
res.status(403).json({ error: 'Forbidden' });
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const randomInt = (min: number, max: number): number => {
|
|
79
|
+
const lo = Math.ceil(min);
|
|
80
|
+
const hi = Math.floor(max);
|
|
81
|
+
return Math.floor(lo + Math.random() * (hi - lo + 1)); // NOSONAR is just a test utility
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const randomName = (): string => {
|
|
85
|
+
const first = ['Alex', 'Jordan', 'Taylor', 'Sam', 'Casey', 'Riley', 'Morgan'];
|
|
86
|
+
const last = ['Lee', 'Kim', 'Patel', 'Garcia', 'Brown', 'Nguyen', 'Smith'];
|
|
87
|
+
return `${first[randomInt(0, first.length - 1)]} ${last[randomInt(0, last.length - 1)]}`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const randomEmail = (): string => {
|
|
91
|
+
const n = randomInt(10000, 99999);
|
|
92
|
+
return `user${n}@example.com`;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const randomPassword = (): string => {
|
|
96
|
+
// Not cryptographically perfect UX-wise, but avoids hard-coded credentials.
|
|
97
|
+
// `base64url` keeps it URL-safe and reasonably short.
|
|
98
|
+
return randomBytes(12).toString('base64url');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const pickAllowed = (body: JsonRecord, allowed: Set<string>): JsonRecord => {
|
|
102
|
+
const out: JsonRecord = {};
|
|
103
|
+
for (const [k, v] of Object.entries(body)) {
|
|
104
|
+
if (allowed.has(k)) out[k] = v;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const hasUnknownKeys = (body: JsonRecord, allowed: Set<string>): string | null => {
|
|
110
|
+
for (const k of Object.keys(body)) {
|
|
111
|
+
if (!allowed.has(k)) return k;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const sanitizeUserUpdateBody = (updateBody: JsonRecord): JsonRecord => {
|
|
117
|
+
const sanitizedUpdateBody: JsonRecord = {};
|
|
118
|
+
if ('name' in updateBody) {
|
|
119
|
+
sanitizedUpdateBody['name'] = Sanitizer.nameText(updateBody['name']).trim();
|
|
120
|
+
}
|
|
121
|
+
if ('email' in updateBody) {
|
|
122
|
+
sanitizedUpdateBody['email'] = Sanitizer.email(updateBody['email']).trim().toLowerCase();
|
|
123
|
+
}
|
|
124
|
+
if ('password' in updateBody) {
|
|
125
|
+
sanitizedUpdateBody['password'] = Sanitizer.safePasswordChars(updateBody['password']);
|
|
126
|
+
}
|
|
127
|
+
return sanitizedUpdateBody;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const buildUserUpdateSchema = (): ReturnType<typeof Schema.create> => {
|
|
131
|
+
return Schema.create()
|
|
132
|
+
.custom(
|
|
133
|
+
'name',
|
|
134
|
+
(v: unknown) => v === undefined || typeof v === 'string',
|
|
135
|
+
'name must be a string'
|
|
136
|
+
)
|
|
137
|
+
.minLength('name', 1)
|
|
138
|
+
.custom(
|
|
139
|
+
'email',
|
|
140
|
+
(v: unknown) => v === undefined || typeof v === 'string',
|
|
141
|
+
'email must be a string'
|
|
142
|
+
)
|
|
143
|
+
.custom(
|
|
144
|
+
'password',
|
|
145
|
+
(v: unknown) => v === undefined || typeof v === 'string',
|
|
146
|
+
'password must be a string'
|
|
147
|
+
)
|
|
148
|
+
.minLength('password', 8);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const buildUserStoreSchema = (): ReturnType<typeof Schema.create> => {
|
|
152
|
+
return Schema.create()
|
|
153
|
+
.custom('name', (v: unknown) => typeof v === 'string', 'name must be a string')
|
|
154
|
+
.minLength('name', 1)
|
|
155
|
+
.custom('email', (v: unknown) => typeof v === 'string', 'email must be a string')
|
|
156
|
+
.email('email')
|
|
157
|
+
.custom('password', (v: unknown) => typeof v === 'string', 'password must be a string')
|
|
158
|
+
.minLength('password', 8);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* User Controller Methods
|
|
163
|
+
*/
|
|
164
|
+
const userControllerMethods: IUserController = {
|
|
165
|
+
/**
|
|
166
|
+
* List all users
|
|
167
|
+
* GET /users
|
|
168
|
+
*/
|
|
169
|
+
async index(req: IRequest, res: IResponse): Promise<void> {
|
|
170
|
+
try {
|
|
171
|
+
const subject = typeof req.user?.sub === 'string' ? req.user.sub : undefined;
|
|
172
|
+
if (subject === undefined || subject.length === 0) {
|
|
173
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const db = useDatabase();
|
|
178
|
+
const users = await QueryBuilder.create('users', db)
|
|
179
|
+
.select('id', 'name', 'email', 'created_at', 'updated_at')
|
|
180
|
+
.where('id', '=', subject)
|
|
181
|
+
.limit(1)
|
|
182
|
+
.get();
|
|
183
|
+
|
|
184
|
+
res.json({ data: users });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
Logger.error('Error fetching users:', error);
|
|
187
|
+
res.status(500).json({ error: 'Failed to fetch users' });
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Show a specific user
|
|
193
|
+
* GET /users/:id
|
|
194
|
+
*/
|
|
195
|
+
async show(req: IRequest, res: IResponse): Promise<void> {
|
|
196
|
+
try {
|
|
197
|
+
const db = useDatabase();
|
|
198
|
+
|
|
199
|
+
const rawId = getParamCompat(req, 'id');
|
|
200
|
+
const id = Sanitizer.digitsOnly(rawId); // Zero trust protection for db id
|
|
201
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
202
|
+
// ✅ Good
|
|
203
|
+
res.status(400).json({ error: 'Missing user id' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!requireSelf(req, res, id)) return;
|
|
207
|
+
|
|
208
|
+
const user = await QueryBuilder.create('users', db)
|
|
209
|
+
.select('id', 'name', 'email', 'created_at', 'updated_at')
|
|
210
|
+
.where('id', '=', id)
|
|
211
|
+
.limit(1)
|
|
212
|
+
.first();
|
|
213
|
+
|
|
214
|
+
if (user === null) {
|
|
215
|
+
res.status(404).json({ error: 'User not found' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
res.json({ data: user });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (isSanitizerError(error)) {
|
|
221
|
+
res.status(400).json({ error: error.message });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
Logger.error('Error fetching user:', error);
|
|
225
|
+
res.status(500).json({ error: 'Failed to fetch user' });
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Show create form
|
|
231
|
+
* GET /users/create
|
|
232
|
+
*/
|
|
233
|
+
async create(_req: IRequest, res: IResponse): Promise<void> {
|
|
234
|
+
res.json({ form: 'Create User Form' });
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Store a new user
|
|
239
|
+
* POST /users
|
|
240
|
+
*/
|
|
241
|
+
async store(req: IRequest, res: IResponse): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
// Use validated body if available (already sanitized by middleware), otherwise fallback to raw
|
|
244
|
+
const body = resolveBody(req);
|
|
245
|
+
|
|
246
|
+
const required = ['name', 'email', 'password'] as const;
|
|
247
|
+
const missing: Record<string, string[]> = {};
|
|
248
|
+
for (const key of required) {
|
|
249
|
+
const val = body[key];
|
|
250
|
+
if (typeof val !== 'string' || val.trim() === '') {
|
|
251
|
+
missing[key] = ['Required'];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (Object.keys(missing).length > 0) {
|
|
255
|
+
res.status(422).json({ errors: missing });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Trust middleware for sanitization if validation passed.
|
|
260
|
+
// If we are here, validation ostensibly passed or we are in a context where we must self-validate.
|
|
261
|
+
// To satisfy defense-in-depth without double-sanitization bottleneck:
|
|
262
|
+
// We assume body is safe-ish if it came from resolved validated body.
|
|
263
|
+
// But to be explicit and type-safe, we cast or read fields directly.
|
|
264
|
+
|
|
265
|
+
const db = useDatabase();
|
|
266
|
+
const ts = nowIso();
|
|
267
|
+
|
|
268
|
+
// Apply bulletproof sanitization for defense-in-depth
|
|
269
|
+
const name = Sanitizer.nameText(body['name']);
|
|
270
|
+
const email = Sanitizer.email(body['email']);
|
|
271
|
+
const password = Sanitizer.safePasswordChars(body['password']);
|
|
272
|
+
|
|
273
|
+
Validator.validate({ name, email, password }, buildUserStoreSchema());
|
|
274
|
+
|
|
275
|
+
await QueryBuilder.create('users', db).insert({
|
|
276
|
+
name,
|
|
277
|
+
email,
|
|
278
|
+
password, // Hashing should be handled by model/service or here if raw
|
|
279
|
+
created_at: ts,
|
|
280
|
+
updated_at: ts,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
res.status(201).json({ message: 'User created' });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (isSanitizerError(error)) {
|
|
286
|
+
res.status(400).json({ error: error.message });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (isValidationError(error)) {
|
|
290
|
+
res.status(422).json({ errors: error.toObject?.() ?? {} });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
Logger.error('Error creating user:', error);
|
|
294
|
+
res.status(500).json({ error: 'Failed to create user' });
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Fill users table with random users
|
|
300
|
+
* POST /users/fill
|
|
301
|
+
*/
|
|
302
|
+
async fill(req: IRequest, res: IResponse): Promise<void> {
|
|
303
|
+
try {
|
|
304
|
+
const body = resolveBody(req);
|
|
305
|
+
const countVal = body['count'];
|
|
306
|
+
|
|
307
|
+
// Ensure count is a number (middleware validation handles this, but we double check or default)
|
|
308
|
+
let count = typeof countVal === 'number' ? countVal : 10;
|
|
309
|
+
if (count < 1) count = 1;
|
|
310
|
+
if (count > 100) count = 100;
|
|
311
|
+
|
|
312
|
+
const db = useDatabase();
|
|
313
|
+
const ts = nowIso();
|
|
314
|
+
|
|
315
|
+
// Optimize: Bulk insert instead of N+1 inserts to reduce IO bottleneck and memory overhead
|
|
316
|
+
const users = Array.from({ length: count }, () => ({
|
|
317
|
+
name: randomName(),
|
|
318
|
+
email: randomEmail(),
|
|
319
|
+
password: randomPassword(),
|
|
320
|
+
created_at: ts,
|
|
321
|
+
updated_at: ts,
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
await QueryBuilder.create('users', db).insert(users);
|
|
325
|
+
|
|
326
|
+
res.status(201).json({ message: 'Users filled', count });
|
|
327
|
+
} catch (error) {
|
|
328
|
+
Logger.error('Error filling users:', error);
|
|
329
|
+
res.status(500).json({ error: 'Failed to fill users' });
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Show edit form
|
|
335
|
+
* GET /users/:id/edit
|
|
336
|
+
*/
|
|
337
|
+
async edit(_req: IRequest, res: IResponse): Promise<void> {
|
|
338
|
+
try {
|
|
339
|
+
res.json({ form: 'Edit User Form' });
|
|
340
|
+
} catch (error) {
|
|
341
|
+
Logger.error('Error loading edit form:', error);
|
|
342
|
+
res.status(500).json({ error: 'Failed to load edit form' });
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Update a user
|
|
348
|
+
* PUT /users/:id
|
|
349
|
+
*/
|
|
350
|
+
async update(req: IRequest, res: IResponse): Promise<void> {
|
|
351
|
+
// NOSONAR bulletproof sanitization requires explicit validation steps
|
|
352
|
+
try {
|
|
353
|
+
const db = useDatabase();
|
|
354
|
+
const rawId = getParamCompat(req, 'id');
|
|
355
|
+
const id = Sanitizer.digitsOnly(rawId);
|
|
356
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
357
|
+
res.status(400).json({ error: 'Missing user id' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!requireSelf(req, res, id)) return;
|
|
361
|
+
|
|
362
|
+
const allowed = new Set(['name', 'email', 'password']);
|
|
363
|
+
const body = resolveBody(req);
|
|
364
|
+
const unknown = hasUnknownKeys(body, allowed);
|
|
365
|
+
if (unknown !== null) {
|
|
366
|
+
res.status(422).json({ errors: { [unknown]: ['Unknown field'] } });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const updateBody = pickAllowed(body, allowed);
|
|
371
|
+
if (Object.keys(updateBody).length === 0) {
|
|
372
|
+
res.status(422).json({ errors: { body: ['No fields to update'] } });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const sanitizedUpdateBody = sanitizeUserUpdateBody(updateBody);
|
|
377
|
+
Validator.validate(sanitizedUpdateBody, buildUserUpdateSchema());
|
|
378
|
+
|
|
379
|
+
const existing = await QueryBuilder.create('users', db)
|
|
380
|
+
.select('id')
|
|
381
|
+
.where('id', '=', id)
|
|
382
|
+
.limit(1)
|
|
383
|
+
.first();
|
|
384
|
+
|
|
385
|
+
if (existing === null) {
|
|
386
|
+
res.status(404).json({ error: 'User not found' });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const ts = nowIso();
|
|
391
|
+
await QueryBuilder.create('users', db)
|
|
392
|
+
.where('id', '=', id)
|
|
393
|
+
.update({ ...sanitizedUpdateBody, updated_at: ts });
|
|
394
|
+
|
|
395
|
+
const user = await QueryBuilder.create('users', db)
|
|
396
|
+
.select('id', 'name', 'email', 'created_at', 'updated_at')
|
|
397
|
+
.where('id', '=', id)
|
|
398
|
+
.limit(1)
|
|
399
|
+
.first();
|
|
400
|
+
|
|
401
|
+
res.json({ message: 'User updated', user });
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (isSanitizerError(error)) {
|
|
404
|
+
res.status(400).json({ error: error.message });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (isValidationError(error)) {
|
|
408
|
+
res.status(422).json({ errors: error.toObject?.() ?? {} });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
Logger.error('Error updating user:', error);
|
|
412
|
+
res.status(500).json({ error: 'Failed to update user' });
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Delete a user
|
|
418
|
+
* DELETE /users/:id
|
|
419
|
+
*/
|
|
420
|
+
async destroy(req: IRequest, res: IResponse): Promise<void> {
|
|
421
|
+
try {
|
|
422
|
+
const db = useDatabase();
|
|
423
|
+
const rawId = getParamCompat(req, 'id');
|
|
424
|
+
const id = Sanitizer.digitsOnly(rawId);
|
|
425
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
426
|
+
res.status(400).json({ error: 'Missing user id' });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!requireSelf(req, res, id)) return;
|
|
431
|
+
|
|
432
|
+
const existing = await QueryBuilder.create('users', db)
|
|
433
|
+
.select('id')
|
|
434
|
+
.where('id', '=', id)
|
|
435
|
+
.limit(1)
|
|
436
|
+
.first();
|
|
437
|
+
|
|
438
|
+
if (existing === null) {
|
|
439
|
+
res.status(404).json({ error: 'User not found' });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await QueryBuilder.create('users', db).where('id', '=', id).delete();
|
|
444
|
+
res.json({ message: 'User deleted' });
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (isSanitizerError(error)) {
|
|
447
|
+
res.status(400).json({ error: error.message });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
Logger.error('Error deleting user:', error);
|
|
451
|
+
res.status(500).json({ error: 'Failed to delete user' });
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* User QueryBuilder Controller Factory
|
|
458
|
+
*/
|
|
459
|
+
export const UserQueryBuilderController = {
|
|
460
|
+
/**
|
|
461
|
+
* Create a new user controller instance
|
|
462
|
+
*/
|
|
463
|
+
create(): IUserController {
|
|
464
|
+
return userControllerMethods;
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export default UserQueryBuilderController;
|