lapeeh 1.0.0
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 +14 -0
- package/LICENSE +21 -0
- package/bin/index.js +934 -0
- package/doc/en/ARCHITECTURE_GUIDE.md +79 -0
- package/doc/en/CHANGELOG.md +203 -0
- package/doc/en/CHEATSHEET.md +90 -0
- package/doc/en/CLI.md +111 -0
- package/doc/en/CONTRIBUTING.md +119 -0
- package/doc/en/DEPLOYMENT.md +171 -0
- package/doc/en/FAQ.md +69 -0
- package/doc/en/FEATURES.md +99 -0
- package/doc/en/GETTING_STARTED.md +84 -0
- package/doc/en/INTRODUCTION.md +62 -0
- package/doc/en/PACKAGES.md +63 -0
- package/doc/en/PERFORMANCE.md +98 -0
- package/doc/en/ROADMAP.md +104 -0
- package/doc/en/SECURITY.md +95 -0
- package/doc/en/STRUCTURE.md +79 -0
- package/doc/en/TUTORIAL.md +145 -0
- package/doc/id/ARCHITECTURE_GUIDE.md +76 -0
- package/doc/id/CHANGELOG.md +203 -0
- package/doc/id/CHEATSHEET.md +90 -0
- package/doc/id/CLI.md +139 -0
- package/doc/id/CONTRIBUTING.md +119 -0
- package/doc/id/DEPLOYMENT.md +171 -0
- package/doc/id/FAQ.md +69 -0
- package/doc/id/FEATURES.md +169 -0
- package/doc/id/GETTING_STARTED.md +91 -0
- package/doc/id/INTRODUCTION.md +62 -0
- package/doc/id/PACKAGES.md +63 -0
- package/doc/id/PERFORMANCE.md +100 -0
- package/doc/id/ROADMAP.md +107 -0
- package/doc/id/SECURITY.md +94 -0
- package/doc/id/STRUCTURE.md +79 -0
- package/doc/id/TUTORIAL.md +145 -0
- package/docker-compose.yml +24 -0
- package/ecosystem.config.js +17 -0
- package/eslint.config.mjs +26 -0
- package/gitignore.template +30 -0
- package/lib/bootstrap.ts +210 -0
- package/lib/core/realtime.ts +34 -0
- package/lib/core/redis.ts +139 -0
- package/lib/core/serializer.ts +63 -0
- package/lib/core/server.ts +70 -0
- package/lib/core/store.ts +116 -0
- package/lib/middleware/auth.ts +63 -0
- package/lib/middleware/error.ts +50 -0
- package/lib/middleware/multipart.ts +13 -0
- package/lib/middleware/rateLimit.ts +14 -0
- package/lib/middleware/requestLogger.ts +27 -0
- package/lib/middleware/visitor.ts +178 -0
- package/lib/utils/logger.ts +100 -0
- package/lib/utils/pagination.ts +56 -0
- package/lib/utils/response.ts +88 -0
- package/lib/utils/validator.ts +394 -0
- package/nodemon.json +6 -0
- package/package.json +126 -0
- package/readme.md +357 -0
- package/scripts/check-update.js +92 -0
- package/scripts/config-clear.js +45 -0
- package/scripts/generate-jwt-secret.js +38 -0
- package/scripts/init-project.js +84 -0
- package/scripts/make-module.js +89 -0
- package/scripts/release.js +494 -0
- package/scripts/seed-json.js +158 -0
- package/scripts/verify-rbac-functional.js +187 -0
- package/src/config/app.ts +9 -0
- package/src/config/cors.ts +5 -0
- package/src/modules/Auth/auth.controller.ts +519 -0
- package/src/modules/Rbac/rbac.controller.ts +533 -0
- package/src/routes/auth.ts +74 -0
- package/src/routes/index.ts +7 -0
- package/src/routes/rbac.ts +42 -0
- package/storage/logs/.gitkeep +0 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import {
|
|
3
|
+
users,
|
|
4
|
+
roles,
|
|
5
|
+
permissions,
|
|
6
|
+
role_permissions,
|
|
7
|
+
user_roles,
|
|
8
|
+
user_permissions,
|
|
9
|
+
generateId,
|
|
10
|
+
Role,
|
|
11
|
+
Permission,
|
|
12
|
+
saveStore,
|
|
13
|
+
} from "@lapeeh/core/store";
|
|
14
|
+
import { sendError, sendFastSuccess } from "@lapeeh/utils/response";
|
|
15
|
+
import { Validator } from "@lapeeh/utils/validator";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { getSerializer, createResponseSchema } from "@lapeeh/core/serializer";
|
|
18
|
+
|
|
19
|
+
// --- Serializers ---
|
|
20
|
+
|
|
21
|
+
const roleSchema = {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
id: { type: "string" },
|
|
25
|
+
name: { type: "string" },
|
|
26
|
+
slug: { type: "string" },
|
|
27
|
+
description: { type: "string", nullable: true },
|
|
28
|
+
created_at: { type: "string", format: "date-time", nullable: true },
|
|
29
|
+
updated_at: { type: "string", format: "date-time", nullable: true },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const permissionSchema = {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
id: { type: "string" },
|
|
37
|
+
name: { type: "string" },
|
|
38
|
+
slug: { type: "string" },
|
|
39
|
+
description: { type: "string", nullable: true },
|
|
40
|
+
created_at: { type: "string", format: "date-time", nullable: true },
|
|
41
|
+
updated_at: { type: "string", format: "date-time", nullable: true },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const roleSerializer = getSerializer("role", createResponseSchema(roleSchema));
|
|
46
|
+
const roleListSerializer = getSerializer(
|
|
47
|
+
"role-list",
|
|
48
|
+
createResponseSchema({ type: "array", items: roleSchema })
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const permissionSerializer = getSerializer(
|
|
52
|
+
"permission",
|
|
53
|
+
createResponseSchema(permissionSchema)
|
|
54
|
+
);
|
|
55
|
+
const permissionListSerializer = getSerializer(
|
|
56
|
+
"permission-list",
|
|
57
|
+
createResponseSchema({ type: "array", items: permissionSchema })
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const voidSerializer = getSerializer(
|
|
61
|
+
"void",
|
|
62
|
+
createResponseSchema({ type: "null" })
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// --- Controllers ---
|
|
66
|
+
|
|
67
|
+
export async function createRole(req: Request, res: Response) {
|
|
68
|
+
const validator = Validator.make(req.body || {}, {
|
|
69
|
+
name: "required|string|min:1",
|
|
70
|
+
slug: "required|string|min:1|unique:roles,slug",
|
|
71
|
+
description: "string",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (await validator.fails()) {
|
|
75
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { name, slug, description } = await validator.validated();
|
|
80
|
+
|
|
81
|
+
// Manual unique check
|
|
82
|
+
if (roles.find((r) => r.slug === slug)) {
|
|
83
|
+
sendError(res, 422, "Validation error", { slug: "Slug already exists" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const role: Role = {
|
|
88
|
+
id: generateId(),
|
|
89
|
+
name,
|
|
90
|
+
slug,
|
|
91
|
+
description: description || null,
|
|
92
|
+
created_at: new Date(),
|
|
93
|
+
updated_at: new Date(),
|
|
94
|
+
};
|
|
95
|
+
roles.push(role);
|
|
96
|
+
saveStore();
|
|
97
|
+
|
|
98
|
+
sendFastSuccess(res, 201, roleSerializer, {
|
|
99
|
+
status: "success",
|
|
100
|
+
message: "Role created",
|
|
101
|
+
data: { ...role, id: role.id.toString() },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function listRoles(_req: Request, res: Response) {
|
|
106
|
+
const serialized = roles.map((r: any) => ({ ...r, id: r.id.toString() }));
|
|
107
|
+
sendFastSuccess(res, 200, roleListSerializer, {
|
|
108
|
+
status: "success",
|
|
109
|
+
message: "Roles list",
|
|
110
|
+
data: serialized,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function updateRole(req: Request, res: Response) {
|
|
115
|
+
const { id } = req.params;
|
|
116
|
+
const roleId = id;
|
|
117
|
+
|
|
118
|
+
const validator = Validator.make(req.body || {}, {
|
|
119
|
+
name: "string",
|
|
120
|
+
slug: `string|unique:roles,slug,${id}`,
|
|
121
|
+
description: "string",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (await validator.fails()) {
|
|
125
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { name, slug, description } = await validator.validated();
|
|
129
|
+
|
|
130
|
+
const roleIndex = roles.findIndex((r) => r.id === roleId);
|
|
131
|
+
if (roleIndex === -1) {
|
|
132
|
+
sendError(res, 404, "Role not found");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Manual unique check for update
|
|
137
|
+
if (
|
|
138
|
+
slug &&
|
|
139
|
+
slug !== roles[roleIndex].slug &&
|
|
140
|
+
roles.find((r) => r.slug === slug)
|
|
141
|
+
) {
|
|
142
|
+
sendError(res, 422, "Validation error", { slug: "Slug already exists" });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
roles[roleIndex] = {
|
|
147
|
+
...roles[roleIndex],
|
|
148
|
+
name: name ?? roles[roleIndex].name,
|
|
149
|
+
slug: slug ?? roles[roleIndex].slug,
|
|
150
|
+
description: description ?? roles[roleIndex].description,
|
|
151
|
+
updated_at: new Date(),
|
|
152
|
+
};
|
|
153
|
+
saveStore();
|
|
154
|
+
|
|
155
|
+
const updated = roles[roleIndex];
|
|
156
|
+
sendFastSuccess(res, 200, roleSerializer, {
|
|
157
|
+
status: "success",
|
|
158
|
+
message: "Role updated",
|
|
159
|
+
data: { ...updated, id: updated.id.toString() },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function deleteRole(req: Request, res: Response) {
|
|
164
|
+
const { id } = req.params;
|
|
165
|
+
const roleId = id;
|
|
166
|
+
const roleIndex = roles.findIndex((r) => r.id === roleId);
|
|
167
|
+
if (roleIndex === -1) {
|
|
168
|
+
sendError(res, 404, "Role not found");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Clean up relationships
|
|
173
|
+
// Remove from role_permissions
|
|
174
|
+
for (let i = role_permissions.length - 1; i >= 0; i--) {
|
|
175
|
+
if (role_permissions[i].role_id === roleId) {
|
|
176
|
+
role_permissions.splice(i, 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Remove from user_roles
|
|
181
|
+
for (let i = user_roles.length - 1; i >= 0; i--) {
|
|
182
|
+
if (user_roles[i].role_id === roleId) {
|
|
183
|
+
user_roles.splice(i, 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
roles.splice(roleIndex, 1);
|
|
188
|
+
|
|
189
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
190
|
+
status: "success",
|
|
191
|
+
message: "Role deleted",
|
|
192
|
+
data: null,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function createPermission(req: Request, res: Response) {
|
|
197
|
+
const validator = Validator.make(req.body || {}, {
|
|
198
|
+
name: "required|string|min:1",
|
|
199
|
+
slug: "required|string|min:1|unique:permissions,slug",
|
|
200
|
+
description: "string",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (await validator.fails()) {
|
|
204
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const { name, slug, description } = await validator.validated();
|
|
208
|
+
|
|
209
|
+
// Manual unique check
|
|
210
|
+
if (permissions.find((p) => p.slug === slug)) {
|
|
211
|
+
sendError(res, 422, "Validation error", { slug: "Slug already exists" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const permission: Permission = {
|
|
216
|
+
id: generateId(),
|
|
217
|
+
name,
|
|
218
|
+
slug,
|
|
219
|
+
description: description || null,
|
|
220
|
+
created_at: new Date(),
|
|
221
|
+
updated_at: new Date(),
|
|
222
|
+
};
|
|
223
|
+
permissions.push(permission);
|
|
224
|
+
saveStore();
|
|
225
|
+
|
|
226
|
+
sendFastSuccess(res, 201, permissionSerializer, {
|
|
227
|
+
status: "success",
|
|
228
|
+
message: "Permission created",
|
|
229
|
+
data: { ...permission, id: permission.id.toString() },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function listPermissions(_req: Request, res: Response) {
|
|
234
|
+
const serialized = permissions.map((p: any) => ({
|
|
235
|
+
...p,
|
|
236
|
+
id: p.id.toString(),
|
|
237
|
+
}));
|
|
238
|
+
sendFastSuccess(res, 200, permissionListSerializer, {
|
|
239
|
+
status: "success",
|
|
240
|
+
message: "Permissions list",
|
|
241
|
+
data: serialized,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function updatePermission(req: Request, res: Response) {
|
|
246
|
+
const { id } = req.params;
|
|
247
|
+
const permissionId = id;
|
|
248
|
+
|
|
249
|
+
const validator = Validator.make(req.body || {}, {
|
|
250
|
+
name: "string",
|
|
251
|
+
slug: `string|unique:permissions,slug,${id}`,
|
|
252
|
+
description: "string",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (await validator.fails()) {
|
|
256
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const { name, slug, description } = await validator.validated();
|
|
260
|
+
|
|
261
|
+
const permIndex = permissions.findIndex((p) => p.id === permissionId);
|
|
262
|
+
if (permIndex === -1) {
|
|
263
|
+
sendError(res, 404, "Permission not found");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
slug &&
|
|
269
|
+
slug !== permissions[permIndex].slug &&
|
|
270
|
+
permissions.find((p) => p.slug === slug)
|
|
271
|
+
) {
|
|
272
|
+
sendError(res, 422, "Validation error", { slug: "Slug already exists" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
permissions[permIndex] = {
|
|
277
|
+
...permissions[permIndex],
|
|
278
|
+
name: name ?? permissions[permIndex].name,
|
|
279
|
+
slug: slug ?? permissions[permIndex].slug,
|
|
280
|
+
description: description ?? permissions[permIndex].description,
|
|
281
|
+
updated_at: new Date(),
|
|
282
|
+
};
|
|
283
|
+
saveStore();
|
|
284
|
+
|
|
285
|
+
const updated = permissions[permIndex];
|
|
286
|
+
|
|
287
|
+
sendFastSuccess(res, 200, permissionSerializer, {
|
|
288
|
+
status: "success",
|
|
289
|
+
message: "Permission updated",
|
|
290
|
+
data: { ...updated, id: updated.id.toString() },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function deletePermission(req: Request, res: Response) {
|
|
295
|
+
const { id } = req.params;
|
|
296
|
+
const permissionId = id;
|
|
297
|
+
|
|
298
|
+
const permIndex = permissions.findIndex((p) => p.id === permissionId);
|
|
299
|
+
if (permIndex === -1) {
|
|
300
|
+
sendError(res, 404, "Permission not found");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Clean up relationships
|
|
305
|
+
for (let i = role_permissions.length - 1; i >= 0; i--) {
|
|
306
|
+
if (role_permissions[i].permission_id === permissionId) {
|
|
307
|
+
role_permissions.splice(i, 1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (let i = user_permissions.length - 1; i >= 0; i--) {
|
|
312
|
+
if (user_permissions[i].permission_id === permissionId) {
|
|
313
|
+
user_permissions.splice(i, 1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
permissions.splice(permIndex, 1);
|
|
318
|
+
saveStore();
|
|
319
|
+
|
|
320
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
321
|
+
status: "success",
|
|
322
|
+
message: "Permission deleted",
|
|
323
|
+
data: null,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function assignRoleToUser(req: Request, res: Response) {
|
|
328
|
+
const validator = Validator.make(req.body || {}, {
|
|
329
|
+
userId: z.string().min(1, "userId wajib diisi"),
|
|
330
|
+
roleId: z.string().min(1, "roleId wajib diisi"),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (await validator.fails()) {
|
|
334
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const { userId, roleId } = await validator.validated();
|
|
338
|
+
|
|
339
|
+
const user = users.find((u) => u.id === userId);
|
|
340
|
+
if (!user) {
|
|
341
|
+
sendError(res, 404, "User not found");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const role = roles.find((r) => r.id === roleId);
|
|
345
|
+
if (!role) {
|
|
346
|
+
sendError(res, 404, "Role not found");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const exists = user_roles.find(
|
|
351
|
+
(ur) => ur.user_id === userId && ur.role_id === roleId
|
|
352
|
+
);
|
|
353
|
+
if (!exists) {
|
|
354
|
+
user_roles.push({
|
|
355
|
+
id: generateId(),
|
|
356
|
+
user_id: userId,
|
|
357
|
+
role_id: roleId,
|
|
358
|
+
created_at: new Date(),
|
|
359
|
+
});
|
|
360
|
+
saveStore();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
364
|
+
status: "success",
|
|
365
|
+
message: "Role assigned to user",
|
|
366
|
+
data: null,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function removeRoleFromUser(req: Request, res: Response) {
|
|
371
|
+
const validator = Validator.make(req.body || {}, {
|
|
372
|
+
userId: z.string().min(1, "userId wajib diisi"),
|
|
373
|
+
roleId: z.string().min(1, "roleId wajib diisi"),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (await validator.fails()) {
|
|
377
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const { userId, roleId } = await validator.validated();
|
|
381
|
+
|
|
382
|
+
for (let i = user_roles.length - 1; i >= 0; i--) {
|
|
383
|
+
if (user_roles[i].user_id === userId && user_roles[i].role_id === roleId) {
|
|
384
|
+
user_roles.splice(i, 1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
389
|
+
status: "success",
|
|
390
|
+
message: "Role removed from user",
|
|
391
|
+
data: null,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function assignPermissionToRole(req: Request, res: Response) {
|
|
396
|
+
const validator = Validator.make(req.body || {}, {
|
|
397
|
+
roleId: z.string().min(1, "roleId wajib diisi"),
|
|
398
|
+
permissionId: z.string().min(1, "permissionId wajib diisi"),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (await validator.fails()) {
|
|
402
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const { roleId, permissionId } = await validator.validated();
|
|
406
|
+
|
|
407
|
+
const role = roles.find((r) => r.id === roleId);
|
|
408
|
+
if (!role) {
|
|
409
|
+
sendError(res, 404, "Role not found");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const permission = permissions.find((p) => p.id === permissionId);
|
|
413
|
+
if (!permission) {
|
|
414
|
+
sendError(res, 404, "Permission not found");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const exists = role_permissions.find(
|
|
419
|
+
(rp) => rp.role_id === roleId && rp.permission_id === permissionId
|
|
420
|
+
);
|
|
421
|
+
if (!exists) {
|
|
422
|
+
role_permissions.push({
|
|
423
|
+
id: generateId(),
|
|
424
|
+
role_id: roleId,
|
|
425
|
+
permission_id: permissionId,
|
|
426
|
+
created_at: new Date(),
|
|
427
|
+
});
|
|
428
|
+
saveStore();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
432
|
+
status: "success",
|
|
433
|
+
message: "Permission assigned to role",
|
|
434
|
+
data: null,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function removePermissionFromRole(req: Request, res: Response) {
|
|
439
|
+
const validator = Validator.make(req.body || {}, {
|
|
440
|
+
roleId: z.string().min(1, "roleId wajib diisi"),
|
|
441
|
+
permissionId: z.string().min(1, "permissionId wajib diisi"),
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (await validator.fails()) {
|
|
445
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const { roleId, permissionId } = await validator.validated();
|
|
449
|
+
|
|
450
|
+
for (let i = role_permissions.length - 1; i >= 0; i--) {
|
|
451
|
+
if (
|
|
452
|
+
role_permissions[i].role_id === roleId &&
|
|
453
|
+
role_permissions[i].permission_id === permissionId
|
|
454
|
+
) {
|
|
455
|
+
role_permissions.splice(i, 1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
460
|
+
status: "success",
|
|
461
|
+
message: "Permission removed from role",
|
|
462
|
+
data: null,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function assignPermissionToUser(req: Request, res: Response) {
|
|
467
|
+
const validator = Validator.make(req.body || {}, {
|
|
468
|
+
userId: z.string().min(1, "userId wajib diisi"),
|
|
469
|
+
permissionId: z.string().min(1, "permissionId wajib diisi"),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (await validator.fails()) {
|
|
473
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const { userId, permissionId } = await validator.validated();
|
|
477
|
+
|
|
478
|
+
const user = users.find((u) => u.id === userId);
|
|
479
|
+
if (!user) {
|
|
480
|
+
sendError(res, 404, "User not found");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const permission = permissions.find((p) => p.id === permissionId);
|
|
484
|
+
if (!permission) {
|
|
485
|
+
sendError(res, 404, "Permission not found");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const exists = user_permissions.find(
|
|
490
|
+
(up) => up.user_id === userId && up.permission_id === permissionId
|
|
491
|
+
);
|
|
492
|
+
if (!exists) {
|
|
493
|
+
user_permissions.push({
|
|
494
|
+
id: generateId(),
|
|
495
|
+
user_id: userId,
|
|
496
|
+
permission_id: permissionId,
|
|
497
|
+
created_at: new Date(),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
502
|
+
status: "success",
|
|
503
|
+
message: "Permission assigned to user",
|
|
504
|
+
data: null,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function removePermissionFromUser(req: Request, res: Response) {
|
|
509
|
+
const validator = Validator.make(req.body || {}, {
|
|
510
|
+
userId: z.string().min(1, "userId wajib diisi"),
|
|
511
|
+
permissionId: z.string().min(1, "permissionId wajib diisi"),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (await validator.fails()) {
|
|
515
|
+
sendError(res, 422, "Validation error", validator.errors());
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const { userId, permissionId } = await validator.validated();
|
|
519
|
+
|
|
520
|
+
const index = user_permissions.findIndex(
|
|
521
|
+
(up) => up.user_id === userId && up.permission_id === permissionId
|
|
522
|
+
);
|
|
523
|
+
if (index !== -1) {
|
|
524
|
+
user_permissions.splice(index, 1);
|
|
525
|
+
saveStore();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
sendFastSuccess(res, 200, voidSerializer, {
|
|
529
|
+
status: "success",
|
|
530
|
+
message: "Permission removed from user",
|
|
531
|
+
data: null,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import rateLimit from "express-rate-limit";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
4
|
+
const multer = require("multer");
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import {
|
|
8
|
+
register,
|
|
9
|
+
login,
|
|
10
|
+
me,
|
|
11
|
+
logout,
|
|
12
|
+
refreshToken,
|
|
13
|
+
updatePassword,
|
|
14
|
+
updateProfile,
|
|
15
|
+
updateAvatar,
|
|
16
|
+
} from "@/modules/Auth/auth.controller";
|
|
17
|
+
import { requireAuth } from "@lapeeh/middleware/auth";
|
|
18
|
+
|
|
19
|
+
const authLimiter = rateLimit({
|
|
20
|
+
windowMs: 15 * 60 * 1000,
|
|
21
|
+
max: 50,
|
|
22
|
+
standardHeaders: true,
|
|
23
|
+
legacyHeaders: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const avatarUploadDir = process.env.AVATAR_UPLOAD_DIR || "uploads/avatars";
|
|
27
|
+
if (!fs.existsSync(avatarUploadDir)) {
|
|
28
|
+
fs.mkdirSync(avatarUploadDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const storage = (multer as any).diskStorage({
|
|
32
|
+
destination(
|
|
33
|
+
_req: any,
|
|
34
|
+
_file: any,
|
|
35
|
+
cb: (error: Error | null, destination: string) => void
|
|
36
|
+
) {
|
|
37
|
+
cb(null, avatarUploadDir);
|
|
38
|
+
},
|
|
39
|
+
filename(
|
|
40
|
+
_req: any,
|
|
41
|
+
file: any,
|
|
42
|
+
cb: (error: Error | null, filename: string) => void
|
|
43
|
+
) {
|
|
44
|
+
const ext = path.extname(file.originalname);
|
|
45
|
+
const base = path.basename(file.originalname, ext);
|
|
46
|
+
const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
47
|
+
cb(null, base + "-" + unique + ext);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const uploadAvatar = multer({ storage });
|
|
52
|
+
|
|
53
|
+
export const authRouter = Router();
|
|
54
|
+
|
|
55
|
+
authRouter.post("/register", authLimiter, register);
|
|
56
|
+
|
|
57
|
+
authRouter.post("/login", authLimiter, login);
|
|
58
|
+
|
|
59
|
+
authRouter.get("/me", requireAuth, me);
|
|
60
|
+
|
|
61
|
+
authRouter.post("/logout", requireAuth, logout);
|
|
62
|
+
|
|
63
|
+
authRouter.post("/refresh", authLimiter, refreshToken);
|
|
64
|
+
|
|
65
|
+
authRouter.put("/password", requireAuth, updatePassword);
|
|
66
|
+
|
|
67
|
+
authRouter.put("/profile", requireAuth, updateProfile);
|
|
68
|
+
|
|
69
|
+
authRouter.post(
|
|
70
|
+
"/avatar",
|
|
71
|
+
requireAuth,
|
|
72
|
+
uploadAvatar.single("avatar"),
|
|
73
|
+
updateAvatar
|
|
74
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { requireAdmin, requireAuth } from "@lapeeh/middleware/auth";
|
|
3
|
+
import {
|
|
4
|
+
createRole,
|
|
5
|
+
listRoles,
|
|
6
|
+
updateRole,
|
|
7
|
+
deleteRole,
|
|
8
|
+
createPermission,
|
|
9
|
+
listPermissions,
|
|
10
|
+
updatePermission,
|
|
11
|
+
deletePermission,
|
|
12
|
+
assignRoleToUser,
|
|
13
|
+
removeRoleFromUser,
|
|
14
|
+
assignPermissionToRole,
|
|
15
|
+
removePermissionFromRole,
|
|
16
|
+
assignPermissionToUser,
|
|
17
|
+
removePermissionFromUser,
|
|
18
|
+
} from "@/modules/Rbac/rbac.controller";
|
|
19
|
+
|
|
20
|
+
export const rbacRouter = Router();
|
|
21
|
+
|
|
22
|
+
rbacRouter.use(requireAuth);
|
|
23
|
+
rbacRouter.use(requireAdmin);
|
|
24
|
+
|
|
25
|
+
rbacRouter.post("/roles", createRole);
|
|
26
|
+
rbacRouter.get("/roles", listRoles);
|
|
27
|
+
rbacRouter.put("/roles/:id", updateRole);
|
|
28
|
+
rbacRouter.delete("/roles/:id", deleteRole);
|
|
29
|
+
|
|
30
|
+
rbacRouter.post("/permissions", createPermission);
|
|
31
|
+
rbacRouter.get("/permissions", listPermissions);
|
|
32
|
+
rbacRouter.put("/permissions/:id", updatePermission);
|
|
33
|
+
rbacRouter.delete("/permissions/:id", deletePermission);
|
|
34
|
+
|
|
35
|
+
rbacRouter.post("/users/assign-role", assignRoleToUser);
|
|
36
|
+
rbacRouter.post("/users/remove-role", removeRoleFromUser);
|
|
37
|
+
|
|
38
|
+
rbacRouter.post("/roles/assign-permission", assignPermissionToRole);
|
|
39
|
+
rbacRouter.post("/roles/remove-permission", removePermissionFromRole);
|
|
40
|
+
|
|
41
|
+
rbacRouter.post("/users/assign-permission", assignPermissionToUser);
|
|
42
|
+
rbacRouter.post("/users/remove-permission", removePermissionFromUser);
|
|
File without changes
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": ".",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@lapeeh/*": ["./lib/*"],
|
|
17
|
+
"@/*": ["./src/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"lib",
|
|
22
|
+
"src",
|
|
23
|
+
"generated",
|
|
24
|
+
"tests"
|
|
25
|
+
],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"node_modules",
|
|
28
|
+
"dist"
|
|
29
|
+
]
|
|
30
|
+
}
|