@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.
- package/README.md +151 -0
- package/dist/api/apiRouter.d.ts +20 -0
- package/dist/api/apiRouter.js +122 -7
- package/dist/api/router/mountCrudRoutes.js +69 -18
- package/dist/api/router/setupAuthRoutes.js +475 -38
- package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
- package/dist/api/router/setupChatWidgetRoutes.js +207 -0
- package/dist/api/router/setupFileRoutes.js +221 -16
- package/dist/api/router/utils.d.ts +2 -1
- package/dist/api/router/utils.js +8 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +145 -64
- package/dist/database/DatabaseAdapter.d.ts +1 -0
- package/dist/database/NMAdapter.d.ts +6 -0
- package/dist/database/NMAdapter.js +437 -86
- package/dist/database/QueryEngine.js +7 -4
- package/dist/files/FileStorageAdapter.d.ts +1 -0
- package/dist/files/LocalFileStorageAdapter.d.ts +1 -0
- package/dist/files/LocalFileStorageAdapter.js +25 -3
- package/dist/files/S3FileStorageAdapter.d.ts +3 -1
- package/dist/files/S3FileStorageAdapter.js +72 -11
- package/dist/files/filename.js +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/models/BaseModel.d.ts +4 -0
- package/dist/policy/authorize.d.ts +1 -1
- package/dist/policy/authorize.js +64 -13
- package/dist/schemas/Users.json +20 -10
- package/dist/services/IndexingService.d.ts +24 -0
- package/dist/services/IndexingService.js +555 -0
- package/dist/services/LocalAIService.d.ts +34 -0
- package/dist/services/LocalAIService.js +471 -0
- package/dist/services/OpenAIService.d.ts +16 -0
- package/dist/services/OpenAIService.js +586 -0
- package/dist/utils/SchemaLoader.d.ts +1 -1
- package/dist/utils/SchemaLoader.js +40 -4
- 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
|
-
//
|
|
12
|
-
ctx.router.post(`${basePath}/
|
|
63
|
+
// SEND OTP
|
|
64
|
+
ctx.router.post(`${basePath}/send-otp`, ctx.apiKeyMiddleware, async (req, res) => {
|
|
13
65
|
try {
|
|
14
|
-
|
|
15
|
-
if (
|
|
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: '
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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: '
|
|
356
|
+
message: 'Credentials and password are required',
|
|
81
357
|
});
|
|
82
358
|
}
|
|
83
359
|
try {
|
|
84
360
|
const userModel = ctx.getModel('users');
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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: '
|
|
624
|
+
message: 'Verification OTP sent successfully.',
|
|
625
|
+
data: { phone: targetPhone },
|
|
242
626
|
});
|
|
243
627
|
}
|
|
244
628
|
catch (error) {
|
|
245
629
|
return res
|
|
246
|
-
.status(
|
|
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
|
}
|