@airoom/nextmin-node 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +151 -0
  2. package/dist/api/apiRouter.d.ts +20 -0
  3. package/dist/api/apiRouter.js +122 -7
  4. package/dist/api/router/mountCrudRoutes.js +69 -18
  5. package/dist/api/router/setupAuthRoutes.js +475 -38
  6. package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
  7. package/dist/api/router/setupChatWidgetRoutes.js +207 -0
  8. package/dist/api/router/setupFileRoutes.js +221 -16
  9. package/dist/api/router/utils.d.ts +2 -1
  10. package/dist/api/router/utils.js +8 -6
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +145 -64
  13. package/dist/database/DatabaseAdapter.d.ts +1 -0
  14. package/dist/database/NMAdapter.d.ts +6 -0
  15. package/dist/database/NMAdapter.js +437 -86
  16. package/dist/database/QueryEngine.js +7 -4
  17. package/dist/files/FileStorageAdapter.d.ts +1 -0
  18. package/dist/files/LocalFileStorageAdapter.d.ts +1 -0
  19. package/dist/files/LocalFileStorageAdapter.js +25 -3
  20. package/dist/files/S3FileStorageAdapter.d.ts +3 -1
  21. package/dist/files/S3FileStorageAdapter.js +72 -11
  22. package/dist/files/filename.js +6 -4
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +3 -1
  25. package/dist/models/BaseModel.d.ts +4 -0
  26. package/dist/policy/authorize.d.ts +1 -1
  27. package/dist/policy/authorize.js +64 -13
  28. package/dist/schemas/Users.json +20 -10
  29. package/dist/services/IndexingService.d.ts +24 -0
  30. package/dist/services/IndexingService.js +555 -0
  31. package/dist/services/LocalAIService.d.ts +34 -0
  32. package/dist/services/LocalAIService.js +471 -0
  33. package/dist/services/OpenAIService.d.ts +16 -0
  34. package/dist/services/OpenAIService.js +586 -0
  35. package/dist/utils/SchemaLoader.d.ts +1 -1
  36. package/dist/utils/SchemaLoader.js +40 -4
  37. package/package.json +12 -4
@@ -6,37 +6,306 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setupAuthRoutes = setupAuthRoutes;
7
7
  const bcrypt_1 = __importDefault(require("bcrypt"));
