@organizasyon/meeting-nanaman-app-backend 1.0.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 (101) hide show
  1. package/dist/controllers/auth/index.d.ts +65 -0
  2. package/dist/controllers/auth/index.js +525 -0
  3. package/dist/controllers/employees/index.d.ts +38 -0
  4. package/dist/controllers/employees/index.js +185 -0
  5. package/dist/controllers/health/index.d.ts +9 -0
  6. package/dist/controllers/health/index.js +42 -0
  7. package/dist/controllers/index.d.ts +16 -0
  8. package/dist/controllers/index.js +19 -0
  9. package/dist/controllers/meetings/index.d.ts +23 -0
  10. package/dist/controllers/meetings/index.js +233 -0
  11. package/dist/controllers/modules/index.d.ts +5 -0
  12. package/dist/controllers/modules/index.js +104 -0
  13. package/dist/controllers/users/index.d.ts +103 -0
  14. package/dist/controllers/users/index.js +841 -0
  15. package/dist/data/modules.json +94 -0
  16. package/dist/database/config/index.d.ts +2 -0
  17. package/dist/database/config/index.js +32 -0
  18. package/dist/database/index.d.ts +9 -0
  19. package/dist/database/index.js +9 -0
  20. package/dist/database/seeder/employees/index.d.ts +1 -0
  21. package/dist/database/seeder/employees/index.js +40 -0
  22. package/dist/database/seeder/index.d.ts +4 -0
  23. package/dist/database/seeder/index.js +20 -0
  24. package/dist/database/seeder/users/index.d.ts +1 -0
  25. package/dist/database/seeder/users/index.js +46 -0
  26. package/dist/index.d.ts +9 -0
  27. package/dist/index.js +23 -0
  28. package/dist/jobs/index.d.ts +1 -0
  29. package/dist/jobs/index.js +18 -0
  30. package/dist/jobs/mailer/index.d.ts +4 -0
  31. package/dist/jobs/mailer/index.js +186 -0
  32. package/dist/jobs/mailer/templates/auth.d.ts +11 -0
  33. package/dist/jobs/mailer/templates/auth.js +117 -0
  34. package/dist/jobs/mailer/templates/index.d.ts +1 -0
  35. package/dist/jobs/mailer/templates/index.js +17 -0
  36. package/dist/jobs/queues/index.d.ts +3 -0
  37. package/dist/jobs/queues/index.js +115 -0
  38. package/dist/middlewares/audit/index.d.ts +0 -0
  39. package/dist/middlewares/audit/index.js +1 -0
  40. package/dist/middlewares/guard/index.d.ts +11 -0
  41. package/dist/middlewares/guard/index.js +53 -0
  42. package/dist/middlewares/index.d.ts +2 -0
  43. package/dist/middlewares/index.js +7 -0
  44. package/dist/middlewares/meeting.d.ts +9 -0
  45. package/dist/middlewares/meeting.js +34 -0
  46. package/dist/models/employees/index.d.ts +83 -0
  47. package/dist/models/employees/index.js +70 -0
  48. package/dist/models/index.d.ts +570 -0
  49. package/dist/models/index.js +17 -0
  50. package/dist/models/meetings/index.d.ts +227 -0
  51. package/dist/models/meetings/index.js +112 -0
  52. package/dist/models/passkeys/index.d.ts +77 -0
  53. package/dist/models/passkeys/index.js +55 -0
  54. package/dist/models/queues/index.d.ts +77 -0
  55. package/dist/models/queues/index.js +57 -0
  56. package/dist/models/users/index.d.ts +107 -0
  57. package/dist/models/users/index.js +92 -0
  58. package/dist/queues/index.d.ts +1 -0
  59. package/dist/queues/index.js +17 -0
  60. package/dist/queues/mailer/index.d.ts +4 -0
  61. package/dist/queues/mailer/index.js +74 -0
  62. package/dist/types/index.d.ts +33 -0
  63. package/dist/types/index.js +2 -0
  64. package/dist/utils/notifications.d.ts +2 -0
  65. package/dist/utils/notifications.js +51 -0
  66. package/package.json +39 -0
  67. package/public/health.html +215 -0
  68. package/src/controllers/auth/index.ts +609 -0
  69. package/src/controllers/employees/index.ts +210 -0
  70. package/src/controllers/health/index.ts +41 -0
  71. package/src/controllers/index.ts +9 -0
  72. package/src/controllers/meetings/index.ts +251 -0
  73. package/src/controllers/modules/index.ts +74 -0
  74. package/src/controllers/users/index.ts +981 -0
  75. package/src/data/modules.json +94 -0
  76. package/src/database/config/index.ts +26 -0
  77. package/src/database/index.ts +5 -0
  78. package/src/database/seeder/employees/index.ts +35 -0
  79. package/src/database/seeder/index.ts +18 -0
  80. package/src/database/seeder/users/index.ts +44 -0
  81. package/src/index.ts +10 -0
  82. package/src/jobs/index.ts +2 -0
  83. package/src/jobs/mailer/index.ts +154 -0
  84. package/src/jobs/mailer/templates/auth.ts +113 -0
  85. package/src/jobs/mailer/templates/index.ts +1 -0
  86. package/src/jobs/queues/index.ts +125 -0
  87. package/src/middlewares/audit/index.ts +0 -0
  88. package/src/middlewares/guard/index.ts +64 -0
  89. package/src/middlewares/index.ts +5 -0
  90. package/src/middlewares/meeting.ts +45 -0
  91. package/src/models/employees/index.ts +70 -0
  92. package/src/models/index.ts +8 -0
  93. package/src/models/meetings/index.ts +112 -0
  94. package/src/models/passkeys/index.ts +53 -0
  95. package/src/models/queues/index.ts +55 -0
  96. package/src/models/users/index.ts +92 -0
  97. package/src/queues/index.ts +1 -0
  98. package/src/queues/mailer/index.ts +80 -0
  99. package/src/types/index.ts +38 -0
  100. package/src/utils/notifications.ts +66 -0
  101. package/tsconfig.json +18 -0
