lapeh 2.6.17 → 3.0.2
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/.env.example +1 -6
- package/README.md +19 -85
- package/bin/index.js +84 -180
- package/dist/lib/bootstrap.d.ts.map +1 -1
- package/dist/lib/bootstrap.js +17 -16
- package/dist/lib/core/store.d.ts +55 -0
- package/dist/lib/core/store.d.ts.map +1 -0
- package/dist/lib/core/store.js +66 -0
- package/dist/lib/middleware/error.d.ts.map +1 -1
- package/dist/lib/middleware/error.js +1 -20
- package/dist/lib/utils/validator.d.ts.map +1 -1
- package/dist/lib/utils/validator.js +3 -32
- package/dist/src/modules/Auth/auth.controller.d.ts.map +1 -1
- package/dist/src/modules/Auth/auth.controller.js +118 -105
- package/dist/src/modules/Rbac/rbac.controller.d.ts.map +1 -1
- package/dist/src/modules/Rbac/rbac.controller.js +141 -140
- package/dist/src/routes/index.d.ts.map +1 -1
- package/dist/src/routes/index.js +0 -5
- package/doc/en/CHEATSHEET.md +3 -7
- package/doc/en/CLI.md +16 -41
- package/doc/en/DEPLOYMENT.md +171 -245
- package/doc/en/GETTING_STARTED.md +1 -25
- package/doc/en/PACKAGES.md +2 -3
- package/doc/en/STRUCTURE.md +1 -11
- package/doc/en/TUTORIAL.md +61 -119
- package/doc/id/CHANGELOG.md +16 -0
- package/doc/id/CHEATSHEET.md +0 -4
- package/doc/id/CLI.md +19 -54
- package/doc/id/DEPLOYMENT.md +171 -245
- package/doc/id/GETTING_STARTED.md +91 -115
- package/doc/id/PACKAGES.md +0 -1
- package/doc/id/STRUCTURE.md +1 -11
- package/doc/id/TUTORIAL.md +51 -109
- package/gitignore.template +0 -10
- package/lib/bootstrap.ts +39 -38
- package/lib/core/store.ts +116 -0
- package/lib/middleware/error.ts +1 -21
- package/lib/utils/validator.ts +3 -39
- package/package.json +4 -18
- package/scripts/init-project.js +2 -108
- package/scripts/make-module.js +1 -12
- package/scripts/seed-json.js +158 -0
- package/src/modules/Auth/auth.controller.ts +156 -106
- package/src/modules/Rbac/rbac.controller.ts +193 -138
- package/src/routes/index.ts +0 -3
- package/src/routes/rbac.ts +42 -42
- package/storage/logs/.0337f5062fe676994d1dc340156e089444e3d6e0-audit.json +5 -10
- package/storage/logs/lapeh-2025-12-30.log +1093 -0
- package/tsconfig.build.json +1 -3
- package/tsconfig.json +0 -1
- package/lib/core/database.ts +0 -5
- package/prisma/base.prisma.template +0 -8
- package/prisma/migrations/20251225163737_init/migration.sql +0 -236
- package/prisma/migrations/20251226000329_create_pets_table/migration.sql +0 -11
- package/prisma/migrations/20251226001249_create_pets_table/migration.sql +0 -82
- package/prisma/migrations/20251226001717_restore_core_models/migration.sql +0 -236
- package/prisma/migrations/migration_lock.toml +0 -3
- package/prisma/schema.prisma +0 -197
- package/prisma/seed.ts +0 -411
- package/scripts/compile-schema.js +0 -64
- package/src/modules/Auth/auth.prisma +0 -106
- package/src/modules/Pets/pets.controller.ts +0 -238
- package/src/modules/Pets/pets.prisma +0 -9
- package/src/modules/Rbac/rbac.prisma +0 -68
- package/src/routes/pets.ts +0 -13
- package/storage/logs/lapeh-2025-12-26.log +0 -88
- package/storage/logs/lapeh-2025-12-27.log +0 -217
|
@@ -2,10 +2,11 @@ import { Request, Response } from "express";
|
|
|
2
2
|
import bcrypt from "bcryptjs";
|
|
3
3
|
import jwt from "jsonwebtoken";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
|
-
import { prisma } from "@lapeh/core/database";
|
|
6
5
|
import { sendError, sendFastSuccess } from "@lapeh/utils/response";
|
|
7
6
|
import { Validator } from "@lapeh/utils/validator";
|
|
8
7
|
import { getSerializer, createResponseSchema } from "@lapeh/core/serializer";
|
|
8
|
+
import { users, roles, user_roles, saveStore } from "@lapeh/core/store";
|
|
9
|
+
import { redis } from "@lapeh/core/redis";
|
|
9
10
|
|
|
10
11
|
export const ACCESS_TOKEN_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
|
|
11
12
|
|
|
@@ -96,39 +97,48 @@ export async function register(req: Request, res: Response) {
|
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
const { email, name, password } = await validator.validated();
|
|
99
|
-
|
|
100
|
+
|
|
101
|
+
// Manual unique check (In-Memory)
|
|
102
|
+
if (users.find((u) => u.email === email)) {
|
|
103
|
+
sendError(res, 422, "Validation error", { email: "Email already taken" });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
const hash = await bcrypt.hash(password, 10);
|
|
101
|
-
const user = await prisma.users.create({
|
|
102
|
-
data: {
|
|
103
|
-
email,
|
|
104
|
-
name,
|
|
105
|
-
password: hash,
|
|
106
|
-
uuid: uuidv4(),
|
|
107
|
-
created_at: new Date(),
|
|
108
|
-
updated_at: new Date(),
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
108
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
const newUser = {
|
|
110
|
+
id: (users.length + 1).toString(), // Simple ID generation
|
|
111
|
+
email,
|
|
112
|
+
name,
|
|
113
|
+
password: hash,
|
|
114
|
+
uuid: uuidv4(),
|
|
115
|
+
created_at: new Date(),
|
|
116
|
+
updated_at: new Date(),
|
|
117
|
+
avatar: null,
|
|
118
|
+
avatar_url: null,
|
|
119
|
+
email_verified_at: null,
|
|
120
|
+
remember_token: null,
|
|
121
|
+
};
|
|
122
|
+
users.push(newUser);
|
|
123
|
+
|
|
124
|
+
const defaultRole = roles.find((r) => r.slug === "user");
|
|
115
125
|
if (defaultRole) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
},
|
|
126
|
+
user_roles.push({
|
|
127
|
+
id: (user_roles.length + 1).toString(),
|
|
128
|
+
user_id: newUser.id,
|
|
129
|
+
role_id: defaultRole.id,
|
|
130
|
+
created_at: new Date(),
|
|
122
131
|
});
|
|
123
132
|
}
|
|
133
|
+
saveStore();
|
|
124
134
|
|
|
125
135
|
sendFastSuccess(res, 201, registerSerializer, {
|
|
126
136
|
status: "success",
|
|
127
|
-
message: "Registration successful",
|
|
137
|
+
message: "Registration successful. You can now login.",
|
|
128
138
|
data: {
|
|
129
|
-
id:
|
|
130
|
-
email:
|
|
131
|
-
name:
|
|
139
|
+
id: newUser.id.toString(),
|
|
140
|
+
email: newUser.email,
|
|
141
|
+
name: newUser.name,
|
|
132
142
|
role: defaultRole ? defaultRole.slug : "user",
|
|
133
143
|
},
|
|
134
144
|
});
|
|
@@ -145,16 +155,9 @@ export async function login(req: Request, res: Response) {
|
|
|
145
155
|
return;
|
|
146
156
|
}
|
|
147
157
|
const { email, password } = await validator.validated();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
user_roles: {
|
|
152
|
-
include: {
|
|
153
|
-
role: true,
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
+
|
|
159
|
+
const user = users.find((u) => u.email === email);
|
|
160
|
+
|
|
158
161
|
if (!user) {
|
|
159
162
|
sendError(res, 401, "Email not registered", {
|
|
160
163
|
field: "email",
|
|
@@ -162,7 +165,7 @@ export async function login(req: Request, res: Response) {
|
|
|
162
165
|
});
|
|
163
166
|
return;
|
|
164
167
|
}
|
|
165
|
-
const ok = await bcrypt.compare(password, user.password);
|
|
168
|
+
const ok = await bcrypt.compare(password, user.password || "");
|
|
166
169
|
if (!ok) {
|
|
167
170
|
sendError(res, 401, "Invalid credentials", {
|
|
168
171
|
field: "password",
|
|
@@ -175,10 +178,18 @@ export async function login(req: Request, res: Response) {
|
|
|
175
178
|
sendError(res, 500, "Server misconfigured");
|
|
176
179
|
return;
|
|
177
180
|
}
|
|
181
|
+
|
|
182
|
+
// Find user roles
|
|
183
|
+
const userRoleLinks = user_roles.filter((ur) => ur.user_id === user.id);
|
|
184
|
+
const userRoleObjects = userRoleLinks
|
|
185
|
+
.map((ur) => roles.find((r) => r.id === ur.role_id))
|
|
186
|
+
.filter((r) => r);
|
|
187
|
+
|
|
178
188
|
const primaryUserRole =
|
|
179
|
-
|
|
180
|
-
?
|
|
189
|
+
userRoleObjects.length > 0 && userRoleObjects[0]
|
|
190
|
+
? userRoleObjects[0].slug
|
|
181
191
|
: "user";
|
|
192
|
+
|
|
182
193
|
const accessExpiresInSeconds = ACCESS_TOKEN_EXPIRES_IN_SECONDS;
|
|
183
194
|
const accessExpiresAt = new Date(
|
|
184
195
|
Date.now() + accessExpiresInSeconds * 1000
|
|
@@ -218,32 +229,54 @@ export async function me(req: Request, res: Response) {
|
|
|
218
229
|
sendError(res, 401, "Unauthorized");
|
|
219
230
|
return;
|
|
220
231
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
|
|
233
|
+
// Try to get from Redis
|
|
234
|
+
const cachedUser = await redis.get(`user:${payload.userId}`);
|
|
235
|
+
if (cachedUser) {
|
|
236
|
+
const user = JSON.parse(cachedUser);
|
|
237
|
+
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
238
|
+
status: "success",
|
|
239
|
+
message: "User profile (cached)",
|
|
240
|
+
data: user,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const user = users.find((u) => u.id === payload.userId);
|
|
246
|
+
|
|
231
247
|
if (!user) {
|
|
232
248
|
sendError(res, 404, "User not found");
|
|
233
249
|
return;
|
|
234
250
|
}
|
|
235
|
-
|
|
251
|
+
|
|
252
|
+
// Find user roles
|
|
253
|
+
const userRoleLinks = user_roles.filter((ur) => ur.user_id === user.id);
|
|
254
|
+
const userRoleObjects = userRoleLinks
|
|
255
|
+
.map((ur) => roles.find((r) => r.id === ur.role_id))
|
|
256
|
+
.filter((r) => r);
|
|
257
|
+
|
|
258
|
+
const primaryRoleSlug =
|
|
259
|
+
userRoleObjects.length > 0 ? userRoleObjects[0]?.slug : "user";
|
|
260
|
+
|
|
261
|
+
const { password, ...rest } = user;
|
|
262
|
+
const userData = {
|
|
263
|
+
...rest,
|
|
264
|
+
id: user.id.toString(),
|
|
265
|
+
role: primaryRoleSlug,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Cache in Redis for 1 hour
|
|
269
|
+
await redis.set(
|
|
270
|
+
`user:${payload.userId}`,
|
|
271
|
+
JSON.stringify(userData),
|
|
272
|
+
"EX",
|
|
273
|
+
3600
|
|
274
|
+
);
|
|
275
|
+
|
|
236
276
|
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
237
277
|
status: "success",
|
|
238
278
|
message: "User profile",
|
|
239
|
-
data:
|
|
240
|
-
...rest,
|
|
241
|
-
id: user.id.toString(),
|
|
242
|
-
role:
|
|
243
|
-
user.user_roles && user.user_roles.length > 0 && user.user_roles[0].role
|
|
244
|
-
? user.user_roles[0].role.slug
|
|
245
|
-
: "user",
|
|
246
|
-
},
|
|
279
|
+
data: userData,
|
|
247
280
|
});
|
|
248
281
|
}
|
|
249
282
|
|
|
@@ -284,24 +317,25 @@ export async function refreshToken(req: Request, res: Response) {
|
|
|
284
317
|
sendError(res, 401, "Invalid refresh token");
|
|
285
318
|
return;
|
|
286
319
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
user_roles: {
|
|
291
|
-
include: {
|
|
292
|
-
role: true,
|
|
293
|
-
},
|
|
294
|
-
},
|
|
295
|
-
},
|
|
296
|
-
});
|
|
320
|
+
|
|
321
|
+
const user = users.find((u) => u.id === decoded.userId);
|
|
322
|
+
|
|
297
323
|
if (!user) {
|
|
298
324
|
sendError(res, 401, "Invalid refresh token");
|
|
299
325
|
return;
|
|
300
326
|
}
|
|
327
|
+
|
|
328
|
+
// Find user roles
|
|
329
|
+
const userRoleLinks = user_roles.filter((ur) => ur.user_id === user.id);
|
|
330
|
+
const userRoleObjects = userRoleLinks
|
|
331
|
+
.map((ur) => roles.find((r) => r.id === ur.role_id))
|
|
332
|
+
.filter((r) => r);
|
|
333
|
+
|
|
301
334
|
const primaryUserRole =
|
|
302
|
-
|
|
303
|
-
?
|
|
335
|
+
userRoleObjects.length > 0 && userRoleObjects[0]
|
|
336
|
+
? userRoleObjects[0].slug
|
|
304
337
|
: "user";
|
|
338
|
+
|
|
305
339
|
const accessExpiresInSeconds = ACCESS_TOKEN_EXPIRES_IN_SECONDS;
|
|
306
340
|
const accessExpiresAt = new Date(
|
|
307
341
|
Date.now() + accessExpiresInSeconds * 1000
|
|
@@ -357,21 +391,22 @@ export async function updateAvatar(req: Request, res: Response) {
|
|
|
357
391
|
const avatar = file.filename;
|
|
358
392
|
const avatar_url =
|
|
359
393
|
process.env.AVATAR_BASE_URL || `/uploads/avatars/${file.filename}`;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
|
|
395
|
+
const userIndex = users.findIndex((u) => u.id === userId);
|
|
396
|
+
if (userIndex === -1) {
|
|
397
|
+
sendError(res, 404, "User not found");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
users[userIndex] = {
|
|
402
|
+
...users[userIndex],
|
|
403
|
+
avatar,
|
|
404
|
+
avatar_url,
|
|
405
|
+
updated_at: new Date(),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const updated = users[userIndex];
|
|
409
|
+
const { password, ...rest } = updated;
|
|
375
410
|
|
|
376
411
|
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
377
412
|
status: "success",
|
|
@@ -400,14 +435,15 @@ export async function updatePassword(req: Request, res: Response) {
|
|
|
400
435
|
return;
|
|
401
436
|
}
|
|
402
437
|
const { currentPassword, newPassword } = await validator.validated();
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (!user) {
|
|
438
|
+
|
|
439
|
+
const userIndex = users.findIndex((u) => u.id === payload.userId);
|
|
440
|
+
if (userIndex === -1) {
|
|
407
441
|
sendError(res, 404, "User not found");
|
|
408
442
|
return;
|
|
409
443
|
}
|
|
410
|
-
|
|
444
|
+
|
|
445
|
+
const user = users[userIndex];
|
|
446
|
+
const ok = await bcrypt.compare(currentPassword, user.password || "");
|
|
411
447
|
if (!ok) {
|
|
412
448
|
sendError(res, 401, "Invalid credentials", {
|
|
413
449
|
field: "currentPassword",
|
|
@@ -416,13 +452,13 @@ export async function updatePassword(req: Request, res: Response) {
|
|
|
416
452
|
return;
|
|
417
453
|
}
|
|
418
454
|
const hash = await bcrypt.hash(newPassword, 10);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
455
|
+
|
|
456
|
+
users[userIndex] = {
|
|
457
|
+
...user,
|
|
458
|
+
password: hash,
|
|
459
|
+
updated_at: new Date(),
|
|
460
|
+
};
|
|
461
|
+
|
|
426
462
|
sendFastSuccess(res, 200, voidSerializer, {
|
|
427
463
|
status: "success",
|
|
428
464
|
message: "Password updated successfully",
|
|
@@ -446,17 +482,31 @@ export async function updateProfile(req: Request, res: Response) {
|
|
|
446
482
|
}
|
|
447
483
|
const { name, email } = await validator.validated();
|
|
448
484
|
const userId = payload.userId;
|
|
449
|
-
// Manual unique check removed as it is handled by validator
|
|
450
485
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
486
|
+
// Manual unique check (In-Memory)
|
|
487
|
+
if (users.find((u) => u.email === email && u.id !== userId)) {
|
|
488
|
+
sendError(res, 422, "Validation error", { email: "Email already taken" });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const userIndex = users.findIndex((u) => u.id === userId);
|
|
493
|
+
if (userIndex === -1) {
|
|
494
|
+
sendError(res, 404, "User not found");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
users[userIndex] = {
|
|
499
|
+
...users[userIndex],
|
|
500
|
+
name,
|
|
501
|
+
email,
|
|
502
|
+
updated_at: new Date(),
|
|
503
|
+
};
|
|
504
|
+
saveStore();
|
|
505
|
+
await redis.del(`user:${userId}`);
|
|
506
|
+
|
|
507
|
+
const updated = users[userIndex];
|
|
508
|
+
const { password, ...rest } = updated;
|
|
509
|
+
|
|
460
510
|
sendFastSuccess(res, 200, userProfileSerializer, {
|
|
461
511
|
status: "success",
|
|
462
512
|
message: "Profile updated successfully",
|