8
8
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
+ const keyv_1 = __importDefault(require("keyv"));
10
+ const Events_1 = require("../../utils/Events");
11
+ const otpStore = new keyv_1.default({ ttl: 300000 }); // 5 minutes TTL
12
+ function getAllowedAuthServices(ctx) {
13
+ let authServices = [];
14
+ const usersSchema = ctx.getSchema('users');
15
+ if (usersSchema?.authServices) {
16
+ authServices = [...usersSchema.authServices];
17
+ }
18
+ const schemas = ctx.liveSchemas || {};
19
+ for (const s of Object.values(schemas)) {
20
+ if (s.extends === 'Users' && s.authServices && Array.isArray(s.authServices)) {
21
+ for (const service of s.authServices) {
22
+ if (!authServices.includes(service)) {
23
+ authServices.push(service);
24
+ }
25
+ }
26
+ }
27
+ }
28
+ if (authServices.length === 0) {
29
+ authServices = ['email', 'username'];
30
+ }
31
+ return authServices;
32
+ }
33
+ function getPhoneVerificationSetting(ctx, roleIdOrName) {
34
+ const usersSchema = ctx.getSchema('users');
35
+ let phoneVerification = !!usersSchema?.phoneVerification;
36
+ if (roleIdOrName) {
37
+ const schemas = ctx.liveSchemas || {};
38
+ const normalizedRole = String(roleIdOrName).trim().toLowerCase();
39
+ for (const s of Object.values(schemas)) {
40
+ const isSubclass = s.extends === 'Users';
41
+ const nameMatches = s.modelName.toLowerCase() === normalizedRole ||
42
+ s.modelName.toLowerCase() === normalizedRole + 's' ||
43
+ (normalizedRole.endsWith('s') && s.modelName.toLowerCase() === normalizedRole.slice(0, -1));
44
+ if (isSubclass && nameMatches) {
45
+ if (s.phoneVerification !== undefined) {
46
+ return !!s.phoneVerification;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ if (!phoneVerification) {
52
+ const schemas = ctx.liveSchemas || {};
53
+ for (const s of Object.values(schemas)) {
54
+ if (s.extends === 'Users' && s.phoneVerification) {
55
+ return true;
56
+ }
57
+ }
58
+ }
59
+ return phoneVerification;
60
+ }
9
61
  function setupAuthRoutes(ctx) {
10
62
  const basePath = '/auth/users';
11
- // REGISTER
12
- ctx.router.post(`${basePath}/register`, ctx.apiKeyMiddleware, async (req, res) => {
63
+ // SEND OTP
64
+ ctx.router.post(`${basePath}/send-otp`, ctx.apiKeyMiddleware, async (req, res) => {
13
65
  try {
14
- let { email, username, password, ...rest } = req.body;
15
- if (String(password).length < 8) {
16
- return res
17
- .status(400)
18
- .json({
66
+ const phoneVerification = getPhoneVerificationSetting(ctx);
67
+ if (!phoneVerification) {
68
+ return res.status(400).json({
19
69
  error: true,
20
- message: 'Password must be at least 8 characters long',
70
+ message: 'Phone verification is not enabled in schema.',
21
71
  });
22
72
  }
73
+ const { phone, purpose = 'register' } = req.body;
74
+ if (!phone || !String(phone).trim()) {
75
+ return res.status(400).json({
76
+ error: true,
77
+ message: 'Phone number is required',
78
+ });
79
+ }
80
+ const cleanPhone = String(phone).trim();
81
+ const userModel = ctx.getModel('users');
82
+ // Find existing user by phone OR username (since username can be the phone number)
83
+ const existingByPhone = await userModel.read({ phone: cleanPhone }, 1, 0, true);
84
+ const existingByUsername = await userModel.read({ username: cleanPhone }, 1, 0, true);
85
+ const userExists = (existingByPhone && existingByPhone.length > 0) || (existingByUsername && existingByUsername.length > 0);
86
+ if (purpose === 'login') {
87
+ if (!userExists) {
88
+ return res.status(400).json({
89
+ error: true,
90
+ message: 'Phone number not registered',
91
+ });
92
+ }
93
+ }
94
+ else {
95
+ if (userExists) {
96
+ return res.status(400).json({
97
+ error: true,
98
+ message: 'Phone number already registered',
99
+ });
100
+ }
101
+ }
102
+ const otp = Math.floor(100000 + Math.random() * 900000).toString();
103
+ await otpStore.set(cleanPhone, otp);
104
+ Events_1.events.emitEvent('auth:otp:send', { phone: cleanPhone, otp });
105
+ // OTP Logging logic for development & FORCE_LOG_OTP
106
+ const isDev = process.env.APP_MODE?.toLowerCase() !== 'production' && process.env.NODE_ENV?.toLowerCase() !== 'production';
107
+ const forceLogOtp = process.env.FORCE_LOG_OTP === 'true';
108
+ if (forceLogOtp) {
109
+ console.log(`[FORCE OTP LOG] Phone: ${cleanPhone}, OTP: ${otp}`);
110
+ }
111
+ else if (isDev) {
112
+ console.log(`[OTP] Phone: ${cleanPhone}, OTP: ${otp}`);
113
+ }
114
+ return res.json({
115
+ success: true,
116
+ message: 'OTP sent successfully.',
117
+ });
118
+ }
119
+ catch (error) {
120
+ return res.status(500).json({ error: true, message: error.message });
121
+ }
122
+ });
123
+ // VERIFY OTP
124
+ ctx.router.post(`${basePath}/verify-otp`, ctx.apiKeyMiddleware, async (req, res) => {
125
+ try {
126
+ const { phone, otp } = req.body;
127
+ if (!phone || !String(phone).trim() || !otp || !String(otp).trim()) {
128
+ return res.status(400).json({
129
+ error: true,
130
+ message: 'Phone number and OTP are required',
131
+ });
132
+ }
133
+ const cleanPhone = String(phone).trim();
134
+ const storedOtp = await otpStore.get(cleanPhone);
135
+ if (!storedOtp || storedOtp !== String(otp).trim()) {
136
+ return res.status(400).json({
137
+ error: true,
138
+ message: 'Invalid or expired OTP',
139
+ });
140
+ }
141
+ return res.json({
142
+ success: true,
143
+ message: 'OTP verified successfully.',
144
+ });
145
+ }
146
+ catch (error) {
147
+ return res.status(500).json({ error: true, message: error.message });
148
+ }
149
+ });
150
+ // REGISTER
151
+ ctx.router.post(`${basePath}/register`, ctx.optionalAuthMiddleware, async (req, res) => {
152
+ try {
23
153
  const userModel = ctx.getModel('users');
24
154
  const usersSchema = ctx.getSchema('users');
155
+ // Whitelist check (backward-compatible: only enforce if publicRegisterFields is defined)
156
+ const whitelist = usersSchema?.publicRegisterFields;
157
+ if (whitelist) {
158
+ const bodyKeys = Object.keys(req.body);
159
+ const forbidden = bodyKeys.filter((key) => key !== 'otp' && !whitelist.includes(key));
160
+ if (forbidden.length > 0) {
161
+ return res.status(400).json({
162
+ error: true,
163
+ message: `Field(s) not allowed during registration: ${forbidden.join(', ')}`,
164
+ });
165
+ }
166
+ }
167
+ let { email, username, password, otp, ...rest } = req.body;
168
+ const isPasswordless = !password && otp;
169
+ if (!isPasswordless) {
170
+ if (!password || String(password).length < 8) {
171
+ return res
172
+ .status(400)
173
+ .json({
174
+ error: true,
175
+ message: 'Password must be at least 8 characters long',
176
+ });
177
+ }
178
+ }
25
179
  if (email)
26
180
  email = String(email).trim().toLowerCase();
27
181
  if (username)
28
182
  username = String(username).trim();
183
+ const phone = rest.phone;
184
+ if (!username && phone) {
185
+ username = String(phone).trim();
186
+ }
29
187
  const data = { ...rest };
30
188
  if (email)
31
189
  data.email = email;
32
190
  if (username)
33
191
  data.username = username;
34
- if (data.status == null)
35
- data.status = 'pending';
192
+ // Resolve Role Name to Database ID (Relation Hydration on Signup)
193
+ const rolesModel = ctx.getModel('roles');
194
+ if (data.role && rolesModel) {
195
+ const roleStr = String(data.role).trim();
196
+ let foundRole = null;
197
+ // Try finding by name first
198
+ const byName = await rolesModel.read({ name: roleStr }, 1, 0, true);
199
+ if (byName && byName.length > 0) {
200
+ foundRole = byName[0];
201
+ }
202
+ else {
203
+ const byNameLower = await rolesModel.read({ name: roleStr.toLowerCase() }, 1, 0, true);
204
+ if (byNameLower && byNameLower.length > 0) {
205
+ foundRole = byNameLower[0];
206
+ }
207
+ }
208
+ // Try finding by ID if not found by name
209
+ if (!foundRole) {
210
+ const isHex24 = /^[0-9a-fA-F]{24}$/.test(roleStr);
211
+ const isNumeric = !isNaN(Number(roleStr)) && roleStr !== '';
212
+ if (isHex24 || isNumeric) {
213
+ const queryId = isNumeric ? Number(roleStr) : roleStr;
214
+ const byId = await rolesModel.read({ id: queryId }, 1, 0, true);
215
+ if (byId && byId.length > 0) {
216
+ foundRole = byId[0];
217
+ }
218
+ }
219
+ }
220
+ if (foundRole) {
221
+ data.role = foundRole.id;
222
+ }
223
+ else {
224
+ return res.status(400).json({
225
+ error: true,
226
+ message: `Invalid role specified: ${roleStr}`,
227
+ });
228
+ }
229
+ }
230
+ // If no role specified, resolve default role to its database ID
231
+ if (!data.role && rolesModel) {
232
+ const roleAttr = usersSchema?.attributes?.role;
233
+ const defaultRoleVal = (Array.isArray(roleAttr) ? roleAttr[0]?.default : roleAttr?.default) || 'user';
234
+ const defaultRoles = await rolesModel.read({ name: String(defaultRoleVal).toLowerCase() }, 1, 0, true);
235
+ if (defaultRoles && defaultRoles.length > 0) {
236
+ data.role = defaultRoles[0].id;
237
+ }
238
+ }
239
+ // Admin-only role assignment validation
240
+ const resolvedRoleName = (await ctx.normalizeRoleName(data.role)) ?? '';
241
+ const targetIsAdmin = ['admin', 'superadmin'].includes(resolvedRoleName.toLowerCase());
242
+ const requesterRole = String(req.user?.role || '').toLowerCase();
243
+ const isSuperadmin = requesterRole === 'superadmin';
244
+ if (targetIsAdmin && !isSuperadmin) {
245
+ return res.status(403).json({
246
+ error: true,
247
+ message: 'Only super administrators can assign administrative roles.',
248
+ });
249
+ }
250
+ // OTP Phone Verification check
251
+ const cleanPhone = phone ? String(phone).trim() : '';
252
+ const verificationEnabled = getPhoneVerificationSetting(ctx, resolvedRoleName);
253
+ if (verificationEnabled && cleanPhone) {
254
+ const hasApiKey = req.headers['x-api-key'] && req.headers['x-api-key'] === ctx.trustedApiKey;
255
+ const isRequesterAdmin = ['admin', 'superadmin'].includes(String(requesterRole).toLowerCase());
256
+ const bypassOtp = hasApiKey || isRequesterAdmin;
257
+ if (!bypassOtp) {
258
+ if (!otp || !String(otp).trim()) {
259
+ return res.status(400).json({
260
+ error: true,
261
+ message: 'Verification OTP is required',
262
+ });
263
+ }
264
+ const storedOtp = await otpStore.get(cleanPhone);
265
+ if (!storedOtp || storedOtp !== String(otp).trim()) {
266
+ return res.status(400).json({
267
+ error: true,
268
+ message: 'Invalid or expired OTP',
269
+ });
270
+ }
271
+ await otpStore.delete(cleanPhone);
272
+ }
273
+ }
274
+ // Onboarding status defaults based on role (Verification gate)
275
+ if (data.status == null) {
276
+ let defaultStatus = 'pending';
277
+ if (usersSchema) {
278
+ const roleDefaultStatuses = usersSchema.roleDefaultStatuses;
279
+ if (roleDefaultStatuses && roleDefaultStatuses[resolvedRoleName.toLowerCase()]) {
280
+ defaultStatus = roleDefaultStatuses[resolvedRoleName.toLowerCase()];
281
+ }
282
+ else {
283
+ const statusAttr = usersSchema.attributes?.status;
284
+ defaultStatus = (Array.isArray(statusAttr) ? statusAttr[0]?.default : statusAttr?.default) || 'pending';
285
+ }
286
+ }
287
+ data.status = defaultStatus;
288
+ }
289
+ // Hydrate default values for required fields that are not in the registration payload
290
+ if (usersSchema) {
291
+ for (const [key, attr] of Object.entries(usersSchema.attributes)) {
292
+ if (Array.isArray(attr))
293
+ continue;
294
+ if (attr?.private)
295
+ continue;
296
+ const reqd = Boolean(attr?.required);
297
+ if (reqd && data[key] == null) {
298
+ const defaultVal = attr?.default;
299
+ if (defaultVal !== undefined) {
300
+ data[key] = defaultVal;
301
+ }
302
+ }
303
+ }
304
+ }
36
305
  if (usersSchema) {
37
306
  const missing = ctx.validateRequiredFields(usersSchema, {
38
307
  ...data,
39
- password,
308
+ ...(isPasswordless ? {} : { password }),
40
309
  });
41
310
  if (missing.length > 0) {
42
311
  return res.status(400).json({
@@ -52,13 +321,20 @@ function setupAuthRoutes(ctx) {
52
321
  });
53
322
  }
54
323
  }
55
- const salt = await bcrypt_1.default.genSalt(10);
56
- const hashed = await bcrypt_1.default.hash(String(password) + ctx.jwtSecret, salt);
57
- data.password = hashed;
324
+ if (!isPasswordless && password) {
325
+ const salt = await bcrypt_1.default.genSalt(10);
326
+ const hashed = await bcrypt_1.default.hash(String(password) + ctx.jwtSecret, salt);
327
+ data.password = hashed;
328
+ }
329
+ else {
330
+ data.isPasswordless = true;
331
+ }
58
332
  const created = await userModel.create(data);
59
333
  const roleName = (await ctx.normalizeRoleName(created.role)) ?? '';
60
334
  const token = jsonwebtoken_1.default.sign({ id: created.id, role: roleName }, ctx.jwtSecret, { expiresIn: '7days' });
61
335
  delete created.password;
336
+ // Emit signup event
337
+ Events_1.events.emitEvent(Events_1.Events.AUTH_SIGNUP, { user: created });
62
338
  return res.json({
63
339
  success: true,
64
340
  message: 'Registration successful.',
@@ -71,26 +347,54 @@ function setupAuthRoutes(ctx) {
71
347
  });
72
348
  // LOGIN
73
349
  ctx.router.post(`${basePath}/login`, ctx.apiKeyMiddleware, async (req, res) => {
74
- let { email, username, password } = req.body;
75
- if ((!email && !username) || !password) {
350
+ let { email, username, password, phone } = req.body;
351
+ if ((!email && !username && !phone) || !password) {
76
352
  return res
77
353
  .status(400)
78
354
  .json({
79
355
  error: true,
80
- message: 'Email/username and password are required',
356
+ message: 'Credentials and password are required',
81
357
  });
82
358
  }
83
359
  try {
84
360
  const userModel = ctx.getModel('users');
85
- const findBy = email
86
- ? { email: String(email).trim().toLowerCase() }
87
- : { username: String(username).trim() };
361
+ const allowedFields = getAllowedAuthServices(ctx);
362
+ let findBy = null;
363
+ if (phone && allowedFields.includes('phone')) {
364
+ findBy = { phone: String(phone).trim() };
365
+ }
366
+ else if (email && allowedFields.includes('email') && !/^\+?[0-9]{7,15}$/.test(String(email).trim())) {
367
+ findBy = { email: String(email).trim().toLowerCase() };
368
+ }
369
+ else if (username || email) {
370
+ const lookupVal = String(username || email).trim();
371
+ const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(lookupVal);
372
+ const isPhone = /^\+?[0-9]{7,15}$/.test(lookupVal);
373
+ if (isPhone && allowedFields.includes('phone')) {
374
+ findBy = { phone: lookupVal };
375
+ }
376
+ else if (isEmail && allowedFields.includes('email')) {
377
+ findBy = { email: lookupVal.toLowerCase() };
378
+ }
379
+ else if (allowedFields.includes('username')) {
380
+ findBy = { username: lookupVal };
381
+ }
382
+ }
383
+ if (!findBy) {
384
+ return res.status(400).json({ error: true, message: 'Invalid credentials' });
385
+ }
88
386
  const users = await userModel.read(findBy, 1, 0, true);
89
387
  const user = users?.[0];
90
388
  if (!user)
91
389
  return res
92
390
  .status(400)
93
391
  .json({ error: true, message: 'Invalid credentials' });
392
+ if (user.isPasswordless) {
393
+ return res.status(400).json({
394
+ error: true,
395
+ message: 'No password set for this account. Please log in using OTP.',
396
+ });
397
+ }
94
398
  const hashedPassword = user.password;
95
399
  if (!hashedPassword)
96
400
  return res
@@ -101,16 +405,11 @@ function setupAuthRoutes(ctx) {
101
405
  return res
102
406
  .status(400)
103
407
  .json({ error: true, message: 'Invalid credentials' });
104
- const status = user.status;
105
- if (status && status !== 'active') {
106
- const msg = status === 'pending'
107
- ? 'Your account is awaiting approval.'
108
- : 'Your account is suspended.';
109
- return res.status(403).json({ error: true, message: msg });
110
- }
111
408
  const roleName = (await ctx.normalizeRoleName(user.role)) ?? '';
112
409
  const token = jsonwebtoken_1.default.sign({ id: user.id, role: roleName }, ctx.jwtSecret, { expiresIn: '7days' });
113
410
  delete user.password;
411
+ // Emit login event
412
+ Events_1.events.emitEvent(Events_1.Events.AUTH_LOGIN, { user: { ...user, roleName }, token });
114
413
  res.json({
115
414
  success: true,
116
415
  message: 'You are successfully logged in.',
@@ -123,6 +422,59 @@ function setupAuthRoutes(ctx) {
123
422
  res.status(500).json({ error: true, message: error.message });
124
423
  }
125
424
  });
425
+ // LOGIN OTP
426
+ ctx.router.post(`${basePath}/login-otp`, ctx.apiKeyMiddleware, async (req, res) => {
427
+ const { phone, otp } = req.body;
428
+ if (!phone || !String(phone).trim() || !otp || !String(otp).trim()) {
429
+ return res.status(400).json({
430
+ error: true,
431
+ message: 'Phone number and OTP are required',
432
+ });
433
+ }
434
+ try {
435
+ const cleanPhone = String(phone).trim();
436
+ const storedOtp = await otpStore.get(cleanPhone);
437
+ if (!storedOtp || storedOtp !== String(otp).trim()) {
438
+ return res.status(400).json({
439
+ error: true,
440
+ message: 'Invalid or expired OTP',
441
+ });
442
+ }
443
+ await otpStore.delete(cleanPhone);
444
+ const userModel = ctx.getModel('users');
445
+ // Find user by phone OR username
446
+ let user = null;
447
+ const byPhone = await userModel.read({ phone: cleanPhone }, 1, 0, true);
448
+ if (byPhone && byPhone.length > 0) {
449
+ user = byPhone[0];
450
+ }
451
+ else {
452
+ const byUsername = await userModel.read({ username: cleanPhone }, 1, 0, true);
453
+ if (byUsername && byUsername.length > 0) {
454
+ user = byUsername[0];
455
+ }
456
+ }
457
+ if (!user) {
458
+ return res.status(404).json({
459
+ error: true,
460
+ message: 'User not found',
461
+ });
462
+ }
463
+ const roleName = (await ctx.normalizeRoleName(user.role)) ?? '';
464
+ const token = jsonwebtoken_1.default.sign({ id: user.id, role: roleName }, ctx.jwtSecret, { expiresIn: '7days' });
465
+ delete user.password;
466
+ // Emit login event
467
+ Events_1.events.emitEvent(Events_1.Events.AUTH_LOGIN, { user: { ...user, roleName }, token });
468
+ return res.json({
469
+ success: true,
470
+ message: 'You are successfully logged in.',
471
+ data: { token, user: { ...user, roleName } },
472
+ });
473
+ }
474
+ catch (error) {
475
+ return res.status(500).json({ error: true, message: error.message });
476
+ }
477
+ });
126
478
  // ME
127
479
  ctx.router.get(`${basePath}/me`, ctx.apiKeyMiddleware, ctx.authenticateMiddleware, async (req, res) => {
128
480
  try {
@@ -224,28 +576,113 @@ function setupAuthRoutes(ctx) {
224
576
  // FORGOT PASSWORD
225
577
  const forgotPasswordHandler = async (req, res) => {
226
578
  try {
227
- const { email } = (req.body || {});
228
- if (!email || !String(email).trim()) {
579
+ const { email, phone } = (req.body || {});
580
+ if ((!email || !String(email).trim()) && (!phone || !String(phone).trim())) {
229
581
  return res
230
582
  .status(400)
231
- .json({ error: true, message: 'Email is required' });
583
+ .json({ error: true, message: 'Email or phone number is required' });
584
+ }
585
+ const services = getAllowedAuthServices(ctx);
586
+ const allowedPhone = services.includes('phone') || services.includes('Phone');
587
+ const allowedEmail = services.includes('email') || services.includes('Email');
588
+ let findBy = null;
589
+ const rawInput = String(phone || email).trim();
590
+ if (phone && allowedPhone) {
591
+ findBy = { phone: rawInput };
232
592
  }
233
- const normalizedEmail = String(email).trim().toLowerCase();
234
- try {
235
- const userModel = ctx.getModel('users');
236
- await userModel.read({ email: normalizedEmail }, 1, 0, true);
593
+ else if (email) {
594
+ const isPhone = /^\+?[0-9]{7,15}$/.test(rawInput);
595
+ if (isPhone && allowedPhone) {
596
+ findBy = { phone: rawInput };
597
+ }
598
+ else if (allowedEmail) {
599
+ findBy = { email: rawInput.toLowerCase() };
600
+ }
601
+ }
602
+ const userModel = ctx.getModel('users');
603
+ let users = findBy ? await userModel.read(findBy, 1, 0, true) : null;
604
+ if ((!users || users.length === 0) && rawInput) {
605
+ users = await userModel.read({ username: rawInput }, 1, 0, true);
606
+ }
607
+ if ((!users || users.length === 0) && rawInput && !findBy?.phone) {
608
+ users = await userModel.read({ phone: rawInput }, 1, 0, true);
237
609
  }
238
- catch { }
610
+ const user = users?.[0];
611
+ if (!user) {
612
+ return res.status(404).json({
613
+ error: true,
614
+ message: 'No account found with the provided phone number or email.',
615
+ });
616
+ }
617
+ const targetPhone = user.phone || user.username || rawInput;
618
+ const otp = Math.floor(100000 + Math.random() * 900000).toString();
619
+ const storeKey = `reset:${targetPhone}`;
620
+ await otpStore.set(storeKey, otp);
621
+ Events_1.events.emitEvent('auth:otp:send', { phone: targetPhone, otp, type: 'forgot_password' });
239
622
  return res.json({
240
623
  success: true,
241
- message: 'If an account exists, reset instructions have been sent.',
624
+ message: 'Verification OTP sent successfully.',
625
+ data: { phone: targetPhone },
242
626
  });
243
627
  }
244
628
  catch (error) {
245
629
  return res
246
- .status(400)
247
- .json({ error: true, message: error?.message || 'Error' });
630
+ .status(500)
631
+ .json({ error: true, message: error?.message || 'Error processing forgot password' });
248
632
  }
249
633
  };
250
634
  ctx.router.post(`${basePath}/forgot-password`, ctx.apiKeyMiddleware, forgotPasswordHandler);
635
+ // RESET PASSWORD
636
+ ctx.router.post(`${basePath}/reset-password`, ctx.apiKeyMiddleware, async (req, res) => {
637
+ try {
638
+ const { phone, email, otp, newPassword } = req.body;
639
+ const identifier = String(phone || email || '').trim();
640
+ if (!identifier || !otp || !String(otp).trim() || !newPassword || !String(newPassword).trim()) {
641
+ return res.status(400).json({
642
+ error: true,
643
+ message: 'Phone/email, OTP, and new password are required',
644
+ });
645
+ }
646
+ if (String(newPassword).length < 8) {
647
+ return res.status(400).json({
648
+ error: true,
649
+ message: 'Password must be at least 8 characters long',
650
+ });
651
+ }
652
+ const storeKey = `reset:${identifier}`;
653
+ const storedOtp = await otpStore.get(storeKey);
654
+ if (!storedOtp || storedOtp !== String(otp).trim()) {
655
+ return res.status(400).json({
656
+ error: true,
657
+ message: 'Invalid or expired OTP',
658
+ });
659
+ }
660
+ const userModel = ctx.getModel('users');
661
+ let users = await userModel.read({ phone: identifier }, 1, 0, true);
662
+ if (!users || users.length === 0) {
663
+ users = await userModel.read({ username: identifier }, 1, 0, true);
664
+ }
665
+ if (!users || users.length === 0) {
666
+ users = await userModel.read({ email: identifier.toLowerCase() }, 1, 0, true);
667
+ }
668
+ const user = users?.[0];
669
+ if (!user) {
670
+ return res.status(404).json({
671
+ error: true,
672
+ message: 'User account not found',
673
+ });
674
+ }
675
+ const salt = await bcrypt_1.default.genSalt(10);
676
+ const newHash = await bcrypt_1.default.hash(String(newPassword) + ctx.jwtSecret, salt);
677
+ await userModel.update(user.id, { password: newHash });
678
+ await otpStore.delete(storeKey);
679
+ return res.json({
680
+ success: true,
681
+ message: 'Password reset successfully. Please sign in with your new password.',
682
+ });
683
+ }
684
+ catch (error) {
685
+ return res.status(500).json({ error: true, message: error?.message || 'Error resetting password' });
686
+ }
687
+ });
251
688
  }
@@ -0,0 +1,3 @@
1
+ import { RouterCtx } from './ctx';
2
+ import { ChatWidgetConfig } from '../apiRouter';
3
+ export declare function setupChatWidgetRoutes(ctx: RouterCtx, chatWidget?: ChatWidgetConfig): void;