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