@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.
- package/dist/controllers/auth/index.d.ts +65 -0
- package/dist/controllers/auth/index.js +525 -0
- package/dist/controllers/employees/index.d.ts +38 -0
- package/dist/controllers/employees/index.js +185 -0
- package/dist/controllers/health/index.d.ts +9 -0
- package/dist/controllers/health/index.js +42 -0
- package/dist/controllers/index.d.ts +16 -0
- package/dist/controllers/index.js +19 -0
- package/dist/controllers/meetings/index.d.ts +23 -0
- package/dist/controllers/meetings/index.js +233 -0
- package/dist/controllers/modules/index.d.ts +5 -0
- package/dist/controllers/modules/index.js +104 -0
- package/dist/controllers/users/index.d.ts +103 -0
- package/dist/controllers/users/index.js +841 -0
- package/dist/data/modules.json +94 -0
- package/dist/database/config/index.d.ts +2 -0
- package/dist/database/config/index.js +32 -0
- package/dist/database/index.d.ts +9 -0
- package/dist/database/index.js +9 -0
- package/dist/database/seeder/employees/index.d.ts +1 -0
- package/dist/database/seeder/employees/index.js +40 -0
- package/dist/database/seeder/index.d.ts +4 -0
- package/dist/database/seeder/index.js +20 -0
- package/dist/database/seeder/users/index.d.ts +1 -0
- package/dist/database/seeder/users/index.js +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +23 -0
- package/dist/jobs/index.d.ts +1 -0
- package/dist/jobs/index.js +18 -0
- package/dist/jobs/mailer/index.d.ts +4 -0
- package/dist/jobs/mailer/index.js +186 -0
- package/dist/jobs/mailer/templates/auth.d.ts +11 -0
- package/dist/jobs/mailer/templates/auth.js +117 -0
- package/dist/jobs/mailer/templates/index.d.ts +1 -0
- package/dist/jobs/mailer/templates/index.js +17 -0
- package/dist/jobs/queues/index.d.ts +3 -0
- package/dist/jobs/queues/index.js +115 -0
- package/dist/middlewares/audit/index.d.ts +0 -0
- package/dist/middlewares/audit/index.js +1 -0
- package/dist/middlewares/guard/index.d.ts +11 -0
- package/dist/middlewares/guard/index.js +53 -0
- package/dist/middlewares/index.d.ts +2 -0
- package/dist/middlewares/index.js +7 -0
- package/dist/middlewares/meeting.d.ts +9 -0
- package/dist/middlewares/meeting.js +34 -0
- package/dist/models/employees/index.d.ts +83 -0
- package/dist/models/employees/index.js +70 -0
- package/dist/models/index.d.ts +570 -0
- package/dist/models/index.js +17 -0
- package/dist/models/meetings/index.d.ts +227 -0
- package/dist/models/meetings/index.js +112 -0
- package/dist/models/passkeys/index.d.ts +77 -0
- package/dist/models/passkeys/index.js +55 -0
- package/dist/models/queues/index.d.ts +77 -0
- package/dist/models/queues/index.js +57 -0
- package/dist/models/users/index.d.ts +107 -0
- package/dist/models/users/index.js +92 -0
- package/dist/queues/index.d.ts +1 -0
- package/dist/queues/index.js +17 -0
- package/dist/queues/mailer/index.d.ts +4 -0
- package/dist/queues/mailer/index.js +74 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/notifications.d.ts +2 -0
- package/dist/utils/notifications.js +51 -0
- package/package.json +39 -0
- package/public/health.html +215 -0
- package/src/controllers/auth/index.ts +609 -0
- package/src/controllers/employees/index.ts +210 -0
- package/src/controllers/health/index.ts +41 -0
- package/src/controllers/index.ts +9 -0
- package/src/controllers/meetings/index.ts +251 -0
- package/src/controllers/modules/index.ts +74 -0
- package/src/controllers/users/index.ts +981 -0
- package/src/data/modules.json +94 -0
- package/src/database/config/index.ts +26 -0
- package/src/database/index.ts +5 -0
- package/src/database/seeder/employees/index.ts +35 -0
- package/src/database/seeder/index.ts +18 -0
- package/src/database/seeder/users/index.ts +44 -0
- package/src/index.ts +10 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/mailer/index.ts +154 -0
- package/src/jobs/mailer/templates/auth.ts +113 -0
- package/src/jobs/mailer/templates/index.ts +1 -0
- package/src/jobs/queues/index.ts +125 -0
- package/src/middlewares/audit/index.ts +0 -0
- package/src/middlewares/guard/index.ts +64 -0
- package/src/middlewares/index.ts +5 -0
- package/src/middlewares/meeting.ts +45 -0
- package/src/models/employees/index.ts +70 -0
- package/src/models/index.ts +8 -0
- package/src/models/meetings/index.ts +112 -0
- package/src/models/passkeys/index.ts +53 -0
- package/src/models/queues/index.ts +55 -0
- package/src/models/users/index.ts +92 -0
- package/src/queues/index.ts +1 -0
- package/src/queues/mailer/index.ts +80 -0
- package/src/types/index.ts +38 -0
- package/src/utils/notifications.ts +66 -0
- 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;
|