lapeh 2.1.6 → 2.1.8

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 CHANGED
@@ -1,22 +1,19 @@
1
- PORT=4000
1
+ PORT=8000
2
2
  DATABASE_PROVIDER="postgresql"
3
3
  DATABASE_URL="postgresql://sianu:12341234@localhost:5432/db_example_test?schema=public"
4
+
5
+ # Used for all encryption-related tasks in the framework (JWT, etc.)
4
6
  JWT_SECRET="replace_this_with_a_secure_random_string"
5
7
 
6
- # Redis Configuration (Optional)
7
- # If REDIS_URL is not set or connection fails, the framework will automatically
8
- # switch to an in-memory Redis mock (bundled). No installation required for development.
9
- # REDIS_URL="redis://lapeh:12341234@localhost:6379"
8
+ # Framework Timezone
9
+ TZ="Asia/Jakarta"
10
+
11
+ # Redis Configuration (Optional)
12
+ # If REDIS_URL is not set or connection fails, the framework will automatically
13
+ # switch to an in-memory Redis mock (bundled). No installation required for development.
14
+ # REDIS_URL="redis://lapeh:12341234@localhost:6379"
10
15
  # NO_REDIS="true"
11
16
 
12
17
  # To force disable Redis and use in-memory mock even if Redis is available:
13
18
  # NO_REDIS="true"
14
19
 
15
- # mysql example:
16
- # DATABASE_PROVIDER="mysql"
17
- # DATABASE_URL="mysql://user:password@localhost:3306/news_db"
18
- # DATABASE_HOST="localhost"
19
- # DATABASE_PORT=3306
20
- # DATABASE_USER="user"
21
- # DATABASE_PASSWORD="password"
22
- # DATABASE_NAME="news_db"
@@ -0,0 +1,37 @@
1
+ # Dokumentasi Perubahan Lapeh Framework
2
+
3
+ File ini mencatat semua perubahan, pembaruan, dan perbaikan yang dilakukan pada framework Lapeh, diurutkan berdasarkan tanggal.
4
+
5
+ ## [2025-12-27] - Pembaruan Struktur & Validasi
6
+
7
+ ### 🚀 Fitur Baru
8
+ - **Laravel-style Validator**:
9
+ - Implementasi utility `Validator` baru di `src/utils/validator.ts` yang meniru gaya validasi Laravel.
10
+ - Mendukung rule string seperti `required|string|min:3|email`.
11
+ - Penambahan rule `unique` untuk pengecekan database otomatis (Prisma).
12
+ - Penambahan rule `mimes`, `image`, `max` (file size) untuk validasi upload file.
13
+ - Penambahan rule `sometimes` untuk field opsional.
14
+ - **Framework Hardening (Keamanan & Stabilitas)**:
15
+ - **Rate Limiting**: Middleware anti-spam/brute-force di `src/middleware/rateLimit.ts`.
16
+ - **Request Logger**: Pencatatan log request masuk di `src/middleware/requestLogger.ts`.
17
+ - **Health Check**: Endpoint `/` kini mengembalikan status kesehatan server.
18
+ - **Graceful Shutdown**: Penanganan penutupan koneksi Database dan Redis yang aman saat server berhenti (`SIGTERM`/`SIGINT`).
19
+ - **Environment Validation**: Validasi variabel `.env` wajib (seperti `DATABASE_URL`, `JWT_SECRET`) saat startup.
20
+ - **Struktur Folder Baru**:
21
+ - Pemisahan konfigurasi inti ke `src/core/` (`server.ts`, `database.ts`, `redis.ts`, `realtime.ts`) agar folder `src` lebih bersih.
22
+ - Sentralisasi route di `src/routes/index.ts` (WIP).
23
+
24
+ ### 🛠️ Perbaikan & Refactoring
25
+ - **Controller Refactoring**:
26
+ - `AuthController`: Migrasi ke `Validator` baru, termasuk validasi upload avatar.
27
+ - `PetController`: Migrasi ke `Validator` baru.
28
+ - `RbacController`: Migrasi sebagian ke `Validator` baru.
29
+ - **Pembersihan**:
30
+ - Penghapusan folder `src/schema/` (Zod schema lama) karena sudah digantikan oleh `Validator` utility.
31
+ - Penghapusan file duplikat/lama di root `src/` setelah migrasi ke `src/core/`.
32
+
33
+ ### 📝 Catatan Teknis
34
+ - **Validator Async**: Method `fails()`, `passes()`, dan `validated()` kini bersifat `async` untuk mendukung pengecekan database (`unique`).
35
+ - **Type Safety**: Semua perubahan telah diverifikasi dengan `npm run typecheck`.
36
+
37
+ ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lapeh",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "description": "Framework API Express yang siap pakai (Standardized)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -74,6 +74,8 @@
74
74
  "slugify": "1.6.6",
