@ciscode/authentication-kit 1.0.43 → 1.1.1

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.
@@ -1,566 +0,0 @@
1
- const passport = require('../config/passport.config');
2
- const jwt = require('jsonwebtoken');
3
- const bcrypt = require('bcryptjs');
4
- const jwksClient = require('jwks-rsa');
5
- const axios = require('axios'); // ← for Google code/idToken exchange
6
- const User = require('../models/user.model');
7
- const Client = require('../models/client.model');
8
- const Role = require('../models/role.model');
9
- const getMillisecondsFromExpiry = require('../utils/helper').getMillisecondsFromExpiry;
10
-
11
- /* ──────────────────────────────────────────────────────────────────────────────
12
- * Microsoft ID token verification (for MSAL mobile token exchange)
13
- * ─────────────────────────────────────────────────────────────────────────── */
14
- const TENANT_ID = process.env.MICROSOFT_TENANT_ID || 'common';
15
- const MSAL_MOBILE_CLIENT_ID = process.env.MSAL_MOBILE_CLIENT_ID; // MUST equal msal_config.json client_id
16
-
17
- const msJwks = jwksClient({
18
- jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys',
19
- cache: true,
20
- rateLimit: true,
21
- jwksRequestsPerMinute: 5,
22
- });
23
-
24
- function verifyMicrosoftIdToken(idToken) {
25
- return new Promise((resolve, reject) => {
26
- const getKey = (header, cb) => {
27
- msJwks
28
- .getSigningKey(header.kid)
29
- .then((k) => cb(null, k.getPublicKey()))
30
- .catch(cb);
31
- };
32
-
33
- jwt.verify(
34
- idToken,
35
- getKey,
36
- { algorithms: ['RS256'], audience: MSAL_MOBILE_CLIENT_ID },
37
- (err, payload) => (err ? reject(err) : resolve(payload))
38
- );
39
- });
40
- }
41
-
42
- /* ──────────────────────────────────────────────────────────────────────────────
43
- * Helpers
44
- * ─────────────────────────────────────────────────────────────────────────── */
45
-
46
- // Issue tokens + cookie + JSON (for pure web flows)
47
- async function issueTokensAndRespond(principal, res) {
48
- const roleDocs = await Role.find({ _id: { $in: principal.roles } })
49
- .select('name permissions -_id')
50
- .lean();
51
-
52
- const roles = roleDocs.map((r) => r.name);
53
- const permissions = Array.from(new Set(roleDocs.flatMap((r) => r.permissions)));
54
-
55
- const accessTTL = process.env.JWT_ACCESS_TOKEN_EXPIRES_IN || '15m';
56
- const refreshTTL = process.env.JWT_REFRESH_TOKEN_EXPIRES_IN || '7d';
57
-
58
- const payload = {
59
- id: principal._id,
60
- email: principal.email,
61
- tenantId: principal.tenantId, // may be undefined for Clients; that's fine
62
- roles,
63
- permissions,
64
- };
65
-
66
- const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: accessTTL });
67
- const refreshToken = jwt.sign({ id: principal._id }, process.env.JWT_REFRESH_SECRET, { expiresIn: refreshTTL });
68
-
69
- // persist refresh token
70
- principal.refreshToken = refreshToken;
71
- try {
72
- await principal.save();
73
- } catch (e) {
74
- console.error('❌ Error saving refreshToken:', e);
75
- }
76
-
77
- const isProd = process.env.NODE_ENV === 'production';
78
- res.cookie('refreshToken', refreshToken, {
79
- httpOnly: true,
80
- secure: isProd,
81
- sameSite: isProd ? 'none' : 'lax',
82
- path: '/',
83
- maxAge: getMillisecondsFromExpiry(refreshTTL),
84
- });
85
-
86
- return res.status(200).json({ accessToken, refreshToken });
87
- }
88
-
89
- // Decide mobile deep link vs web JSON/cookie (for OAuth callbacks)
90
- async function respondWebOrMobile(req, res, principal) {
91
- const roleDocs = await Role.find({ _id: { $in: principal.roles } })
92
- .select('name permissions -_id')
93
- .lean();
94
-
95
- const roles = roleDocs.map((r) => r.name);
96
- const permissions = Array.from(new Set(roleDocs.flatMap((r) => r.permissions)));
97
-
98
- const accessTTL = process.env.JWT_ACCESS_TOKEN_EXPIRES_IN || '15m';
99
- const refreshTTL = process.env.JWT_REFRESH_TOKEN_EXPIRES_IN || '7d';
100
-
101
- const payload = {
102
- id: principal._id,
103
- email: principal.email,
104
- tenantId: principal.tenantId,
105
- roles,
106
- permissions,
107
- };
108
-
109
- const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: accessTTL });
110
- const refreshToken = jwt.sign({ id: principal._id }, process.env.JWT_REFRESH_SECRET, { expiresIn: refreshTTL });
111
-
112
- // always persist the refresh token in DB
113
- principal.refreshToken = refreshToken;
114
- try { await principal.save(); } catch (e) { console.error('❌ Saving refreshToken failed:', e); }
115
-
116
- // try to decode deep link from state
117
- let mobileRedirect;
118
- if (req.query.state) {
119
- try {
120
- const decoded = JSON.parse(Buffer.from(req.query.state, 'base64url').toString('utf8'));
121
- mobileRedirect = decoded.redirect; // e.g. restosoft://auth/google/callback
122
- } catch (_) {}
123
- }
124
-
125
- if (mobileRedirect) {
126
- // MOBILE: redirect back to app with tokens as query params
127
- const url = new URL(mobileRedirect);
128
- url.searchParams.set('accessToken', accessToken);
129
- url.searchParams.set('refreshToken', refreshToken);
130
- return res.redirect(302, url.toString());
131
- }
132
-
133
- // WEB: set cookie + return JSON
134
- const isProd = process.env.NODE_ENV === 'production';
135
- res.cookie('refreshToken', refreshToken, {
136
- httpOnly: true,
137
- secure: isProd,
138
- sameSite: isProd ? 'none' : 'lax',
139
- path: '/',
140
- maxAge: getMillisecondsFromExpiry(refreshTTL),
141
- });
142
-
143
- return res.status(200).json({ accessToken, refreshToken });
144
- }
145
-
146
- /* ──────────────────────────────────────────────────────────────────────────────
147
- * Client Registration
148
- * ─────────────────────────────────────────────────────────────────────────── */
149
- const registerClient = async (req, res) => {
150
- try {
151
- const { email, password, name, roles = [] } = req.body;
152
- if (!email || !password) {
153
- return res.status(400).json({ message: 'Email and password are required.' });
154
- }
155
- if (await Client.findOne({ email })) {
156
- return res.status(409).json({ message: 'Email already in use.' });
157
- }
158
- const salt = await bcrypt.genSalt(10);
159
- const hashed = await bcrypt.hash(password, salt);
160
- const client = new Client({ email, password: hashed, name, roles });
161
- await client.save();
162
- return res.status(201).json({
163
- id: client._id,
164
- email: client.email,
165
- name: client.name,
166
- roles: client.roles,
167
- });
168
- } catch (err) {
169
- console.error('❌ registerClient error:', err);
170
- return res.status(500).json({ message: 'Server error.' });
171
- }
172
- };
173
-
174
- /* ──────────────────────────────────────────────────────────────────────────────
175
- * Client Login (local)
176
- * ─────────────────────────────────────────────────────────────────────────── */
177
- const clientLogin = async (req, res) => {
178
- try {
179
- const { email, password } = req.body;
180
- if (!email || !password) {
181
- return res.status(400).json({ message: 'Email and password are required.' });
182
- }
183
- const client = await Client.findOne({ email })
184
- .select('+password')
185
- .populate('roles', 'name permissions');
186
- if (!client) {
187
- return res.status(400).json({ message: 'Incorrect email.' });
188
- }
189
- const match = await bcrypt.compare(password, client.password);
190
- if (!match) {
191
- return res.status(400).json({ message: 'Incorrect password.' });
192
- }
193
-
194
- return issueTokensAndRespond(client, res);
195
- } catch (err) {
196
- console.error('❌ clientLogin error:', err);
197
- return res.status(500).json({ message: 'Server error.' });
198
- }
199
- };
200
-
201
- /* ──────────────────────────────────────────────────────────────────────────────
202
- * Local Login (Users)
203
- * ─────────────────────────────────────────────────────────────────────────── */
204
- const localLogin = (req, res, next) => {
205
- passport.authenticate('local', { session: false }, async (err, user, info) => {
206
- if (err) return next(err);
207
- if (!user) return res.status(400).json({ message: info?.message || 'Invalid credentials.' });
208
-
209
- try {
210
- return issueTokensAndRespond(user, res);
211
- } catch (e) {
212
- console.error('❌ localLogin error:', e);
213
- return res.status(500).json({ message: 'Server error.' });
214
- }
215
- })(req, res, next);
216
- };
217
-
218
- /* ──────────────────────────────────────────────────────────────────────────────
219
- * Microsoft (Users) — OAuth start/callback
220
- * ─────────────────────────────────────────────────────────────────────────── */
221
- const microsoftLogin = (req, res, next) => {
222
- const redirect = req.query.redirect;
223
- const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined;
224
-
225
- return passport.authenticate('azure_ad_oauth2', {
226
- session: false,
227
- state,
228
- })(req, res, next);
229
- };
230
-
231
- const microsoftCallback = (req, res, next) => {
232
- passport.authenticate('azure_ad_oauth2', { session: false }, async (err, user) => {
233
- if (err) return next(err);
234
- if (!user) return res.status(400).json({ message: 'Microsoft authentication failed.' });
235
- return respondWebOrMobile(req, res, user);
236
- })(req, res, next);
237
- };
238
-
239
- /* ──────────────────────────────────────────────────────────────────────────────
240
- * Microsoft ID Token → Local JWTs (MSAL mobile token exchange)
241
- * POST /api/auth/microsoft/exchange { idToken }
242
- * ─────────────────────────────────────────────────────────────────────────── */
243
- const microsoftExchange = async (req, res) => {
244
- try {
245
- if (!MSAL_MOBILE_CLIENT_ID) {
246
- console.error('❌ MSAL_MOBILE_CLIENT_ID is not set in environment.');
247
- return res.status(500).json({ message: 'Server misconfiguration.' });
248
- }
249
-
250
- const { idToken } = req.body || {};
251
- if (!idToken) {
252
- return res.status(400).json({ message: 'idToken is required.' });
253
- }
254
-
255
- let ms;
256
- try {
257
- ms = await verifyMicrosoftIdToken(idToken);
258
- } catch (e) {
259
- console.error('❌ ID token verify failed:', e.message || e);
260
- return res.status(401).json({ message: 'Invalid Microsoft ID token.' });
261
- }
262
-
263
- const email = ms.preferred_username || ms.email;
264
- const name = ms.name;
265
- const tid = ms.tid;
266
-
267
- if (TENANT_ID && TENANT_ID !== 'common' && tid && tid !== TENANT_ID) {
268
- return res.status(401).json({ message: 'Tenant mismatch.' });
269
- }
270
- if (!email) {
271
- return res.status(400).json({ message: 'Email claim missing in Microsoft ID token.' });
272
- }
273
-
274
- const microsoftId = ms.oid || ms.sub;
275
- let user = await User.findOne({ email });
276
-
277
- if (!user) {
278
- user = new User({
279
- email,
280
- name,
281
- tenantId: tid || TENANT_ID,
282
- microsoftId,
283
- roles: [],
284
- status: 'active',
285
- });
286
- await user.save();
287
- } else {
288
- let changed = false;
289
- if (!user.microsoftId) { user.microsoftId = microsoftId; changed = true; }
290
- if (!user.tenantId) { user.tenantId = tid || TENANT_ID; changed = true; }
291
- if (changed) await user.save();
292
- }
293
-
294
- return issueTokensAndRespond(user, res);
295
- } catch (e) {
296
- console.error('💥 microsoftExchange error:', e);
297
- return res.status(500).json({ message: 'Server error.' });
298
- }
299
- };
300
-
301
- /* ──────────────────────────────────────────────────────────────────────────────
302
- * Google OAuth — Users & Clients (browser redirect flow)
303
- * ─────────────────────────────────────────────────────────────────────────── */
304
- const googleUserLogin = (req, res, next) => {
305
- const redirect = req.query.redirect;
306
- const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined;
307
- return passport.authenticate('google-user', { session: false, scope: ['profile', 'email'], state })(req, res, next);
308
- };
309
-
310
- const googleUserCallback = (req, res, next) => {
311
- passport.authenticate('google-user', { session: false }, async (err, user) => {
312
- if (err) return next(err);
313
- if (!user) return res.status(400).json({ message: 'Google authentication failed.' });
314
- return respondWebOrMobile(req, res, user);
315
- })(req, res, next);
316
- };
317
-
318
- const googleClientLogin = (req, res, next) => {
319
- const redirect = req.query.redirect;
320
- const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined;
321
- return passport.authenticate('google-client', { session: false, scope: ['profile', 'email'], state })(req, res, next);
322
- };
323
-
324
- const googleClientCallback = (req, res, next) => {
325
- passport.authenticate('google-client', { session: false }, async (err, client) => {
326
- if (err) return next(err);
327
- if (!client) return res.status(400).json({ message: 'Google authentication failed.' });
328
- return respondWebOrMobile(req, res, client);
329
- })(req, res, next);
330
- };
331
-
332
- /* ──────────────────────────────────────────────────────────────────────────────
333
- * Google Token Exchange (mobile-friendly)
334
- * POST /api/auth/google/exchange
335
- * Body:
336
- * - Preferred: { code: "<serverAuthCode>", type: "user"|"client" }
337
- * - Optional: { idToken: "<google id_token>", type: "user"|"client" }
338
- * ─────────────────────────────────────────────────────────────────────────── */
339
- const googleExchange = async (req, res) => {
340
- try {
341
- let { code, idToken, type = 'user' } = req.body || {};
342
- if (!['user', 'client'].includes(type)) {
343
- return res.status(400).json({ message: 'invalid type; must be "user" or "client"' });
344
- }
345
-
346
- let email, name, googleId;
347
-
348
- if (code) {
349
- // Exchange server auth code for tokens using "postmessage" (for installed apps)
350
- const tokenResp = await axios.post('https://oauth2.googleapis.com/token', {
351
- code,
352
- client_id: process.env.GOOGLE_CLIENT_ID,
353
- client_secret: process.env.GOOGLE_CLIENT_SECRET,
354
- redirect_uri: 'postmessage',
355
- grant_type: 'authorization_code',
356
- });
357
-
358
- const { access_token } = tokenResp.data || {};
359
- if (!access_token) {
360
- return res.status(401).json({ message: 'Failed to exchange code with Google.' });
361
- }
362
-
363
- // Get profile with access token
364
- const profileResp = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
365
- headers: { Authorization: `Bearer ${access_token}` },
366
- });
367
- email = profileResp.data?.email;
368
- name = profileResp.data?.name || profileResp.data?.given_name || '';
369
- googleId = profileResp.data?.id;
370
- } else if (idToken) {
371
- // Verify ID token via Google tokeninfo endpoint
372
- const verifyResp = await axios.get('https://oauth2.googleapis.com/tokeninfo', {
373
- params: { id_token: idToken },
374
- });
375
- email = verifyResp.data?.email;
376
- name = verifyResp.data?.name || '';
377
- googleId = verifyResp.data?.sub;
378
- } else {
379
- return res.status(400).json({ message: 'code or idToken is required' });
380
- }
381
-
382
- if (!email) return res.status(400).json({ message: 'Google profile missing email.' });
383
-
384
- const Model = type === 'user' ? User : Client;
385
-
386
- // Find or create principal
387
- let principal = await Model.findOne({ $or: [{ email }, { googleId }] });
388
- if (!principal) {
389
- principal = new Model(
390
- type === 'user'
391
- ? { email, name, googleId, tenantId: process.env.DEFAULT_TENANT_ID, roles: [], status: 'active' }
392
- : { email, name, googleId, roles: [] }
393
- );
394
- await principal.save();
395
- } else if (!principal.googleId) {
396
- principal.googleId = googleId;
397
- if (type === 'user' && !principal.tenantId) principal.tenantId = process.env.DEFAULT_TENANT_ID;
398
- await principal.save();
399
- }
400
-
401
- // Issue your tokens and cookie (JSON response, mobile-friendly)
402
- const roleDocs = await Role.find({ _id: { $in: principal.roles } })
403
- .select('name permissions -_id').lean();
404
- const roles = roleDocs.map((r) => r.name);
405
- const permissions = Array.from(new Set(roleDocs.flatMap((r) => r.permissions)));
406
- const accessTTL = process.env.JWT_ACCESS_TOKEN_EXPIRES_IN || '15m';
407
- const refreshTTL = process.env.JWT_REFRESH_TOKEN_EXPIRES_IN || '7d';
408
-
409
- const payload = { id: principal._id, email: principal.email, tenantId: principal.tenantId, roles, permissions };
410
- const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: accessTTL });
411
- const refreshToken = jwt.sign({ id: principal._id }, process.env.JWT_REFRESH_SECRET, { expiresIn: refreshTTL });
412
-
413
- principal.refreshToken = refreshToken;
414
- try { await principal.save(); } catch (e) { console.error('❌ Saving refreshToken failed:', e); }
415
-
416
- const isProd = process.env.NODE_ENV === 'production';
417
- res.cookie('refreshToken', refreshToken, {
418
- httpOnly: true,
419
- secure: isProd,
420
- sameSite: isProd ? 'none' : 'lax',
421
- path: '/',
422
- maxAge: getMillisecondsFromExpiry(refreshTTL),
423
- });
424
-
425
- return res.status(200).json({ accessToken, refreshToken });
426
- } catch (err) {
427
- console.error('❌ googleExchange error:', err?.response?.data || err.message || err);
428
- return res.status(500).json({ message: 'Server error during Google exchange.' });
429
- }
430
- };
431
-
432
- /* ──────────────────────────────────────────────────────────────────────────────
433
- * Facebook OAuth — Users & Clients
434
- * ─────────────────────────────────────────────────────────────────────────── */
435
- const facebookUserLogin = (req, res, next) => {
436
- const redirect = req.query.redirect;
437
- const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined;
438
- return passport.authenticate('facebook-user', { session: false, scope: ['email'], state })(req, res, next);
439
- };
440
-
441
- const facebookUserCallback = (req, res, next) => {
442
- passport.authenticate('facebook-user', { session: false }, async (err, user) => {
443
- if (err) return next(err);
444
- if (!user) return res.status(400).json({ message: 'Facebook authentication failed.' });
445
- return respondWebOrMobile(req, res, user);
446
- })(req, res, next);
447
- };
448
-
449
- const facebookClientLogin = (req, res, next) => {
450
- const redirect = req.query.redirect;
451
- const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined;
452
- return passport.authenticate('facebook-client', { session: false, scope: ['email'], state })(req, res, next);
453
- };
454
-
455
- const facebookClientCallback = (req, res, next) => {
456
- passport.authenticate('facebook-client', { session: false }, async (err, client) => {
457
- if (err) return next(err);
458
- if (!client) return res.status(400).json({ message: 'Facebook authentication failed.' });
459
- return respondWebOrMobile(req, res, client);
460
- })(req, res, next);
461
- };
462
-
463
- /* ──────────────────────────────────────────────────────────────────────────────
464
- * Microsoft (Clients) — OAuth start/callback
465
- * ─────────────────────────────────────────────────────────────────────────── */
466
- const microsoftClientLogin = (req, res, next) => {
467
- const redirect = req.query.redirect;
468
- const state = redirect ? Buffer.from(JSON.stringify({ redirect }), 'utf8').toString('base64url') : undefined;
469
- return passport.authenticate('azure_ad_oauth2_client', { session: false, state })(req, res, next);
470
- };
471
-
472
- const microsoftClientCallback = (req, res, next) => {
473
- passport.authenticate('azure_ad_oauth2_client', { session: false }, async (err, client) => {
474
- if (err) return next(err);
475
- if (!client) return res.status(400).json({ message: 'Microsoft authentication failed.' });
476
- return respondWebOrMobile(req, res, client);
477
- })(req, res, next);
478
- };
479
-
480
- /* ──────────────────────────────────────────────────────────────────────────────
481
- * Refresh Token → new Access Token (supports User or Client)
482
- * ─────────────────────────────────────────────────────────────────────────── */
483
- const refreshToken = async (req, res) => {
484
- try {
485
- const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;
486
- if (!refreshToken) {
487
- return res.status(401).json({ message: 'Refresh token missing.' });
488
- }
489
-
490
- let decoded;
491
- try {
492
- decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
493
- } catch (err) {
494
- const msg = err.name === 'TokenExpiredError' ? 'Refresh token expired.' : 'Invalid refresh token.';
495
- return res.status(401).json({ message: msg });
496
- }
497
-
498
- // Try User first; if not found, try Client
499
- let principal = await User.findById(decoded.id);
500
- let principalType = 'user';
501
- if (!principal) {
502
- principal = await Client.findById(decoded.id);
503
- principalType = 'client';
504
- }
505
- if (!principal) return res.status(401).json({ message: 'Account not found.' });
506
-
507
- if (principal.refreshToken !== refreshToken) {
508
- return res.status(401).json({ message: 'Refresh token mismatch.' });
509
- }
510
-
511
- const roleDocs = await Role.find({ _id: { $in: principal.roles } })
512
- .select('name permissions -_id').lean();
513
- const roles = roleDocs.map((r) => r.name);
514
- const permissions = Array.from(new Set(roleDocs.flatMap((r) => r.permissions)));
515
-
516
- const payload = {
517
- id: principal._id,
518
- email: principal.email,
519
- tenantId: principal.tenantId, // undefined for Client is fine
520
- roles,
521
- permissions,
522
- };
523
-
524
- const accessTokenExpiresIn = process.env.JWT_ACCESS_TOKEN_EXPIRES_IN || '15m';
525
- const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: accessTokenExpiresIn });
526
-
527
- return res.status(200).json({ accessToken, type: principalType });
528
- } catch (error) {
529
- console.error('💥 [Refresh] Unexpected error:', error);
530
- return res.status(500).json({ message: 'Server error during token refresh.' });
531
- }
532
- };
533
-
534
- module.exports = {
535
- // Local + registration
536
- localLogin,
537
- registerClient,
538
- clientLogin,
539
-
540
- // Microsoft (Users)
541
- microsoftLogin,
542
- microsoftCallback,
543
- microsoftExchange,
544
-
545
- // Microsoft (Clients)
546
- microsoftClientLogin,
547
- microsoftClientCallback,
548
-
549
- // Google (browser)
550
- googleUserLogin,
551
- googleUserCallback,
552
- googleClientLogin,
553
- googleClientCallback,
554
-
555
- // Google (mobile exchange)
556
- googleExchange,
557
-
558
- // Facebook
559
- facebookUserLogin,
560
- facebookUserCallback,
561
- facebookClientLogin,
562
- facebookClientCallback,
563
-
564
- // Refresh
565
- refreshToken,
566
- };
@@ -1,127 +0,0 @@
1
- const crypto = require('crypto');
2
- const bcrypt = require('bcryptjs');
3
- const nodemailer = require('nodemailer');
4
- const User = require('../models/user.model');
5
- const Client = require('../models/client.model');
6
-
7
- // Utility to choose the right model
8
- const getModel = (type) => {
9
- switch (type) {
10
- case 'user':
11
- return User;
12
- case 'client':
13
- return Client;
14
- default:
15
- throw new Error('Invalid account type');
16
- }
17
- };
18
-
19
- // ─── Request Password Reset ────────────────────────────────
20
- const requestPasswordReset = async (req, res) => {
21
- try {
22
- const { email, type } = req.body;
23
-
24
- if (!email || !type) {
25
- return res.status(400).json({ message: 'Email and type are required.' });
26
- }
27
-
28
- const Model = getModel(type);
29
- const account = await Model.findOne({ email });
30
-
31
- if (!account) {
32
- // Security: don't reveal existence
33
- return res.status(200).json({
34
- message:
35
- 'If that email address is in our system, a password reset link has been sent.'
36
- });
37
- }
38
-
39
- const token = crypto.randomBytes(20).toString('hex');
40
- account.resetPasswordToken = token;
41
- account.resetPasswordExpires = Date.now() + 3600000; // 1h
42
-
43
- // Use updateOne if tenantId or required fields cause validation issues
44
- await account.save();
45
-
46
- const transporter = nodemailer.createTransport({
47
- host: process.env.SMTP_HOST,
48
- port: parseInt(process.env.SMTP_PORT),
49
- secure: process.env.SMTP_SECURE === 'true',
50
- auth: {
51
- user: process.env.SMTP_USER,
52
- pass: process.env.SMTP_PASS
53
- }
54
- });
55
-
56
- const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}&type=${type}`;
57
-
58
- const mailOptions = {
59
- from: process.env.FROM_EMAIL,
60
- to: account.email,
61
- subject: 'Password Reset',
62
- text: `You are receiving this email because you (or someone else) requested a password reset.
63
- Please click the link below, or paste it into your browser:
64
- ${resetUrl}
65
-
66
- If you did not request this, please ignore this email.
67
- This link will expire in 1 hour.`
68
- };
69
-
70
- await transporter.sendMail(mailOptions);
71
-
72
- return res.status(200).json({
73
- message:
74
- 'If that email address is in our system, a password reset link has been sent.'
75
- });
76
- } catch (error) {
77
- console.error('Error in requestPasswordReset:', error);
78
- return res
79
- .status(500)
80
- .json({ message: 'Server error', error: error.message });
81
- }
82
- };
83
-
84
- // ─── Reset Password ────────────────────────────────
85
- const resetPassword = async (req, res) => {
86
- try {
87
- const { token, newPassword, type } = req.body;
88
-
89
- if (!token || !newPassword || !type) {
90
- return res
91
- .status(400)
92
- .json({ message: 'Token, new password, and type are required.' });
93
- }
94
-
95
- const Model = getModel(type);
96
-
97
- const account = await Model.findOne({
98
- resetPasswordToken: token,
99
- resetPasswordExpires: { $gt: Date.now() }
100
- });
101
-
102
- if (!account) {
103
- return res.status(400).json({ message: 'Invalid or expired token.' });
104
- }
105
-
106
- const salt = await bcrypt.genSalt(10);
107
- account.password = await bcrypt.hash(newPassword, salt);
108
- account.resetPasswordToken = undefined;
109
- account.resetPasswordExpires = undefined;
110
-
111
- await account.save();
112
-
113
- return res
114
- .status(200)
115
- .json({ message: 'Password has been reset successfully.' });
116
- } catch (error) {
117
- console.error('Error in resetPassword:', error);
118
- return res
119
- .status(500)
120
- .json({ message: 'Server error', error: error.message });
121
- }
122
- };
123
-
124
- module.exports = {
125
- requestPasswordReset,
126
- resetPassword
127
- };