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 +10 -13
- package/doc/CHANGELOG.md +37 -0
- package/package.json +4 -1
- package/prisma/seed.ts +1 -1
- package/scripts/make-controller.js +1 -1
- package/src/controllers/authController.ts +66 -63
- package/src/controllers/petController.ts +22 -14
- package/src/controllers/rbacController.ts +108 -93
- package/src/core/server.ts +55 -0
- package/src/index.ts +69 -7
- package/src/middleware/auth.ts +1 -0
- package/src/middleware/error.ts +18 -11
- package/src/middleware/multipart.ts +13 -0
- package/src/middleware/rateLimit.ts +14 -0
- package/src/middleware/requestLogger.ts +27 -0
- package/src/middleware/visitor.ts +1 -1
- package/src/routes/index.ts +10 -0
- package/src/routes/pets.ts +3 -2
- package/src/utils/logger.ts +100 -0
- package/src/utils/response.ts +8 -0
- package/src/utils/validator.ts +430 -0
- package/storage/logs/.gitkeep +0 -0
- package/src/schema/auth-schema.ts +0 -62
- package/src/schema/pet-schema.ts +0 -14
- package/src/server.ts +0 -34
- /package/src/{prisma.ts → core/database.ts} +0 -0
- /package/src/{realtime.ts → core/realtime.ts} +0 -0
- /package/src/{redis.ts → core/redis.ts} +0 -0
package/.env.example
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
PORT=
|
|
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
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
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"
|
package/doc/CHANGELOG.md
ADDED
|
@@ -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.
|
|
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
|
@@ -37,7 +37,7 @@ let content = '';
|
|
|
37
37
|
|
|
38
38
|
if (isResource) {
|
|
39
39
|
content = `import { Request, Response } from "express";
|
|
40
|
-
import { prisma } from "../
|
|
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
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 } =
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 } =
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
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 (
|
|
328
|
-
sendError(res,
|
|
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 "../
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
...
|
|
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
|
|
88
|
+
const validator = Validator.make(req.body || {}, {
|
|
89
|
+
name: "string",
|
|
90
|
+
species: "string",
|
|
91
|
+
age: "integer|min:1",
|
|
92
|
+
});
|
|
85
93
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
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
|
-
...
|
|
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
|
});
|