@@ -0,0 +1,981 @@
1
+ import { Response } from 'express'
2
+ import UsersModel from '../../models/users'
3
+ import EmployeesModel from '../../models/employees'
4
+ import PasskeysModel from '../../models/passkeys'
5
+ import { getEmailQueue } from '../../queues'
6
+ import bcrypt from 'bcrypt'
7
+ import jwt from 'jsonwebtoken'
8
+ import crypto from 'crypto'
9
+ import type { MulterRequest } from '../../types'
10
+
11
+ class Users {
12
+ /**
13
+ * Register a new user
14
+ */
15
+ public async register(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
16
+ try {
17
+ const body = req.body || {}
18
+
19
+ const { email, password, first_name, last_name, role = 'employee', employee_id } = body
20
+
21
+ if (!email || !password || !first_name || !last_name) {
22
+ res.status(400).json({ message: 'email, password, first_name, and last_name are required' })
23
+ return
24
+ }
25
+
26
+ // Check if user already exists
27
+ const existingUser = await UsersModel.findOne({
28
+ email,
29
+ deleted_at: null
30
+ }).lean().exec()
31
+
32
+ if (existingUser) {
33
+ res.status(409).json({ message: 'User with this email already exists' })
34
+ return
35
+ }
36
+
37
+ // Validate employee_id if provided
38
+ if (employee_id) {
39
+ const employee = await EmployeesModel.findOne({
40
+ _id: employee_id,
41
+ deleted_at: null
42
+ }).lean().exec()
43
+
44
+ if (!employee) {
45
+ res.status(400).json({ message: 'Invalid employee_id' })
46
+ return
47
+ }
48
+ }
49
+
50
+ // Hash password
51
+ const hashedPassword = await bcrypt.hash(password, 10)
52
+
53
+ // Create user
54
+ const user = await UsersModel.create({
55
+ email: email.trim().toLowerCase(),
56
+ password: hashedPassword,
57
+ first_name: first_name.trim(),
58
+ middle_name: body.middle_name?.trim() || null,
59
+ last_name: last_name.trim(),
60
+ role,
61
+ employee_id: employee_id || null,
62
+ is_active: true,
63
+ created_at: Date.now(),
64
+ updated_at: Date.now()
65
+ })
66
+
67
+ // Remove password from response
68
+ const userResponse = {
69
+ id: user._id,
70
+ email: user.email,
71
+ first_name: user.first_name,
72
+ middle_name: user.middle_name,
73
+ last_name: user.last_name,
74
+ full_name: `${user.first_name} ${user.last_name}`,
75
+ role: user.role,
76
+ employee_id: user.employee_id,
77
+ is_active: user.is_active,
78
+ created_at: user.created_at
79
+ }
80
+
81
+ res.status(201).json({
82
+ message: 'User registered successfully',
83
+ user: userResponse
84
+ })
85
+ } catch (err) {
86
+ console.error('Register user error', err)
87
+ res.status(500).json({ message: 'Failed to register user', error: String(err) })
88
+ }
89
+ }
90
+
91
+ /**
92
+ * User login
93
+ */
94
+ public async login(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
95
+ try {
96
+ const body = req.body || {}
97
+ const { email, password } = body
98
+
99
+ if (!email || !password) {
100
+ res.status(400).json({ message: 'email and password are required' })
101
+ return
102
+ }
103
+
104
+ // Find user by email
105
+ const user = await UsersModel.findOne({
106
+ email: email.trim().toLowerCase(),
107
+ deleted_at: null,
108
+ is_active: true
109
+ })
110
+ .populate('employee_id', 'first_name last_name email position department')
111
+ .lean()
112
+ .exec()
113
+
114
+ if (!user) {
115
+ res.status(401).json({ message: 'Invalid credentials' })
116
+ return
117
+ }
118
+
119
+ // Verify password
120
+ const isPasswordValid = await bcrypt.compare(password, user.password as string)
121
+
122
+ if (!isPasswordValid) {
123
+ res.status(401).json({ message: 'Invalid credentials' })
124
+ return
125
+ }
126
+
127
+ // Generate JWT tokens
128
+ const jwtSecret = process.env.JWT_SECRET;
129
+ if (!jwtSecret) {
130
+ throw new Error('JWT_SECRET environment variable is required');
131
+ }
132
+ const jwtRefreshSecret = process.env.JWT_REFRESH_SECRET || jwtSecret;
133
+
134
+ const accessToken = jwt.sign(
135
+ {
136
+ userId: user._id,
137
+ email: user.email,
138
+ role: user.role
139
+ },
140
+ jwtSecret,
141
+ { expiresIn: '15m' } // Short-lived access token
142
+ );
143
+
144
+ const refreshToken = jwt.sign(
145
+ {
146
+ userId: user._id,
147
+ email: user.email,
148
+ role: user.role
149
+ },
150
+ jwtRefreshSecret,
151
+ { expiresIn: '7d' } // Long-lived refresh token
152
+ );
153
+
154
+ // Remove password from response
155
+ const userResponse = {
156
+ id: user._id,
157
+ email: user.email,
158
+ first_name: user.first_name,
159
+ middle_name: user.middle_name,
160
+ last_name: user.last_name,
161
+ full_name: `${user.first_name} ${user.last_name}`,
162
+ role: user.role,
163
+ employee_id: user.employee_id,
164
+ is_active: user.is_active
165
+ }
166
+
167
+ res.json({
168
+ message: 'Login successful',
169
+ accessToken,
170
+ refreshToken,
171
+ user: userResponse
172
+ })
173
+ } catch (err) {
174
+ console.error('Login error', err)
175
+ res.status(500).json({ message: 'Login failed', error: String(err) })
176
+ }
177
+ }
178
+
179
+ /**
180
+ * User logout
181
+ */
182
+ public async logout(req: MulterRequest, res: Response): Promise<void> {
183
+ try {
184
+ // In a stateless JWT setup, logout is typically handled client-side
185
+ // by removing the token from storage. However, we can provide a response
186
+ // to confirm the logout action.
187
+ res.json({
188
+ message: 'Logout successful',
189
+ success: true
190
+ })
191
+ } catch (err) {
192
+ console.error('Logout error', err)
193
+ res.status(500).json({ message: 'Logout failed', error: String(err) })
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Get user profile
199
+ */
200
+ public async getProfile(req: MulterRequest, res: Response): Promise<void> {
201
+ try {
202
+ // Get user ID from JWT token (assuming middleware has authenticated the user)
203
+ const userId = (req as any).user?.userId
204
+ console.log('Get profile for userId:', userId)
205
+
206
+ if (!userId) {
207
+ console.log('No userId in request')
208
+ res.status(401).json({ message: 'Unauthorized' })
209
+ return
210
+ }
211
+
212
+ const user = await UsersModel.findOne({
213
+ _id: userId,
214
+ deleted_at: null
215
+ })
216
+ .select('-password -__v')
217
+ .populate('employee_id', 'first_name last_name middle_name email position department contact_number address')
218
+ .lean()
219
+ .exec()
220
+
221
+ console.log('User found:', !!user)
222
+ if (!user) {
223
+ console.log('User not found for id:', userId)
224
+ res.status(404).json({ message: 'User not found' })
225
+ return
226
+ }
227
+
228
+ const response = {
229
+ message: 'Profile retrieved successfully',
230
+ user
231
+ }
232
+ console.log('Sending response:', response)
233
+ res.json(response)
234
+ } catch (err) {
235
+ console.error('Get profile error', err)
236
+ res.status(500).json({ message: 'Failed to retrieve profile', error: String(err) })
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Get all users
242
+ */
243
+ public async getAll(req: MulterRequest, res: Response): Promise<void> {
244
+ try {
245
+ const users = await UsersModel.find({ deleted_at: null })
246
+ .select('-password -__v')
247
+ .populate('employee_id', 'first_name last_name email position department')
248
+ .sort({ created_at: -1 })
249
+ .lean()
250
+ .exec()
251
+
252
+ res.json({
253
+ message: 'Users retrieved successfully',
254
+ count: users.length,
255
+ users
256
+ })
257
+ } catch (err) {
258
+ console.error('Get users error', err)
259
+ res.status(500).json({ message: 'Failed to retrieve users', error: String(err) })
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Get user by ID
265
+ */
266
+ public async getById(req: MulterRequest, res: Response): Promise<void> {
267
+ try {
268
+ const { id } = req.params
269
+
270
+ if (!id) {
271
+ res.status(400).json({ message: 'User ID is required' })
272
+ return
273
+ }
274
+
275
+ const user = await UsersModel.findOne({
276
+ _id: id,
277
+ deleted_at: null
278
+ })
279
+ .select('-password -__v')
280
+ .populate('employee_id', 'first_name last_name email position department')
281
+ .lean()
282
+ .exec()
283
+
284
+ if (!user) {
285
+ res.status(404).json({ message: 'User not found' })
286
+ return
287
+ }
288
+
289
+ res.json({
290
+ message: 'User retrieved successfully',
291
+ user
292
+ })
293
+ } catch (err) {
294
+ console.error('Get user error', err)
295
+ res.status(500).json({ message: 'Failed to retrieve user', error: String(err) })
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Update user
301
+ */
302
+ public async update(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
303
+ try {
304
+ const { id } = req.params
305
+ const body = req.body || {}
306
+
307
+ if (!id) {
308
+ res.status(400).json({ message: 'User ID is required' })
309
+ return
310
+ }
311
+
312
+ const updateData: any = {
313
+ updated_at: Date.now()
314
+ }
315
+
316
+ // Only update provided fields (excluding password)
317
+ const allowedFields = ['email', 'first_name', 'middle_name', 'last_name', 'role', 'employee_id', 'is_active']
318
+ for (const field of allowedFields) {
319
+ if (body[field] !== undefined) {
320
+ updateData[field] = body[field]
321
+ }
322
+ }
323
+
324
+ // Validate employee_id if provided
325
+ if (updateData.employee_id) {
326
+ const employee = await EmployeesModel.findOne({
327
+ _id: updateData.employee_id,
328
+ deleted_at: null
329
+ }).lean().exec()
330
+
331
+ if (!employee) {
332
+ res.status(400).json({ message: 'Invalid employee_id' })
333
+ return
334
+ }
335
+ }
336
+
337
+ const user = await UsersModel.findOneAndUpdate(
338
+ { _id: id, deleted_at: null },
339
+ updateData,
340
+ { new: true, runValidators: true }
341
+ )
342
+ .select('-password -__v')
343
+ .populate('employee_id', 'first_name last_name email position department')
344
+ .lean()
345
+ .exec()
346
+
347
+ if (!user) {
348
+ res.status(404).json({ message: 'User not found' })
349
+ return
350
+ }
351
+
352
+ res.json({
353
+ message: 'User updated successfully',
354
+ user
355
+ })
356
+ } catch (err) {
357
+ console.error('Update user error', err)
358
+ res.status(500).json({ message: 'Failed to update user', error: String(err) })
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Soft delete user
364
+ */
365
+ public async delete(req: MulterRequest, res: Response): Promise<void> {
366
+ try {
367
+ const { id } = req.params
368
+
369
+ if (!id) {
370
+ res.status(400).json({ message: 'User ID is required' })
371
+ return
372
+ }
373
+
374
+ const user = await UsersModel.findOneAndUpdate(
375
+ { _id: id, deleted_at: null },
376
+ { deleted_at: Date.now() },
377
+ { new: true }
378
+ )
379
+ .lean()
380
+ .exec()
381
+
382
+ if (!user) {
383
+ res.status(404).json({ message: 'User not found' })
384
+ return
385
+ }
386
+
387
+ res.json({
388
+ message: 'User deleted successfully',
389
+ user: {
390
+ id: user._id,
391
+ email: user.email
392
+ }
393
+ })
394
+ } catch (err) {
395
+ console.error('Delete user error', err)
396
+ res.status(500).json({ message: 'Failed to delete user', error: String(err) })
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Forgot password - send reset email
402
+ */
403
+ public async forgotPassword(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
404
+ try {
405
+ const body = req.body || {}
406
+ const { email } = body
407
+
408
+ if (!email) {
409
+ res.status(400).json({ message: 'Email is required' })
410
+ return
411
+ }
412
+
413
+ // Find user by email
414
+ const user = await UsersModel.findOne({
415
+ email: email.trim().toLowerCase(),
416
+ deleted_at: null,
417
+ is_active: true
418
+ }).lean().exec() as any
419
+
420
+ if (!user) {
421
+ res.status(404).json({ message: 'User with this email not found' })
422
+ return
423
+ }
424
+
425
+ // Generate reset token
426
+ const resetToken = crypto.randomBytes(32).toString('hex')
427
+ const resetTokenExpiry = Date.now() + (60 * 60 * 1000) // 1 hour
428
+
429
+ // Update user with reset token
430
+ await UsersModel.updateOne(
431
+ { _id: user._id },
432
+ {
433
+ reset_token: resetToken,
434
+ reset_token_expires: resetTokenExpiry,
435
+ updated_at: Date.now()
436
+ }
437
+ )
438
+
439
+ // Add to email queue
440
+ await getEmailQueue().add('send-email', {
441
+ type: 'password_reset',
442
+ data: {
443
+ user_id: user._id,
444
+ email: user.email,
445
+ reset_token: resetToken,
446
+ name: user.full_name || `${user.first_name} ${user.last_name}`
447
+ }
448
+ })
449
+
450
+ res.json({
451
+ message: 'Password reset email sent successfully',
452
+ success: true
453
+ })
454
+ } catch (err) {
455
+ console.error('Forgot password error', err)
456
+ res.status(500).json({ message: 'Failed to send password reset email', error: String(err) })
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Change password
462
+ */
463
+ public async changePassword(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
464
+ try {
465
+ const body = req.body || {}
466
+ const { current_password, new_password } = body
467
+
468
+ if (!current_password || !new_password) {
469
+ res.status(400).json({ message: 'Current password and new password are required' })
470
+ return
471
+ }
472
+
473
+ // Get user ID from JWT token
474
+ const userId = (req as any).user?.userId
475
+ if (!userId) {
476
+ res.status(401).json({ message: 'Unauthorized' })
477
+ return
478
+ }
479
+
480
+ // Find user
481
+ const user = await UsersModel.findOne({
482
+ _id: userId,
483
+ deleted_at: null,
484
+ is_active: true
485
+ }).exec()
486
+
487
+ if (!user) {
488
+ res.status(404).json({ message: 'User not found' })
489
+ return
490
+ }
491
+
492
+ // Verify current password
493
+ const isCurrentPasswordValid = await bcrypt.compare(current_password, user.password as string)
494
+ if (!isCurrentPasswordValid) {
495
+ res.status(400).json({ message: 'Current password is incorrect' })
496
+ return
497
+ }
498
+
499
+ // Hash new password
500
+ const hashedNewPassword = await bcrypt.hash(new_password, 10)
501
+
502
+ // Update password
503
+ await UsersModel.updateOne(
504
+ { _id: userId },
505
+ {
506
+ password: hashedNewPassword,
507
+ updated_at: Date.now()
508
+ }
509
+ )
510
+
511
+ // Add to email queue
512
+ await getEmailQueue().add('send-email', {
513
+ type: 'password_changed',
514
+ data: {
515
+ user_id: user._id,
516
+ email: user.email,
517
+ name: `${user.first_name} ${user.middle_name ? user.middle_name + ' ' : ''}${user.last_name}`
518
+ }
519
+ })
520
+
521
+ res.json({
522
+ message: 'Password changed successfully',
523
+ success: true
524
+ })
525
+ } catch (err) {
526
+ console.error('Change password error', err)
527
+ res.status(500).json({ message: 'Failed to change password', error: String(err) })
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Start passkey registration challenge
533
+ */
534
+ public async registerPasskey(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
535
+ try {
536
+ const body = req.body || {}
537
+ const { user_id, email } = body
538
+
539
+ if (!user_id || !email) {
540
+ res.status(400).json({ message: 'User ID and email are required' })
541
+ return
542
+ }
543
+
544
+ // Find user
545
+ const user = await UsersModel.findOne({
546
+ _id: user_id,
547
+ email: email.trim().toLowerCase(),
548
+ deleted_at: null,
549
+ is_active: true
550
+ }).lean().exec()
551
+
552
+ if (!user) {
553
+ res.status(404).json({ message: 'User not found' })
554
+ return
555
+ }
556
+
557
+ // Generate WebAuthn challenge
558
+ const challenge = crypto.randomBytes(32)
559
+ const deviceId = crypto.randomBytes(16).toString('hex')
560
+
561
+ res.json({
562
+ challenge: challenge.toString('base64'),
563
+ device_id: deviceId,
564
+ device_name: `Device ${new Date().toLocaleDateString()}`
565
+ })
566
+ } catch (err) {
567
+ console.error('Register passkey challenge error', err)
568
+ res.status(500).json({ message: 'Failed to start registration', error: String(err) })
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Complete passkey registration
574
+ */
575
+ public async completePasskeyRegistration(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
576
+ try {
577
+ const body = req.body || {}
578
+ const { user_id, credential_id, device_id, name, public_key } = body
579
+
580
+ if (!user_id || !credential_id || !device_id || !name || !public_key) {
581
+ res.status(400).json({ message: 'All fields are required' })
582
+ return
583
+ }
584
+
585
+ // Check if passkey already exists
586
+ const existingPasskey = await PasskeysModel.findOne({
587
+ credential_id,
588
+ deleted_at: null
589
+ }).lean().exec()
590
+
591
+ if (existingPasskey) {
592
+ res.status(409).json({ message: 'Passkey already registered' })
593
+ return
594
+ }
595
+
596
+ // Get user info for email
597
+ const user = await UsersModel.findOne({ _id: user_id }).lean().exec()
598
+ if (!user) {
599
+ res.status(404).json({ message: 'User not found' })
600
+ return
601
+ }
602
+
603
+ // Create new passkey
604
+ await PasskeysModel.create({
605
+ user_id,
606
+ credential_id,
607
+ device_id,
608
+ name,
609
+ public_key,
610
+ created_at: Date.now(),
611
+ updated_at: Date.now()
612
+ })
613
+
614
+ // Add to email queue
615
+ await getEmailQueue().add('send-email', {
616
+ type: 'passkey',
617
+ data: {
618
+ user_id: user._id,
619
+ email: user.email,
620
+ name: `${user.first_name} ${user.middle_name ? user.middle_name + ' ' : ''}${user.last_name}`,
621
+ device_name: name
622
+ }
623
+ })
624
+
625
+ res.json({
626
+ message: 'Passkey registered successfully',
627
+ success: true,
628
+ device_id,
629
+ device_name: name
630
+ })
631
+ } catch (err) {
632
+ console.error('Complete passkey registration error', err)
633
+ res.status(500).json({ message: 'Failed to register passkey', error: String(err) })
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Get user passkeys
639
+ */
640
+ public async getPasskeys(req: MulterRequest, res: Response): Promise<void> {
641
+ try {
642
+ const { user_id } = req.params
643
+
644
+ if (!user_id) {
645
+ res.status(400).json({ message: 'User ID is required' })
646
+ return
647
+ }
648
+
649
+ const passkeys = await PasskeysModel.find({
650
+ user_id,
651
+ deleted_at: null,
652
+ is_active: true
653
+ })
654
+ .select('-__v')
655
+ .sort({ created_at: -1 })
656
+ .lean()
657
+ .exec()
658
+
659
+ res.json({
660
+ message: 'Passkeys retrieved successfully',
661
+ passkeys
662
+ })
663
+ } catch (err) {
664
+ console.error('Get passkeys error', err)
665
+ res.status(500).json({ message: 'Failed to retrieve passkeys', error: String(err) })
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Search users by email
671
+ */
672
+ public async searchUsers(req: MulterRequest, res: Response): Promise<void> {
673
+ try {
674
+ const query = req.query.q as string;
675
+
676
+ if (!query || query.trim().length < 2) {
677
+ res.json({
678
+ message: 'Search query must be at least 2 characters',
679
+ users: []
680
+ });
681
+ return;
682
+ }
683
+
684
+ const searchTerm = query.trim().toLowerCase();
685
+
686
+ const users = await UsersModel.find({
687
+ deleted_at: null,
688
+ is_active: true,
689
+ $or: [
690
+ { email: { $regex: searchTerm, $options: 'i' } },
691
+ { first_name: { $regex: searchTerm, $options: 'i' } },
692
+ { last_name: { $regex: searchTerm, $options: 'i' } }
693
+ ]
694
+ })
695
+ .select('-password -__v')
696
+ .limit(10)
697
+ .lean()
698
+ .exec();
699
+
700
+ res.json({
701
+ message: 'Users retrieved successfully',
702
+ users,
703
+ count: users.length
704
+ });
705
+ } catch (err) {
706
+ console.error('Search users error', err);
707
+ res.status(500).json({ message: 'Failed to search users', error: String(err) });
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Start passkey authentication challenge
713
+ */
714
+ public async authenticatePasskey(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
715
+ try {
716
+ const body = req.body || {}
717
+ const { email, device_id } = body
718
+
719
+ if (!email || !device_id) {
720
+ res.status(400).json({ message: 'Email and device ID are required' })
721
+ return
722
+ }
723
+
724
+ // Find user by email
725
+ const user = await UsersModel.findOne({
726
+ email: email.trim().toLowerCase(),
727
+ deleted_at: null,
728
+ is_active: true
729
+ }).lean().exec()
730
+
731
+ if (!user) {
732
+ res.status(401).json({ message: 'Invalid credentials' })
733
+ return
734
+ }
735
+
736
+ // Find the latest active passkey for this user and device
737
+ const passkey = await PasskeysModel.findOne({
738
+ user_id: user._id,
739
+ device_id,
740
+ is_active: true,
741
+ deleted_at: null
742
+ })
743
+ .sort({ created_at: -1 })
744
+ .lean()
745
+ .exec()
746
+
747
+ if (!passkey) {
748
+ res.status(401).json({ message: 'No passkey found for this device' })
749
+ return
750
+ }
751
+
752
+ // Generate WebAuthn challenge
753
+ const challenge = crypto.randomBytes(32)
754
+
755
+ res.json({
756
+ challenge: challenge.toString('base64'),
757
+ user_id: user._id,
758
+ credential_id: passkey.credential_id
759
+ })
760
+ } catch (err) {
761
+ console.error('Passkey authentication challenge error', err)
762
+ res.status(500).json({ message: 'Failed to start authentication', error: String(err) })
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Validate reset token
768
+ */
769
+ public async validateResetToken(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
770
+ try {
771
+ const body = req.body || {}
772
+ const { token } = body
773
+
774
+ if (!token) {
775
+ res.status(400).json({ message: 'Token is required' })
776
+ return
777
+ }
778
+
779
+ // Find user with valid reset token
780
+ const user = await UsersModel.findOne({
781
+ reset_token: token,
782
+ reset_token_expires: { $gt: Date.now() },
783
+ deleted_at: null,
784
+ is_active: true
785
+ }).lean().exec()
786
+
787
+ if (!user) {
788
+ res.status(400).json({ message: 'Invalid or expired token' })
789
+ return
790
+ }
791
+
792
+ res.json({
793
+ message: 'Token is valid',
794
+ valid: true
795
+ })
796
+ } catch (err) {
797
+ console.error('Validate reset token error', err)
798
+ res.status(500).json({ message: 'Failed to validate token', error: String(err) })
799
+ }
800
+ }
801
+
802
+ /**
803
+ * Reset password with token
804
+ */
805
+ public async resetPassword(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
806
+ try {
807
+ const body = req.body || {}
808
+ const { token, new_password } = body
809
+
810
+ if (!token || !new_password) {
811
+ res.status(400).json({ message: 'Token and new password are required' })
812
+ return
813
+ }
814
+
815
+ // Find user with valid reset token
816
+ const user = await UsersModel.findOne({
817
+ reset_token: token,
818
+ reset_token_expires: { $gt: Date.now() },
819
+ deleted_at: null,
820
+ is_active: true
821
+ }).exec()
822
+
823
+ if (!user) {
824
+ res.status(400).json({ message: 'Invalid or expired token' })
825
+ return
826
+ }
827
+
828
+ // Hash new password
829
+ const hashedPassword = await bcrypt.hash(new_password, 10)
830
+
831
+ // Update password and clear reset token
832
+ await UsersModel.updateOne(
833
+ { _id: user._id },
834
+ {
835
+ password: hashedPassword,
836
+ reset_token: null,
837
+ reset_token_expires: null,
838
+ updated_at: Date.now()
839
+ }
840
+ )
841
+
842
+ // Add to email queue for password changed notification
843
+ await getEmailQueue().add('send-email', {
844
+ type: 'password_changed',
845
+ data: {
846
+ user_id: user._id,
847
+ email: user.email,
848
+ name: `${user.first_name} ${user.middle_name ? user.middle_name + ' ' : ''}${user.last_name}`
849
+ }
850
+ })
851
+
852
+ res.json({
853
+ message: 'Password reset successfully',
854
+ success: true
855
+ })
856
+ } catch (err) {
857
+ console.error('Reset password error', err)
858
+ res.status(500).json({ message: 'Failed to reset password', error: String(err) })
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Complete passkey authentication
864
+ */
865
+ public async verifyPasskey(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
866
+ try {
867
+ const body = req.body || {}
868
+ const { email, device_id, credential_id, authenticator_data, client_data_json, signature } = body
869
+
870
+ if (!email || !device_id || !credential_id || !authenticator_data || !client_data_json || !signature) {
871
+ res.status(400).json({ message: 'All authentication data is required' })
872
+ return
873
+ }
874
+
875
+ // Find user by email
876
+ const user = await UsersModel.findOne({
877
+ email: email.trim().toLowerCase(),
878
+ deleted_at: null,
879
+ is_active: true
880
+ }).lean().exec()
881
+
882
+ if (!user) {
883
+ res.status(401).json({ message: 'Invalid credentials' })
884
+ return
885
+ }
886
+
887
+ // Find the passkey
888
+ const passkey = await PasskeysModel.findOne({
889
+ user_id: user._id,
890
+ credential_id,
891
+ device_id,
892
+ is_active: true,
893
+ deleted_at: null
894
+ }).lean().exec()
895
+
896
+ if (!passkey) {
897
+ res.status(401).json({ message: 'Invalid passkey' })
898
+ return
899
+ }
900
+
901
+ // TODO: Implement WebAuthn signature verification
902
+ // For now, we'll trust the client-side verification and proceed
903
+
904
+ // Update last used timestamp
905
+ await PasskeysModel.updateOne(
906
+ { _id: passkey._id },
907
+ { last_used_at: Date.now(), updated_at: Date.now() }
908
+ )
909
+
910
+ // Generate JWT token
911
+ const jwtSecret = process.env.JWT_SECRET
912
+ if (!jwtSecret) {
913
+ throw new Error('JWT_SECRET environment variable is required')
914
+ }
915
+
916
+ const token = jwt.sign(
917
+ {
918
+ userId: user._id,
919
+ email: user.email,
920
+ role: user.role
921
+ },
922
+ jwtSecret,
923
+ { expiresIn: '24h' }
924
+ )
925
+
926
+ res.json({
927
+ message: 'Passkey authentication successful',
928
+ token,
929
+ user: {
930
+ id: user._id,
931
+ email: user.email,
932
+ first_name: user.first_name,
933
+ middle_name: user.middle_name,
934
+ last_name: user.last_name,
935
+ full_name: `${user.first_name} ${user.last_name}`,
936
+ role: user.role,
937
+ employee_id: user.employee_id,
938
+ is_active: user.is_active
939
+ }
940
+ })
941
+ } catch (err) {
942
+ console.error('Passkey verification error', err)
943
+ res.status(500).json({ message: 'Passkey authentication failed', error: String(err) })
944
+ }
945
+ }
946
+
947
+ /**
948
+ * Delete passkey
949
+ */
950
+ public async deletePasskey(req: MulterRequest, res: Response): Promise<void> {
951
+ try {
952
+ const { passkeyId } = req.params
953
+
954
+ if (!passkeyId) {
955
+ res.status(400).json({ message: 'Passkey ID is required' })
956
+ return
957
+ }
958
+
959
+ const passkey = await PasskeysModel.findOneAndUpdate(
960
+ { _id: passkeyId, deleted_at: null },
961
+ { deleted_at: Date.now() },
962
+ { new: true }
963
+ ).lean().exec()
964
+
965
+ if (!passkey) {
966
+ res.status(404).json({ message: 'Passkey not found' })
967
+ return
968
+ }
969
+
970
+ res.json({
971
+ message: 'Passkey deleted successfully',
972
+ success: true
973
+ })
974
+ } catch (err) {
975
+ console.error('Delete passkey error', err)
976
+ res.status(500).json({ message: 'Failed to delete passkey', error: String(err) })
977
+ }
978
+ }
979
+ }
980
+
981
+ export default Users;