@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,609 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
import type { MulterRequest } from '../../types';
|
|
3
|
+
import UsersModel from '../../models/users';
|
|
4
|
+
import PasskeysModel from '../../models/passkeys';
|
|
5
|
+
import Users from './../../controllers/users';
|
|
6
|
+
import { getEmailQueue } from '../../queues';
|
|
7
|
+
import bcrypt from 'bcrypt';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import jwt from 'jsonwebtoken';
|
|
10
|
+
import type { Document } from 'mongoose';
|
|
11
|
+
|
|
12
|
+
export class AuthController extends Users {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get user profile
|
|
16
|
+
*/
|
|
17
|
+
public async getProfile(req: MulterRequest, res: Response): Promise<void> {
|
|
18
|
+
console.log('=== Backend AuthController.getProfile called ===');
|
|
19
|
+
console.log('req.user:', (req as any).user);
|
|
20
|
+
try {
|
|
21
|
+
// Get user ID from JWT token
|
|
22
|
+
const userId = (req as any).user?.userId;
|
|
23
|
+
console.log('Get profile for userId:', userId);
|
|
24
|
+
|
|
25
|
+
if (!userId) {
|
|
26
|
+
console.log('No userId in request');
|
|
27
|
+
res.status(401).json({ message: 'Unauthorized' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const user = await UsersModel.findOne({
|
|
32
|
+
_id: userId,
|
|
33
|
+
deleted_at: null
|
|
34
|
+
})
|
|
35
|
+
.select('-password -__v')
|
|
36
|
+
.populate('employee_id', 'first_name last_name middle_name email position department contact_number address')
|
|
37
|
+
.lean()
|
|
38
|
+
.exec();
|
|
39
|
+
|
|
40
|
+
console.log('User found:', !!user);
|
|
41
|
+
if (!user) {
|
|
42
|
+
console.log('User not found for id:', userId);
|
|
43
|
+
res.status(404).json({ message: 'User not found' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Format user data same as login response
|
|
48
|
+
const userData = {
|
|
49
|
+
id: user._id,
|
|
50
|
+
email: user.email,
|
|
51
|
+
first_name: user.first_name,
|
|
52
|
+
last_name: user.last_name,
|
|
53
|
+
middle_name: user.middle_name,
|
|
54
|
+
full_name: `${user.first_name} ${user.last_name}`,
|
|
55
|
+
role: user.role,
|
|
56
|
+
employee_id: user.employee_id,
|
|
57
|
+
is_active: user.is_active,
|
|
58
|
+
created_at: user.created_at
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const response = {
|
|
62
|
+
message: 'Profile retrieved successfully',
|
|
63
|
+
user: userData
|
|
64
|
+
};
|
|
65
|
+
console.log('Sending response:', response);
|
|
66
|
+
console.log('=== Backend AuthController.getProfile completed ===');
|
|
67
|
+
res.json(response);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Get profile error', err);
|
|
70
|
+
console.log('=== Backend AuthController.getProfile failed ===');
|
|
71
|
+
res.status(500).json({ message: 'Failed to retrieve profile', error: String(err) });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Forgot password - send reset email
|
|
77
|
+
*/
|
|
78
|
+
public async forgotPassword(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
const body = req.body || {};
|
|
81
|
+
const { email } = body;
|
|
82
|
+
|
|
83
|
+
if (!email) {
|
|
84
|
+
res.status(400).json({ message: 'Email is required' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find user by email
|
|
89
|
+
const user = await UsersModel.findOne({
|
|
90
|
+
email: email.trim().toLowerCase(),
|
|
91
|
+
deleted_at: null,
|
|
92
|
+
is_active: true
|
|
93
|
+
}).lean().exec() as any;
|
|
94
|
+
|
|
95
|
+
if (!user) {
|
|
96
|
+
res.status(404).json({ message: 'User with this email not found' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate reset token
|
|
101
|
+
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
102
|
+
const resetTokenExpiry = Date.now() + (60 * 60 * 1000); // 1 hour
|
|
103
|
+
|
|
104
|
+
// Update user with reset token
|
|
105
|
+
await UsersModel.updateOne(
|
|
106
|
+
{ _id: user._id },
|
|
107
|
+
{
|
|
108
|
+
reset_token: resetToken,
|
|
109
|
+
reset_token_expires: resetTokenExpiry,
|
|
110
|
+
updated_at: Date.now()
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Add to email queue
|
|
115
|
+
await getEmailQueue().add('send-email', {
|
|
116
|
+
type: 'password_reset',
|
|
117
|
+
data: {
|
|
118
|
+
user_id: user._id,
|
|
119
|
+
email: user.email,
|
|
120
|
+
reset_token: resetToken,
|
|
121
|
+
name: user.full_name || `${user.first_name} ${user.last_name}`
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
res.json({
|
|
126
|
+
message: 'Password reset email sent successfully',
|
|
127
|
+
success: true
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error('Forgot password error', err);
|
|
131
|
+
res.status(500).json({ message: 'Failed to send password reset email', error: String(err) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Change password
|
|
137
|
+
*/
|
|
138
|
+
public async changePassword(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
139
|
+
try {
|
|
140
|
+
const body = req.body || {};
|
|
141
|
+
const { current_password, new_password } = body;
|
|
142
|
+
|
|
143
|
+
if (!current_password || !new_password) {
|
|
144
|
+
res.status(400).json({ message: 'Current password and new password are required' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Get user ID from JWT token
|
|
149
|
+
const userId = (req as any).user?.userId;
|
|
150
|
+
if (!userId) {
|
|
151
|
+
res.status(401).json({ message: 'Unauthorized' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Find user
|
|
156
|
+
const user = await UsersModel.findOne({
|
|
157
|
+
_id: userId,
|
|
158
|
+
deleted_at: null,
|
|
159
|
+
is_active: true
|
|
160
|
+
}).exec();
|
|
161
|
+
|
|
162
|
+
if (!user) {
|
|
163
|
+
res.status(404).json({ message: 'User not found' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Verify current password
|
|
168
|
+
const isCurrentPasswordValid = await bcrypt.compare(current_password, user.password as string);
|
|
169
|
+
if (!isCurrentPasswordValid) {
|
|
170
|
+
res.status(400).json({ message: 'Current password is incorrect' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Hash new password
|
|
175
|
+
const hashedNewPassword = await bcrypt.hash(new_password, 10);
|
|
176
|
+
|
|
177
|
+
// Update password
|
|
178
|
+
await UsersModel.updateOne(
|
|
179
|
+
{ _id: userId },
|
|
180
|
+
{
|
|
181
|
+
password: hashedNewPassword,
|
|
182
|
+
updated_at: Date.now()
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Add to email queue
|
|
187
|
+
await getEmailQueue().add('send-email', {
|
|
188
|
+
type: 'password_changed',
|
|
189
|
+
data: {
|
|
190
|
+
user_id: user._id,
|
|
191
|
+
email: user.email,
|
|
192
|
+
name: `${user.first_name} ${user.middle_name ? user.middle_name + ' ' : ''}${user.last_name}`
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
res.json({
|
|
197
|
+
message: 'Password changed successfully',
|
|
198
|
+
success: true
|
|
199
|
+
});
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error('Change password error', err);
|
|
202
|
+
res.status(500).json({ message: 'Failed to change password', error: String(err) });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Start passkey registration challenge
|
|
208
|
+
*/
|
|
209
|
+
public async registerPasskey(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
210
|
+
try {
|
|
211
|
+
const body = req.body || {};
|
|
212
|
+
const { user_id, email } = body;
|
|
213
|
+
|
|
214
|
+
if (!user_id || !email) {
|
|
215
|
+
res.status(400).json({ message: 'User ID and email are required' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Find user
|
|
220
|
+
const user = await UsersModel.findOne({
|
|
221
|
+
_id: user_id,
|
|
222
|
+
email: email.trim().toLowerCase(),
|
|
223
|
+
deleted_at: null,
|
|
224
|
+
is_active: true
|
|
225
|
+
}).lean().exec();
|
|
226
|
+
|
|
227
|
+
if (!user) {
|
|
228
|
+
res.status(404).json({ message: 'User not found' });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Generate WebAuthn challenge
|
|
233
|
+
const challenge = crypto.randomBytes(32);
|
|
234
|
+
const deviceId = crypto.randomBytes(16).toString('hex');
|
|
235
|
+
|
|
236
|
+
res.json({
|
|
237
|
+
challenge: challenge.toString('base64'),
|
|
238
|
+
device_id: deviceId,
|
|
239
|
+
device_name: `Device ${new Date().toLocaleDateString()}`
|
|
240
|
+
});
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error('Register passkey challenge error', err);
|
|
243
|
+
res.status(500).json({ message: 'Failed to start registration', error: String(err) });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Complete passkey registration
|
|
249
|
+
*/
|
|
250
|
+
public async completePasskeyRegistration(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
251
|
+
try {
|
|
252
|
+
const body = req.body || {};
|
|
253
|
+
const { user_id, credential_id, device_id, name, public_key } = body;
|
|
254
|
+
|
|
255
|
+
if (!user_id || !credential_id || !device_id || !name || !public_key) {
|
|
256
|
+
res.status(400).json({ message: 'All fields are required' });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check if passkey already exists
|
|
261
|
+
const existingPasskey = await PasskeysModel.findOne({
|
|
262
|
+
credential_id,
|
|
263
|
+
deleted_at: null
|
|
264
|
+
}).lean().exec();
|
|
265
|
+
|
|
266
|
+
if (existingPasskey) {
|
|
267
|
+
res.status(409).json({ message: 'Passkey already registered' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Get user info for email
|
|
272
|
+
const user = await UsersModel.findOne({ _id: user_id }).lean().exec();
|
|
273
|
+
if (!user) {
|
|
274
|
+
res.status(404).json({ message: 'User not found' });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Create new passkey - public_key is COSE format CBOR from WebAuthn
|
|
279
|
+
await PasskeysModel.create({
|
|
280
|
+
user_id,
|
|
281
|
+
credential_id,
|
|
282
|
+
device_id,
|
|
283
|
+
name,
|
|
284
|
+
public_key: public_key, // Store COSE public key
|
|
285
|
+
created_at: Date.now(),
|
|
286
|
+
updated_at: Date.now()
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Add to email queue
|
|
290
|
+
await getEmailQueue().add('send-email', {
|
|
291
|
+
type: 'passkey',
|
|
292
|
+
data: {
|
|
293
|
+
user_id: user._id,
|
|
294
|
+
email: user.email,
|
|
295
|
+
name: `${user.first_name} ${user.middle_name ? user.middle_name + ' ' : ''}${user.last_name}`,
|
|
296
|
+
device_name: name
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
res.json({
|
|
301
|
+
message: 'Passkey registered successfully',
|
|
302
|
+
success: true,
|
|
303
|
+
device_id,
|
|
304
|
+
device_name: name
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error('Complete passkey registration error', err);
|
|
308
|
+
res.status(500).json({ message: 'Failed to register passkey', error: String(err) });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get user passkeys
|
|
314
|
+
*/
|
|
315
|
+
public async getPasskeys(req: MulterRequest, res: Response): Promise<void> {
|
|
316
|
+
try {
|
|
317
|
+
const { user_id } = req.params;
|
|
318
|
+
|
|
319
|
+
if (!user_id) {
|
|
320
|
+
res.status(400).json({ message: 'User ID is required' });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const passkeys = await PasskeysModel.find({
|
|
325
|
+
user_id,
|
|
326
|
+
deleted_at: null,
|
|
327
|
+
is_active: true
|
|
328
|
+
})
|
|
329
|
+
.select('-__v')
|
|
330
|
+
.sort({ created_at: -1 })
|
|
331
|
+
.lean()
|
|
332
|
+
.exec();
|
|
333
|
+
|
|
334
|
+
res.json({
|
|
335
|
+
message: 'Passkeys retrieved successfully',
|
|
336
|
+
passkeys
|
|
337
|
+
});
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error('Get passkeys error', err);
|
|
340
|
+
res.status(500).json({ message: 'Failed to retrieve passkeys', error: String(err) });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Delete passkey
|
|
346
|
+
*/
|
|
347
|
+
public async deletePasskey(req: MulterRequest, res: Response): Promise<void> {
|
|
348
|
+
try {
|
|
349
|
+
const { passkeyId } = req.params;
|
|
350
|
+
|
|
351
|
+
if (!passkeyId) {
|
|
352
|
+
res.status(400).json({ message: 'Passkey ID is required' });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Soft delete the passkey
|
|
357
|
+
await PasskeysModel.updateOne(
|
|
358
|
+
{ _id: passkeyId },
|
|
359
|
+
{ deleted_at: Date.now(), is_active: false }
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
res.json({
|
|
363
|
+
message: 'Passkey deleted successfully',
|
|
364
|
+
success: true
|
|
365
|
+
});
|
|
366
|
+
} catch (err) {
|
|
367
|
+
console.error('Delete passkey error', err);
|
|
368
|
+
res.status(500).json({ message: 'Failed to delete passkey', error: String(err) });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Start passkey authentication challenge
|
|
374
|
+
*/
|
|
375
|
+
public async authenticatePasskey(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
376
|
+
try {
|
|
377
|
+
const body = req.body || {};
|
|
378
|
+
const { email, device_id } = body;
|
|
379
|
+
|
|
380
|
+
if (!email || !device_id) {
|
|
381
|
+
res.status(400).json({ message: 'Email and device ID are required' });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Find user by email
|
|
386
|
+
const user = await UsersModel.findOne({
|
|
387
|
+
email: email.trim().toLowerCase(),
|
|
388
|
+
deleted_at: null,
|
|
389
|
+
is_active: true
|
|
390
|
+
}).lean().exec();
|
|
391
|
+
|
|
392
|
+
if (!user) {
|
|
393
|
+
res.status(401).json({ message: 'Invalid credentials' });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Find the latest active passkey for this user and device
|
|
398
|
+
const passkey = await PasskeysModel.findOne({
|
|
399
|
+
user_id: user._id,
|
|
400
|
+
device_id,
|
|
401
|
+
is_active: true,
|
|
402
|
+
deleted_at: null
|
|
403
|
+
})
|
|
404
|
+
.sort({ created_at: -1 })
|
|
405
|
+
.lean()
|
|
406
|
+
.exec();
|
|
407
|
+
|
|
408
|
+
if (!passkey) {
|
|
409
|
+
res.status(401).json({ message: 'No passkey found for this device' });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Generate WebAuthn challenge
|
|
414
|
+
const challenge = crypto.randomBytes(32);
|
|
415
|
+
|
|
416
|
+
res.json({
|
|
417
|
+
challenge: challenge.toString('base64'),
|
|
418
|
+
user_id: user._id,
|
|
419
|
+
credential_id: passkey.credential_id
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error('Passkey authentication challenge error', err);
|
|
423
|
+
res.status(500).json({ message: 'Failed to start authentication', error: String(err) });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Validate reset token
|
|
429
|
+
*/
|
|
430
|
+
public async validateResetToken(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
431
|
+
try {
|
|
432
|
+
const body = req.body || {};
|
|
433
|
+
const { token } = body;
|
|
434
|
+
|
|
435
|
+
if (!token) {
|
|
436
|
+
res.status(400).json({ message: 'Token is required' });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Find user with valid reset token
|
|
441
|
+
const user = await UsersModel.findOne({
|
|
442
|
+
reset_token: token,
|
|
443
|
+
reset_token_expires: { $gt: Date.now() },
|
|
444
|
+
deleted_at: null,
|
|
445
|
+
is_active: true
|
|
446
|
+
}).lean().exec();
|
|
447
|
+
|
|
448
|
+
if (!user) {
|
|
449
|
+
res.status(400).json({ message: 'Invalid or expired token' });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
res.json({
|
|
454
|
+
message: 'Token is valid',
|
|
455
|
+
valid: true
|
|
456
|
+
});
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.error('Validate reset token error', err);
|
|
459
|
+
res.status(500).json({ message: 'Failed to validate token', error: String(err) });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Reset password with token
|
|
465
|
+
*/
|
|
466
|
+
public async resetPassword(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
467
|
+
try {
|
|
468
|
+
const body = req.body || {};
|
|
469
|
+
const { token, new_password } = body;
|
|
470
|
+
|
|
471
|
+
if (!token || !new_password) {
|
|
472
|
+
res.status(400).json({ message: 'Token and new password are required' });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Find user with valid reset token
|
|
477
|
+
const user = await UsersModel.findOne({
|
|
478
|
+
reset_token: token,
|
|
479
|
+
reset_token_expires: { $gt: Date.now() },
|
|
480
|
+
deleted_at: null,
|
|
481
|
+
is_active: true
|
|
482
|
+
}).exec();
|
|
483
|
+
|
|
484
|
+
if (!user) {
|
|
485
|
+
res.status(400).json({ message: 'Invalid or expired token' });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Hash new password
|
|
490
|
+
const hashedPassword = await bcrypt.hash(new_password, 10);
|
|
491
|
+
|
|
492
|
+
// Update password and clear reset token
|
|
493
|
+
await UsersModel.updateOne(
|
|
494
|
+
{ _id: user._id },
|
|
495
|
+
{
|
|
496
|
+
password: hashedPassword,
|
|
497
|
+
reset_token: null,
|
|
498
|
+
reset_token_expires: null,
|
|
499
|
+
updated_at: Date.now()
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// Add to email queue for password changed notification
|
|
504
|
+
await getEmailQueue().add('send-email', {
|
|
505
|
+
type: 'password_changed',
|
|
506
|
+
data: {
|
|
507
|
+
user_id: user._id,
|
|
508
|
+
email: user.email,
|
|
509
|
+
name: `${user.first_name} ${user.middle_name ? user.middle_name + ' ' : ''}${user.last_name}`
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
res.json({
|
|
514
|
+
message: 'Password reset successfully',
|
|
515
|
+
success: true
|
|
516
|
+
});
|
|
517
|
+
} catch (err) {
|
|
518
|
+
console.error('Reset password error', err);
|
|
519
|
+
res.status(500).json({ message: 'Failed to reset password', error: String(err) });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Complete passkey authentication
|
|
525
|
+
*/
|
|
526
|
+
public async verifyPasskey(req: MulterRequest & { body?: Record<string, any> }, res: Response): Promise<void> {
|
|
527
|
+
try {
|
|
528
|
+
const body = req.body || {};
|
|
529
|
+
const { email, device_id, credential_id, authenticator_data } = body;
|
|
530
|
+
|
|
531
|
+
if (!email || !device_id || !credential_id || !authenticator_data) {
|
|
532
|
+
res.status(400).json({ message: 'All authentication data is required' });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Find user by email
|
|
537
|
+
const user = await UsersModel.findOne({
|
|
538
|
+
email: email.trim().toLowerCase(),
|
|
539
|
+
deleted_at: null,
|
|
540
|
+
is_active: true
|
|
541
|
+
}).lean().exec();
|
|
542
|
+
|
|
543
|
+
if (!user) {
|
|
544
|
+
res.status(401).json({ message: 'Invalid credentials' });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Find the passkey
|
|
549
|
+
const passkey = await PasskeysModel.findOne({
|
|
550
|
+
user_id: user._id,
|
|
551
|
+
credential_id,
|
|
552
|
+
device_id,
|
|
553
|
+
is_active: true,
|
|
554
|
+
deleted_at: null
|
|
555
|
+
}).lean().exec();
|
|
556
|
+
|
|
557
|
+
if (!passkey) {
|
|
558
|
+
res.status(401).json({ message: 'Invalid passkey' });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// For now, trust the client-side WebAuthn verification
|
|
563
|
+
// The authenticator already verified the user locally with biometrics
|
|
564
|
+
// We'll implement full cryptographic signature verification in a future update
|
|
565
|
+
|
|
566
|
+
// Update last used timestamp
|
|
567
|
+
await PasskeysModel.updateOne(
|
|
568
|
+
{ _id: passkey._id },
|
|
569
|
+
{ last_used_at: Date.now(), updated_at: Date.now() }
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
// Generate JWT token
|
|
573
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
574
|
+
if (!jwtSecret) {
|
|
575
|
+
throw new Error('JWT_SECRET environment variable is required');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const jwt = require('jsonwebtoken');
|
|
579
|
+
const token = jwt.sign(
|
|
580
|
+
{
|
|
581
|
+
userId: user._id,
|
|
582
|
+
email: user.email,
|
|
583
|
+
role: user.role
|
|
584
|
+
},
|
|
585
|
+
jwtSecret,
|
|
586
|
+
{ expiresIn: '24h' }
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
res.json({
|
|
590
|
+
message: 'Passkey authentication successful',
|
|
591
|
+
token,
|
|
592
|
+
user: {
|
|
593
|
+
id: user._id,
|
|
594
|
+
email: user.email,
|
|
595
|
+
first_name: user.first_name,
|
|
596
|
+
middle_name: user.middle_name,
|
|
597
|
+
last_name: user.last_name,
|
|
598
|
+
full_name: `${user.first_name} ${user.last_name}`,
|
|
599
|
+
role: user.role,
|
|
600
|
+
employee_id: user.employee_id,
|
|
601
|
+
is_active: user.is_active
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
} catch (err) {
|
|
605
|
+
console.error('Passkey verification error', err);
|
|
606
|
+
res.status(500).json({ message: 'Passkey authentication failed', error: String(err) });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|