75
75
  "socket.io": "4.8.3",
76
76
  "uuid": "13.0.0",
77
+ "winston": "^3.19.0",
78
+ "winston-daily-rotate-file": "^5.0.0",
77
79
  "zod": "3.23.8"
78
80
  },
79
81
  "devDependencies": {
@@ -81,6 +83,7 @@
81
83
  "@types/cors": "2.8.19",
82
84
  "@types/express": "5.0.6",
83
85
  "@types/jsonwebtoken": "9.0.10",
86
+ "@types/multer": "^2.0.0",
84
87
  "@types/node": "25.0.3",
85
88
  "@types/pg": "8.16.0",
86
89
  "@types/uuid": "10.0.0",
package/prisma/seed.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { prisma } from "../src/prisma";
1
+ import { prisma } from "../src/core/database";
2
2
  import bcrypt from "bcryptjs";
3
3
  import { v4 as uuidv4 } from "uuid";
4
4
  import slugify from "slugify";
@@ -37,7 +37,7 @@ let content = '';
37
37
 
38
38
  if (isResource) {
39
39
  content = `import { Request, Response } from "express";
40
- import { prisma } from "../prisma";
40
+ import { prisma } from "../core/database";
41
41
  import { sendSuccess, sendError } from "../utils/response";
42
42
  import { getPagination, buildPaginationMeta } from "../utils/pagination";
43
43
 
@@ -1,35 +1,27 @@
1
- import { Request, Response } from "express";
2
- import { prisma } from "../prisma";
3
- import bcrypt from "bcryptjs";
4
- import jwt from "jsonwebtoken";
5
- import { v4 as uuidv4 } from "uuid";
6
- import { sendSuccess, sendError } from "../utils/response";
7
- import {
8
- registerSchema,
9
- loginSchema,
10
- refreshSchema,
11
- updatePasswordSchema,
12
- updateProfileSchema,
13
- } from "../schema/auth-schema";
1
+ import { Request, Response } from "express";
2
+ import bcrypt from "bcryptjs";
3
+ import jwt from "jsonwebtoken";
4
+ import { v4 as uuidv4 } from "uuid";
5
+ import { prisma } from "../core/database";
6
+ import { sendSuccess, sendError } from "../utils/response";
7
+ import { Validator } from "../utils/validator";
14
8
 
15
9
  export const ACCESS_TOKEN_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
16
10
 
17
11
  export async function register(req: Request, res: Response) {
18
- const parsed = registerSchema.safeParse(req.body);
19
- if (!parsed.success) {
20
- const errors = parsed.error.flatten().fieldErrors;
21
- sendError(res, 422, "Validation error", errors);
22
- return;
23
- }
24
- const { email, name, password } = parsed.data;
25
- const existing = await prisma.users.findUnique({ where: { email } });
26
- if (existing) {
27
- sendError(res, 409, "Email already used", {
28
- field: "email",
29
- message: "Email is already registered, please use another email",
30
- });
12
+ const validator = Validator.make(req.body || {}, {
13
+ email: "required|email|unique:users,email",
14
+ name: "required|min:1",
15
+ password: "required|min:4",
16
+ confirmPassword: "required|min:4|same:password",
17
+ });
18
+
19
+ if (await validator.fails()) {
20
+ sendError(res, 422, "Validation error", validator.errors());
31
21
  return;
32
22
  }
23
+ const { email, name, password } = await validator.validated();
24
+ // Manual unique check removed as it is handled by validator
33
25
  const hash = await bcrypt.hash(password, 10);
34
26
  const user = await prisma.users.create({
35
27
  data: {
@@ -64,13 +56,16 @@ export async function register(req: Request, res: Response) {
64
56
  }
65
57
 
66
58
  export async function login(req: Request, res: Response) {
67
- const parsed = loginSchema.safeParse(req.body);
68
- if (!parsed.success) {
69
- const errors = parsed.error.flatten().fieldErrors;
70
- sendError(res, 422, "Validation error", errors);
59
+ const validator = Validator.make(req.body || {}, {
60
+ email: "required|email",
61
+ password: "required|min:4",
62
+ });
63
+
64
+ if (await validator.fails()) {
65
+ sendError(res, 422, "Validation error", validator.errors());
71
66
  return;
72
67
  }
73
- const { email, password } = parsed.data;
68
+ const { email, password } = await validator.validated();
74
69
  const user = await prisma.users.findUnique({
75
70
  where: { email },
76
71
  include: {
@@ -170,10 +165,11 @@ export async function logout(req: Request, res: Response) {
170
165
  }
171
166
 
172
167
  export async function refreshToken(req: Request, res: Response) {
173
- const parsed = refreshSchema.safeParse(req.body);
174
- if (!parsed.success) {
175
- const errors = parsed.error.flatten().fieldErrors;
176
- sendError(res, 422, "Validation error", errors);
168
+ const validator = Validator.make(req.body || {}, {
169
+ refreshToken: "required|min:1",
170
+ });
171
+ if (await validator.fails()) {
172
+ sendError(res, 422, "Validation error", validator.errors());
177
173
  return;
178
174
  }
179
175
  const secret = process.env.JWT_SECRET;
@@ -182,7 +178,8 @@ export async function refreshToken(req: Request, res: Response) {
182
178
  return;
183
179
  }
184
180
  try {
185
- const decoded = jwt.verify(parsed.data.refreshToken, secret) as {
181
+ const validatedData = await validator.validated();
182
+ const decoded = jwt.verify(validatedData.refreshToken, secret) as {
186
183
  userId: string;
187
184
  role: string;
188
185
  tokenType?: string;
@@ -238,10 +235,22 @@ export async function updateAvatar(req: Request, res: Response) {
238
235
  sendError(res, 401, "Unauthorized");
239
236
  return;
240
237
  }
241
- const file = (req as any).file as {
242
- filename: string;
243
- path: string;
244
- } | null;
238
+
239
+ const data = {
240
+ avatar: (req as any).file,
241
+ };
242
+
243
+ const validator = Validator.make(data, {
244
+ avatar: "nullable|image|mimes:jpeg,png,jpg,gif|max:2048",
245
+ });
246
+
247
+ if (await validator.fails()) {
248
+ sendError(res, 422, "Validation error", validator.errors());
249
+ return;
250
+ }
251
+
252
+ const { avatar: file } = await validator.validated();
253
+
245
254
  if (!file) {
246
255
  sendError(res, 400, "Avatar file is required");
247
256
  return;
@@ -271,13 +280,16 @@ export async function updatePassword(req: Request, res: Response) {
271
280
  sendError(res, 401, "Unauthorized");
272
281
  return;
273
282
  }
274
- const parsed = updatePasswordSchema.safeParse(req.body);
275
- if (!parsed.success) {
276
- const errors = parsed.error.flatten().fieldErrors;
277
- sendError(res, 422, "Validation error", errors);
283
+ const validator = Validator.make(req.body || {}, {
284
+ currentPassword: "required|min:4",
285
+ newPassword: "required|min:4",
286
+ confirmPassword: "required|min:4|same:newPassword",
287
+ });
288
+ if (await validator.fails()) {
289
+ sendError(res, 422, "Validation error", validator.errors());
278
290
  return;
279
291
  }
280
- const { currentPassword, newPassword } = parsed.data;
292
+ const { currentPassword, newPassword } = await validator.validated();
281
293
  const user = await prisma.users.findUnique({
282
294
  where: { id: BigInt(payload.userId) },
283
295
  });
@@ -310,27 +322,18 @@ export async function updateProfile(req: Request, res: Response) {
310
322
  sendError(res, 401, "Unauthorized");
311
323
  return;
312
324
  }
313
- const parsed = updateProfileSchema.safeParse(req.body);
314
- if (!parsed.success) {
315
- const errors = parsed.error.flatten().fieldErrors;
316
- sendError(res, 422, "Validation error", errors);
317
- return;
318
- }
319
- const { name, email } = parsed.data;
320
- const userId = BigInt(payload.userId);
321
- const existing = await prisma.users.findFirst({
322
- where: {
323
- email,
324
- NOT: { id: userId },
325
- },
325
+ const validator = Validator.make(req.body || {}, {
326
+ name: "required|min:1",
327
+ email: `required|email|unique:users,email,${payload.userId}`,
326
328
  });
327
- if (existing) {
328
- sendError(res, 409, "Email already used", {
329
- field: "email",
330
- message: "Email is already registered, please use another email",
331
- });
329
+ if (await validator.fails()) {
330
+ sendError(res, 422, "Validation error", validator.errors());
332
331
  return;
333
332
  }
333
+ const { name, email } = await validator.validated();
334
+ const userId = BigInt(payload.userId);
335
+ // Manual unique check removed as it is handled by validator
336
+
334
337
  const updated = await prisma.users.update({
335
338
  where: { id: userId },
336
339
  data: {
@@ -1,9 +1,8 @@
1
-
2
1
  import { Request, Response } from "express";
3
- import { prisma } from "../prisma";
2
+ import { prisma } from "../core/database";
4
3
  import { sendSuccess, sendError } from "../utils/response";
5
- import { createPetSchema, updatePetSchema } from "../schema/pet-schema";
6
4
  import { getPagination, buildPaginationMeta } from "../utils/pagination";
5
+ import { Validator } from "../utils/validator";
7
6
 
8
7
  export async function index(req: Request, res: Response) {
9
8
  const { page, perPage, skip, take } = getPagination(req.query);
@@ -58,16 +57,21 @@ export async function show(req: Request, res: Response) {
58
57
  }
59
58
 
60
59
  export async function store(req: Request, res: Response) {
61
- const parsed = createPetSchema.safeParse(req.body);
62
- if (!parsed.success) {
63
- const errors = parsed.error.flatten().fieldErrors;
64
- sendError(res, 422, "Validation error", errors);
60
+ const validator = Validator.make(req.body || {}, {
61
+ name: "required|string",
62
+ species: "required|string",
63
+ age: "required|integer|min:1",
64
+ });
65
+
66
+ if (await validator.fails()) {
67
+ sendError(res, 422, "Validation error", validator.errors());
65
68
  return;
66
69
  }
67
70
 
71
+ const validatedData = await validator.validated();
68
72
  const pet = await prisma.pets.create({
69
73
  data: {
70
- ...parsed.data,
74
+ ...validatedData,
71
75
  created_at: new Date(),
72
76
  updated_at: new Date(),
73
77
  },
@@ -81,11 +85,14 @@ export async function store(req: Request, res: Response) {
81
85
 
82
86
  export async function update(req: Request, res: Response) {
83
87
  const { id } = req.params;
84
- const parsed = updatePetSchema.safeParse(req.body);
88
+ const validator = Validator.make(req.body || {}, {
89
+ name: "string",
90
+ species: "string",
91
+ age: "integer|min:1",
92
+ });
85
93
 
86
- if (!parsed.success) {
87
- const errors = parsed.error.flatten().fieldErrors;
88
- sendError(res, 422, "Validation error", errors);
94
+ if (await validator.fails()) {
95
+ sendError(res, 422, "Validation error", validator.errors());
89
96
  return;
90
97
  }
91
98
 
@@ -98,10 +105,11 @@ export async function update(req: Request, res: Response) {
98
105
  return;
99
106
  }
100
107
 
108
+ const validatedData = await validator.validated();
101
109
  const updated = await prisma.pets.update({
102
110
  where: { id: BigInt(id) },
103
111
  data: {
104
- ...parsed.data,
112
+ ...validatedData,
105
113
  updated_at: new Date(),
106
114
  },
107
115
  });
@@ -114,7 +122,7 @@ export async function update(req: Request, res: Response) {
114
122
 
115
123
  export async function destroy(req: Request, res: Response) {
116
124
  const { id } = req.params;
117
-
125
+
118
126
  const existing = await prisma.pets.findUnique({
119
127
  where: { id: BigInt(id) },
120
128
  });