vr-commons 1.0.24 → 1.0.26
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/utils/index.d.ts +1 -1
- package/dist/utils/index.js +24 -10
- package/dist/utils/profiles.utils.d.ts +193 -34
- package/dist/utils/profiles.utils.js +439 -132
- package/dist/validations/auth.validations.d.ts +42 -3
- package/dist/validations/auth.validations.js +15 -4
- package/dist/validations/index.d.ts +2 -2
- package/dist/validations/index.js +16 -3
- package/dist/validations/profiles.validations.d.ts +362 -75
- package/dist/validations/profiles.validations.js +202 -25
- package/package.json +1 -1
|
@@ -3,78 +3,184 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.softDeleteUser = exports.hashPassword = exports.validatePassword = exports.validateUniqueFields = exports.generateJacketId = exports.getSortOrder = exports.generateEventSearchConditions = exports.generateUserSearchConditions = exports.findSecurityClearanceByRole = exports.getUsersByRole = exports.getUserById = exports.isAccountAccessible = exports.canModifyAccount = exports.hasAllPermissions = exports.hasAnyPermission = exports.hasPermission = exports.hasRole = exports.formatUserListResponse = exports.formatUserProfile = exports.JACKET_ID_PREFIX = exports.JACKET_ID_REGEX = exports.NATIONAL_ID_REGEX = exports.PHONE_REGEX = void 0;
|
|
7
7
|
const vr_models_1 = require("vr-models");
|
|
8
8
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
9
9
|
const sequelize_1 = require("sequelize");
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// REGEX PATTERNS & CONSTANTS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
exports.PHONE_REGEX = /^(078|079|072|073)\d{7}$/;
|
|
14
|
+
exports.NATIONAL_ID_REGEX = /^\d{16}$/;
|
|
15
|
+
exports.JACKET_ID_REGEX = /^JKT-\d{9}$/;
|
|
16
|
+
exports.JACKET_ID_PREFIX = "JKT";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// FORMATTING FUNCTIONS (WITH EXPLICIT RETURN TYPES)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Format user profile based on viewer type and options
|
|
22
|
+
*/
|
|
23
|
+
const formatUserProfile = (user, viewerType = "ADMIN", options = {}) => {
|
|
24
|
+
const baseProfile = {
|
|
12
25
|
id: user.id,
|
|
13
26
|
firstName: user.firstName,
|
|
14
27
|
lastName: user.lastName,
|
|
15
28
|
phoneNumber: user.phoneNumber,
|
|
16
29
|
email: user.email,
|
|
17
30
|
nationalId: user.nationalId,
|
|
18
|
-
jacketId: user.jacketId,
|
|
19
|
-
plateNumber: user.plateNumber,
|
|
20
31
|
isActive: user.isActive,
|
|
21
|
-
isSuspended: user.isSuspended,
|
|
22
|
-
isDeactivated: user.isDeactivated,
|
|
23
|
-
deactivatedAt: user.deactivatedAt,
|
|
24
32
|
lastLoginAt: user.lastLoginAt,
|
|
25
33
|
createdAt: user.createdAt,
|
|
26
|
-
role: user.securityClearance?.role,
|
|
27
|
-
level: user.securityClearance?.level,
|
|
28
|
-
permissions: user.securityClearance?.permissions || [],
|
|
29
34
|
};
|
|
35
|
+
const securityFields = options.includeSecurity
|
|
36
|
+
? {
|
|
37
|
+
role: user.securityClearance?.role,
|
|
38
|
+
level: user.securityClearance?.level,
|
|
39
|
+
permissions: user.securityClearance?.permissions || [],
|
|
40
|
+
}
|
|
41
|
+
: viewerType === "SUPER_ADMIN" || viewerType === "ADMIN"
|
|
42
|
+
? {
|
|
43
|
+
role: user.securityClearance?.role,
|
|
44
|
+
}
|
|
45
|
+
: {};
|
|
46
|
+
// Return type-specific profiles
|
|
47
|
+
switch (viewerType) {
|
|
48
|
+
case "RIDER":
|
|
49
|
+
return {
|
|
50
|
+
...baseProfile,
|
|
51
|
+
jacketId: user.jacketId,
|
|
52
|
+
plateNumber: user.plateNumber,
|
|
53
|
+
...securityFields,
|
|
54
|
+
};
|
|
55
|
+
case "PASSENGER":
|
|
56
|
+
return {
|
|
57
|
+
...baseProfile,
|
|
58
|
+
jacketId: user.jacketId,
|
|
59
|
+
plateNumber: user.plateNumber,
|
|
60
|
+
...securityFields,
|
|
61
|
+
};
|
|
62
|
+
case "AGENT":
|
|
63
|
+
return {
|
|
64
|
+
...baseProfile,
|
|
65
|
+
...securityFields,
|
|
66
|
+
};
|
|
67
|
+
case "ADMIN":
|
|
68
|
+
return {
|
|
69
|
+
...baseProfile,
|
|
70
|
+
jacketId: user.jacketId,
|
|
71
|
+
plateNumber: user.plateNumber,
|
|
72
|
+
isSuspended: user.isSuspended,
|
|
73
|
+
isDeactivated: user.isDeactivated,
|
|
74
|
+
...securityFields,
|
|
75
|
+
};
|
|
76
|
+
case "SUPER_ADMIN":
|
|
77
|
+
return {
|
|
78
|
+
...baseProfile,
|
|
79
|
+
jacketId: user.jacketId,
|
|
80
|
+
plateNumber: user.plateNumber,
|
|
81
|
+
isSuspended: user.isSuspended,
|
|
82
|
+
isDeactivated: user.isDeactivated,
|
|
83
|
+
bannedAt: user.bannedAt,
|
|
84
|
+
banReason: user.banReason,
|
|
85
|
+
suspendedAt: user.suspendedAt,
|
|
86
|
+
suspensionReason: user.suspensionReason,
|
|
87
|
+
deactivatedAt: user.deactivatedAt,
|
|
88
|
+
securityClearanceId: user.securityClearanceId,
|
|
89
|
+
...securityFields,
|
|
90
|
+
};
|
|
91
|
+
default:
|
|
92
|
+
return {
|
|
93
|
+
...baseProfile,
|
|
94
|
+
...securityFields,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
30
97
|
};
|
|
31
|
-
exports.
|
|
32
|
-
|
|
33
|
-
|
|
98
|
+
exports.formatUserProfile = formatUserProfile;
|
|
99
|
+
/**
|
|
100
|
+
* Format user list for response (paginated)
|
|
101
|
+
*/
|
|
102
|
+
const formatUserListResponse = (users, total, page, limit, viewerType = "ADMIN") => {
|
|
34
103
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
lastLoginAt: user.lastLoginAt,
|
|
43
|
-
createdAt: user.createdAt,
|
|
44
|
-
role: user.securityClearance?.role,
|
|
45
|
-
level: user.securityClearance?.level,
|
|
46
|
-
permissions: user.securityClearance?.permissions || [],
|
|
104
|
+
data: users.map((user) => (0, exports.formatUserProfile)(user, viewerType)),
|
|
105
|
+
pagination: {
|
|
106
|
+
total,
|
|
107
|
+
page,
|
|
108
|
+
limit,
|
|
109
|
+
pages: Math.ceil(total / limit),
|
|
110
|
+
},
|
|
47
111
|
};
|
|
48
112
|
};
|
|
49
|
-
exports.
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
exports.formatUserListResponse = formatUserListResponse;
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// USER ROLE CHECK FUNCTIONS
|
|
116
|
+
// ============================================================================
|
|
117
|
+
/**
|
|
118
|
+
* Check if user has a specific role
|
|
119
|
+
*/
|
|
120
|
+
const hasRole = (user, role) => {
|
|
121
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
122
|
+
return roles.includes(user.securityClearance?.role);
|
|
123
|
+
};
|
|
124
|
+
exports.hasRole = hasRole;
|
|
125
|
+
/**
|
|
126
|
+
* Check if user has a specific permission
|
|
127
|
+
*/
|
|
128
|
+
const hasPermission = (user, permission) => {
|
|
129
|
+
const permissions = user.securityClearance?.permissions || [];
|
|
130
|
+
return permissions.includes(permission);
|
|
131
|
+
};
|
|
132
|
+
exports.hasPermission = hasPermission;
|
|
133
|
+
/**
|
|
134
|
+
* Check if user has any of the given permissions
|
|
135
|
+
*/
|
|
136
|
+
const hasAnyPermission = (user, permissions) => {
|
|
137
|
+
const userPermissions = user.securityClearance?.permissions || [];
|
|
138
|
+
return permissions.some((p) => userPermissions.includes(p));
|
|
139
|
+
};
|
|
140
|
+
exports.hasAnyPermission = hasAnyPermission;
|
|
141
|
+
/**
|
|
142
|
+
* Check if user has all of the given permissions
|
|
143
|
+
*/
|
|
144
|
+
const hasAllPermissions = (user, permissions) => {
|
|
145
|
+
const userPermissions = user.securityClearance?.permissions || [];
|
|
146
|
+
return permissions.every((p) => userPermissions.includes(p));
|
|
147
|
+
};
|
|
148
|
+
exports.hasAllPermissions = hasAllPermissions;
|
|
149
|
+
/**
|
|
150
|
+
* Check if account can be modified (not deactivated/suspended)
|
|
151
|
+
*/
|
|
152
|
+
const canModifyAccount = (user) => {
|
|
153
|
+
return !user.isDeactivated && !user.isSuspended;
|
|
154
|
+
};
|
|
155
|
+
exports.canModifyAccount = canModifyAccount;
|
|
156
|
+
/**
|
|
157
|
+
* Check if account is active and accessible
|
|
158
|
+
*/
|
|
159
|
+
const isAccountAccessible = (user) => {
|
|
160
|
+
return user.isActive && !user.isDeactivated && !user.isSuspended;
|
|
161
|
+
};
|
|
162
|
+
exports.isAccountAccessible = isAccountAccessible;
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// DATABASE QUERY FUNCTIONS
|
|
165
|
+
// ============================================================================
|
|
166
|
+
/**
|
|
167
|
+
* Get user by ID with security clearance
|
|
168
|
+
*/
|
|
169
|
+
const getUserById = async (userId, includeSecurity = true, excludeSensitive = true) => {
|
|
170
|
+
const query = {
|
|
69
171
|
where: { id: userId },
|
|
70
|
-
|
|
172
|
+
};
|
|
173
|
+
if (includeSecurity) {
|
|
174
|
+
query.include = [
|
|
71
175
|
{
|
|
72
176
|
model: vr_models_1.SecurityClearance,
|
|
73
177
|
as: "securityClearance",
|
|
74
178
|
attributes: ["role", "level", "permissions"],
|
|
75
179
|
},
|
|
76
|
-
]
|
|
77
|
-
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
if (excludeSensitive) {
|
|
183
|
+
query.attributes = {
|
|
78
184
|
exclude: [
|
|
79
185
|
"password",
|
|
80
186
|
"otp",
|
|
@@ -82,14 +188,57 @@ const getAdminWithClearance = async (userId) => {
|
|
|
82
188
|
"tokenVersion",
|
|
83
189
|
"forgotPassword",
|
|
84
190
|
],
|
|
85
|
-
}
|
|
86
|
-
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return await vr_models_1.User.findOne(query);
|
|
87
194
|
};
|
|
88
|
-
exports.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
195
|
+
exports.getUserById = getUserById;
|
|
196
|
+
/**
|
|
197
|
+
* Get users by role with optional filters
|
|
198
|
+
*/
|
|
199
|
+
const getUsersByRole = async (role, filters = {}, pagination) => {
|
|
200
|
+
const where = {};
|
|
201
|
+
if (role) {
|
|
202
|
+
where["$securityClearance.role$"] = role;
|
|
203
|
+
}
|
|
204
|
+
// Apply filters
|
|
205
|
+
if (filters.isActive !== undefined)
|
|
206
|
+
where.isActive = filters.isActive;
|
|
207
|
+
if (filters.isSuspended !== undefined)
|
|
208
|
+
where.isSuspended = filters.isSuspended;
|
|
209
|
+
if (filters.isDeactivated !== undefined)
|
|
210
|
+
where.isDeactivated = filters.isDeactivated;
|
|
211
|
+
if (filters.isBanned !== undefined) {
|
|
212
|
+
if (filters.isBanned) {
|
|
213
|
+
where.bannedAt = { [sequelize_1.Op.ne]: null };
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
where.bannedAt = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Date range filter
|
|
220
|
+
if (filters.startDate || filters.endDate) {
|
|
221
|
+
where.createdAt = {};
|
|
222
|
+
if (filters.startDate) {
|
|
223
|
+
where.createdAt[sequelize_1.Op.gte] = new Date(filters.startDate);
|
|
224
|
+
}
|
|
225
|
+
if (filters.endDate) {
|
|
226
|
+
where.createdAt[sequelize_1.Op.lte] = new Date(filters.endDate + "T23:59:59.999Z");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Search filter
|
|
230
|
+
if (filters.search) {
|
|
231
|
+
where[sequelize_1.Op.or] = [
|
|
232
|
+
{ firstName: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
233
|
+
{ lastName: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
234
|
+
{ phoneNumber: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
235
|
+
{ email: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
236
|
+
{ nationalId: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
237
|
+
{ jacketId: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
const query = {
|
|
241
|
+
where,
|
|
93
242
|
include: [
|
|
94
243
|
{
|
|
95
244
|
model: vr_models_1.SecurityClearance,
|
|
@@ -106,93 +255,251 @@ const getUserById = async (userId) => {
|
|
|
106
255
|
"forgotPassword",
|
|
107
256
|
],
|
|
108
257
|
},
|
|
109
|
-
}
|
|
258
|
+
};
|
|
259
|
+
// Apply pagination
|
|
260
|
+
if (pagination) {
|
|
261
|
+
const { page, limit } = pagination;
|
|
262
|
+
query.limit = limit;
|
|
263
|
+
query.offset = (page - 1) * limit;
|
|
264
|
+
}
|
|
265
|
+
const { rows, count } = await vr_models_1.User.findAndCountAll(query);
|
|
266
|
+
return {
|
|
267
|
+
users: rows,
|
|
268
|
+
total: count,
|
|
269
|
+
};
|
|
110
270
|
};
|
|
111
|
-
exports.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
271
|
+
exports.getUsersByRole = getUsersByRole;
|
|
272
|
+
/**
|
|
273
|
+
* Find security clearance by role
|
|
274
|
+
*/
|
|
275
|
+
const findSecurityClearanceByRole = async (role, requireDefault = true) => {
|
|
276
|
+
const where = { role };
|
|
277
|
+
if (requireDefault) {
|
|
278
|
+
where.isDefault = true;
|
|
279
|
+
}
|
|
280
|
+
return await vr_models_1.SecurityClearance.findOne({ where });
|
|
117
281
|
};
|
|
118
282
|
exports.findSecurityClearanceByRole = findSecurityClearanceByRole;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
field: "nationalId",
|
|
140
|
-
message: "National ID already registered",
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
// Check email if provided
|
|
144
|
-
if (email) {
|
|
145
|
-
const existingEmail = await vr_models_1.User.findOne({ where: { email } });
|
|
146
|
-
if (existingEmail) {
|
|
147
|
-
errors.push({ field: "email", message: "Email already registered" });
|
|
283
|
+
/**
|
|
284
|
+
* Generate search conditions for user queries
|
|
285
|
+
*/
|
|
286
|
+
const generateUserSearchConditions = (filters) => {
|
|
287
|
+
const where = {};
|
|
288
|
+
if (filters.role) {
|
|
289
|
+
where["$securityClearance.role$"] = filters.role;
|
|
290
|
+
}
|
|
291
|
+
if (filters.isActive !== undefined) {
|
|
292
|
+
where.isActive = filters.isActive;
|
|
293
|
+
}
|
|
294
|
+
if (filters.isSuspended !== undefined) {
|
|
295
|
+
where.isSuspended = filters.isSuspended;
|
|
296
|
+
}
|
|
297
|
+
if (filters.isBanned !== undefined) {
|
|
298
|
+
if (filters.isBanned) {
|
|
299
|
+
where.bannedAt = { [sequelize_1.Op.ne]: null };
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
where.bannedAt = null;
|
|
148
303
|
}
|
|
149
304
|
}
|
|
150
|
-
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (targetRole === "SUPER_ADMIN") {
|
|
159
|
-
return { canModify: false, reason: "Cannot modify super admin accounts" };
|
|
160
|
-
}
|
|
161
|
-
// Admins cannot modify other admins (only super admins can)
|
|
162
|
-
if (targetRole === "ADMIN" && adminRole !== "SUPER_ADMIN") {
|
|
163
|
-
return {
|
|
164
|
-
canModify: false,
|
|
165
|
-
reason: "Admins cannot modify other admin accounts",
|
|
166
|
-
};
|
|
305
|
+
if (filters.startDate || filters.endDate) {
|
|
306
|
+
where.createdAt = {};
|
|
307
|
+
if (filters.startDate) {
|
|
308
|
+
where.createdAt[sequelize_1.Op.gte] = new Date(filters.startDate);
|
|
309
|
+
}
|
|
310
|
+
if (filters.endDate) {
|
|
311
|
+
where.createdAt[sequelize_1.Op.lte] = new Date(filters.endDate + "T23:59:59.999Z");
|
|
312
|
+
}
|
|
167
313
|
}
|
|
168
|
-
//
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
314
|
+
// Search filter
|
|
315
|
+
if (filters.search) {
|
|
316
|
+
where[sequelize_1.Op.or] = [
|
|
317
|
+
{ firstName: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
318
|
+
{ lastName: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
319
|
+
{ phoneNumber: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
320
|
+
{ email: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
321
|
+
{ nationalId: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
322
|
+
{ jacketId: { [sequelize_1.Op.iLike]: `%${filters.search}%` } },
|
|
323
|
+
];
|
|
174
324
|
}
|
|
175
|
-
return
|
|
325
|
+
return where;
|
|
176
326
|
};
|
|
177
|
-
exports.
|
|
178
|
-
|
|
179
|
-
|
|
327
|
+
exports.generateUserSearchConditions = generateUserSearchConditions;
|
|
328
|
+
/**
|
|
329
|
+
* Generate search conditions for event logs
|
|
330
|
+
*/
|
|
331
|
+
const generateEventSearchConditions = (userId, filters) => {
|
|
180
332
|
const where = {};
|
|
181
|
-
|
|
182
|
-
|
|
333
|
+
// Search filter
|
|
334
|
+
where[sequelize_1.Op.or] = [{ actorId: userId }, { entityId: userId }];
|
|
335
|
+
if (filters.action) {
|
|
336
|
+
where.action = filters.action;
|
|
183
337
|
}
|
|
184
|
-
if (
|
|
185
|
-
where.
|
|
338
|
+
if (filters.entity) {
|
|
339
|
+
where.entity = filters.entity;
|
|
186
340
|
}
|
|
187
|
-
if (
|
|
188
|
-
where
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
341
|
+
if (filters.startDate || filters.endDate) {
|
|
342
|
+
where.createdAt = {};
|
|
343
|
+
if (filters.startDate) {
|
|
344
|
+
where.createdAt[sequelize_1.Op.gte] = new Date(filters.startDate);
|
|
345
|
+
}
|
|
346
|
+
if (filters.endDate) {
|
|
347
|
+
where.createdAt[sequelize_1.Op.lte] = new Date(filters.endDate + "T23:59:59.999Z");
|
|
348
|
+
}
|
|
195
349
|
}
|
|
196
350
|
return where;
|
|
197
351
|
};
|
|
198
|
-
exports.
|
|
352
|
+
exports.generateEventSearchConditions = generateEventSearchConditions;
|
|
353
|
+
/**
|
|
354
|
+
* Get sort order for queries
|
|
355
|
+
*/
|
|
356
|
+
const getSortOrder = (sortBy = "createdAt", order = "desc") => {
|
|
357
|
+
const validSortFields = ["firstName", "lastLoginAt", "createdAt"];
|
|
358
|
+
const sortField = validSortFields.includes(sortBy) ? sortBy : "createdAt";
|
|
359
|
+
const sortOrder = order.toLowerCase() === "asc" ? "ASC" : "DESC";
|
|
360
|
+
if (sortField === "firstName") {
|
|
361
|
+
return [
|
|
362
|
+
["firstName", sortOrder],
|
|
363
|
+
["lastName", sortOrder],
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
return [[sortField, sortOrder]];
|
|
367
|
+
};
|
|
368
|
+
exports.getSortOrder = getSortOrder;
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// GENERATION FUNCTIONS
|
|
371
|
+
// ============================================================================
|
|
372
|
+
/**
|
|
373
|
+
* Generate unique jacket ID
|
|
374
|
+
*/
|
|
375
|
+
const generateJacketId = async () => {
|
|
376
|
+
let isUnique = false;
|
|
377
|
+
let jacketId = "";
|
|
378
|
+
while (!isUnique) {
|
|
379
|
+
const timestamp = Date.now().toString().slice(-6);
|
|
380
|
+
const random = Math.floor(Math.random() * 1000)
|
|
381
|
+
.toString()
|
|
382
|
+
.padStart(3, "0");
|
|
383
|
+
jacketId = `${exports.JACKET_ID_PREFIX}-${timestamp}${random}`;
|
|
384
|
+
const existing = await vr_models_1.User.findOne({ where: { jacketId } });
|
|
385
|
+
isUnique = !existing;
|
|
386
|
+
}
|
|
387
|
+
return jacketId;
|
|
388
|
+
};
|
|
389
|
+
exports.generateJacketId = generateJacketId;
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// VALIDATION FUNCTIONS
|
|
392
|
+
// ============================================================================
|
|
393
|
+
/**
|
|
394
|
+
* Validate unique user fields
|
|
395
|
+
*/
|
|
396
|
+
const validateUniqueFields = async (fields, excludeUserId) => {
|
|
397
|
+
const errors = [];
|
|
398
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
399
|
+
if (!value)
|
|
400
|
+
continue;
|
|
401
|
+
const where = { [field]: value };
|
|
402
|
+
if (excludeUserId) {
|
|
403
|
+
where.id = { [sequelize_1.Op.ne]: excludeUserId };
|
|
404
|
+
}
|
|
405
|
+
const existing = await vr_models_1.User.findOne({ where });
|
|
406
|
+
if (existing) {
|
|
407
|
+
let message = "";
|
|
408
|
+
switch (field) {
|
|
409
|
+
case "phoneNumber":
|
|
410
|
+
message = "Phone number already registered";
|
|
411
|
+
break;
|
|
412
|
+
case "nationalId":
|
|
413
|
+
message = "National ID already registered";
|
|
414
|
+
break;
|
|
415
|
+
case "email":
|
|
416
|
+
message = "Email already registered";
|
|
417
|
+
break;
|
|
418
|
+
case "jacketId":
|
|
419
|
+
message = "Jacket ID already taken";
|
|
420
|
+
break;
|
|
421
|
+
default:
|
|
422
|
+
message = `${field} already exists`;
|
|
423
|
+
}
|
|
424
|
+
errors.push({ field, message });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return errors;
|
|
428
|
+
};
|
|
429
|
+
exports.validateUniqueFields = validateUniqueFields;
|
|
430
|
+
/**
|
|
431
|
+
* Validate password
|
|
432
|
+
*/
|
|
433
|
+
const validatePassword = async (plainPassword, hashedPassword) => {
|
|
434
|
+
if (!hashedPassword)
|
|
435
|
+
return false;
|
|
436
|
+
return await bcryptjs_1.default.compare(plainPassword, hashedPassword);
|
|
437
|
+
};
|
|
438
|
+
exports.validatePassword = validatePassword;
|
|
439
|
+
// ============================================================================
|
|
440
|
+
// PASSWORD FUNCTIONS
|
|
441
|
+
// ============================================================================
|
|
442
|
+
/**
|
|
443
|
+
* Hash password
|
|
444
|
+
*/
|
|
445
|
+
const hashPassword = async (password) => {
|
|
446
|
+
const salt = await bcryptjs_1.default.genSalt(10);
|
|
447
|
+
return await bcryptjs_1.default.hash(password, salt);
|
|
448
|
+
};
|
|
449
|
+
exports.hashPassword = hashPassword;
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// ACCOUNT MANAGEMENT FUNCTIONS
|
|
452
|
+
// ============================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Soft delete user
|
|
455
|
+
*/
|
|
456
|
+
const softDeleteUser = async (userId, options) => {
|
|
457
|
+
const updateData = {
|
|
458
|
+
isActive: false,
|
|
459
|
+
isDeactivated: true,
|
|
460
|
+
deactivatedAt: new Date(),
|
|
461
|
+
};
|
|
462
|
+
// Anonymize sensitive data
|
|
463
|
+
const timestamp = Date.now();
|
|
464
|
+
updateData.email = null;
|
|
465
|
+
updateData.password = null;
|
|
466
|
+
updateData.phoneNumber = `DELETED_${timestamp}`;
|
|
467
|
+
updateData.nationalId = `DELETED_${timestamp}`;
|
|
468
|
+
updateData.jacketId = `DELETED_${timestamp}`;
|
|
469
|
+
await vr_models_1.User.update(updateData, {
|
|
470
|
+
where: { id: userId },
|
|
471
|
+
transaction: options?.transaction,
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
exports.softDeleteUser = softDeleteUser;
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// EXPORT ALL FUNCTIONS
|
|
477
|
+
// ============================================================================
|
|
478
|
+
exports.default = {
|
|
479
|
+
// Formatting
|
|
480
|
+
formatUserProfile: exports.formatUserProfile,
|
|
481
|
+
formatUserListResponse: exports.formatUserListResponse,
|
|
482
|
+
// Role checks
|
|
483
|
+
hasRole: exports.hasRole,
|
|
484
|
+
hasPermission: exports.hasPermission,
|
|
485
|
+
hasAnyPermission: exports.hasAnyPermission,
|
|
486
|
+
hasAllPermissions: exports.hasAllPermissions,
|
|
487
|
+
canModifyAccount: exports.canModifyAccount,
|
|
488
|
+
isAccountAccessible: exports.isAccountAccessible,
|
|
489
|
+
// Database queries
|
|
490
|
+
getUserById: exports.getUserById,
|
|
491
|
+
getUsersByRole: exports.getUsersByRole,
|
|
492
|
+
findSecurityClearanceByRole: exports.findSecurityClearanceByRole,
|
|
493
|
+
generateUserSearchConditions: exports.generateUserSearchConditions,
|
|
494
|
+
generateEventSearchConditions: exports.generateEventSearchConditions,
|
|
495
|
+
getSortOrder: exports.getSortOrder,
|
|
496
|
+
// Generation
|
|
497
|
+
generateJacketId: exports.generateJacketId,
|
|
498
|
+
// Validation
|
|
499
|
+
validateUniqueFields: exports.validateUniqueFields,
|
|
500
|
+
validatePassword: exports.validatePassword,
|
|
501
|
+
// Password
|
|
502
|
+
hashPassword: exports.hashPassword,
|
|
503
|
+
// Account management
|
|
504
|
+
softDeleteUser: exports.softDeleteUser,
|
|
505
|
+
};
|