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.
Files changed (67) hide show
  1. package/.env.example +1 -6
  2. package/README.md +19 -85
  3. package/bin/index.js +84 -180
  4. package/dist/lib/bootstrap.d.ts.map +1 -1
  5. package/dist/lib/bootstrap.js +17 -16
  6. package/dist/lib/core/store.d.ts +55 -0
  7. package/dist/lib/core/store.d.ts.map +1 -0
  8. package/dist/lib/core/store.js +66 -0
  9. package/dist/lib/middleware/error.d.ts.map +1 -1
  10. package/dist/lib/middleware/error.js +1 -20
  11. package/dist/lib/utils/validator.d.ts.map +1 -1
  12. package/dist/lib/utils/validator.js +3 -32
  13. package/dist/src/modules/Auth/auth.controller.d.ts.map +1 -1
  14. package/dist/src/modules/Auth/auth.controller.js +118 -105
  15. package/dist/src/modules/Rbac/rbac.controller.d.ts.map +1 -1
  16. package/dist/src/modules/Rbac/rbac.controller.js +141 -140
  17. package/dist/src/routes/index.d.ts.map +1 -1
  18. package/dist/src/routes/index.js +0 -5
  19. package/doc/en/CHEATSHEET.md +3 -7
  20. package/doc/en/CLI.md +16 -41
  21. package/doc/en/DEPLOYMENT.md +171 -245
  22. package/doc/en/GETTING_STARTED.md +1 -25
  23. package/doc/en/PACKAGES.md +2 -3
  24. package/doc/en/STRUCTURE.md +1 -11
  25. package/doc/en/TUTORIAL.md +61 -119
  26. package/doc/id/CHANGELOG.md +16 -0
  27. package/doc/id/CHEATSHEET.md +0 -4
  28. package/doc/id/CLI.md +19 -54
  29. package/doc/id/DEPLOYMENT.md +171 -245
  30. package/doc/id/GETTING_STARTED.md +91 -115
  31. package/doc/id/PACKAGES.md +0 -1
  32. package/doc/id/STRUCTURE.md +1 -11
  33. package/doc/id/TUTORIAL.md +51 -109
  34. package/gitignore.template +0 -10
  35. package/lib/bootstrap.ts +39 -38
  36. package/lib/core/store.ts +116 -0
  37. package/lib/middleware/error.ts +1 -21
  38. package/lib/utils/validator.ts +3 -39
  39. package/package.json +4 -18
  40. package/scripts/init-project.js +2 -108
  41. package/scripts/make-module.js +1 -12
  42. package/scripts/seed-json.js +158 -0
  43. package/src/modules/Auth/auth.controller.ts +156 -106
  44. package/src/modules/Rbac/rbac.controller.ts +193 -138
  45. package/src/routes/index.ts +0 -3
  46. package/src/routes/rbac.ts +42 -42
  47. package/storage/logs/.0337f5062fe676994d1dc340156e089444e3d6e0-audit.json +5 -10
  48. package/storage/logs/lapeh-2025-12-30.log +1093 -0
  49. package/tsconfig.build.json +1 -3
  50. package/tsconfig.json +0 -1
  51. package/lib/core/database.ts +0 -5
  52. package/prisma/base.prisma.template +0 -8
  53. package/prisma/migrations/20251225163737_init/migration.sql +0 -236
  54. package/prisma/migrations/20251226000329_create_pets_table/migration.sql +0 -11
  55. package/prisma/migrations/20251226001249_create_pets_table/migration.sql +0 -82
  56. package/prisma/migrations/20251226001717_restore_core_models/migration.sql +0 -236
  57. package/prisma/migrations/migration_lock.toml +0 -3
  58. package/prisma/schema.prisma +0 -197
  59. package/prisma/seed.ts +0 -411
  60. package/scripts/compile-schema.js +0 -64
  61. package/src/modules/Auth/auth.prisma +0 -106
  62. package/src/modules/Pets/pets.controller.ts +0 -238
  63. package/src/modules/Pets/pets.prisma +0 -9
  64. package/src/modules/Rbac/rbac.prisma +0 -68
  65. package/src/routes/pets.ts +0 -13
  66. package/storage/logs/lapeh-2025-12-26.log +0 -88
  67. 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
- // Manual unique check removed as it is handled by validator
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 defaultRole = await prisma.roles.findUnique({
113
- where: { slug: "user" },
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
- await prisma.user_roles.create({
117
- data: {
118
- user_id: user.id,
119
- role_id: defaultRole.id,
120
- created_at: new Date(),
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: user.id.toString(),
130
- email: user.email,
131
- name: user.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
- const user = await prisma.users.findUnique({
149
- where: { email },
150
- include: {
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
- user.user_roles && user.user_roles.length > 0 && user.user_roles[0].role
180
- ? user.user_roles[0].role.slug
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
- const user = await prisma.users.findUnique({
222
- where: { id: payload.userId },
223
- include: {
224
- user_roles: {
225
- include: {
226
- role: true,
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
- const { password, remember_token, ...rest } = user as any;
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
- const user = await prisma.users.findUnique({
288
- where: { id: decoded.userId },
289
- include: {
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
- user.user_roles && user.user_roles.length > 0 && user.user_roles[0].role
303
- ? user.user_roles[0].role.slug
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
- const updated = await prisma.users.update({
361
- where: { id: userId },
362
- data: {
363
- avatar,
364
- avatar_url,
365
- updated_at: new Date(),
366
- },
367
- });
368
- const { password, remember_token, ...rest } = updated as any;
369
- // Note: user_roles might not be fetched in update, so role defaults to "user" or fetched if needed.
370
- // Ideally we should refetch or pass existing role.
371
- // For now assuming role is preserved or handled by frontend state, but API should return it.
372
- // Let's rely on nullable role or simple "user" fallback if not present in `updated`.
373
- // Actually `update` returns what was updated. Relations are not included unless specified.
374
- // For now we will return it compatible with userProfileSchema.
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
- const user = await prisma.users.findUnique({
404
- where: { id: payload.userId },
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
- const ok = await bcrypt.compare(currentPassword, user.password);
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
- await prisma.users.update({
420
- where: { id: user.id },
421
- data: {
422
- password: hash,
423
- updated_at: new Date(),
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
- const updated = await prisma.users.update({
452
- where: { id: userId },
453
- data: {
454
- name,
455
- email,
456
- updated_at: new Date(),
457
- },
458
- });
459
- const { password, remember_token, ...rest } = updated as any;
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",