lapeh 1.0.1

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.
@@ -0,0 +1,158 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const moduleName = process.argv[2];
5
+
6
+ if (!moduleName) {
7
+ console.error('❌ Please specify the module name.');
8
+ console.error(' Usage: npm run make:module <name>');
9
+ console.error(' Example: npm run make:module product');
10
+ process.exit(1);
11
+ }
12
+
13
+ // Convert "product" -> "Product" (PascalCase) for Class names
14
+ const PascalCaseName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
15
+ // Convert "product" -> "product" (camelCase) for variables/files
16
+ const camelCaseName = moduleName.toLowerCase();
17
+
18
+ const srcDir = path.join(__dirname, '..', 'src');
19
+
20
+ // 1. Create Controller
21
+ const controllerContent = `import { Request, Response } from 'express';
22
+ import { ${PascalCaseName}Service } from '../services/${camelCaseName}Service';
23
+ import { successResponse, errorResponse } from '../utils/response';
24
+
25
+ export const ${PascalCaseName}Controller = {
26
+ async getAll(req: Request, res: Response) {
27
+ try {
28
+ const data = await ${PascalCaseName}Service.getAll();
29
+ return successResponse(res, data, '${PascalCaseName}s retrieved successfully');
30
+ } catch (error) {
31
+ return errorResponse(res, error as Error);
32
+ }
33
+ },
34
+
35
+ async getById(req: Request, res: Response) {
36
+ try {
37
+ const { id } = req.params;
38
+ const data = await ${PascalCaseName}Service.getById(id);
39
+ if (!data) return errorResponse(res, new Error('${PascalCaseName} not found'), 404);
40
+ return successResponse(res, data, '${PascalCaseName} retrieved successfully');
41
+ } catch (error) {
42
+ return errorResponse(res, error as Error);
43
+ }
44
+ },
45
+
46
+ async create(req: Request, res: Response) {
47
+ try {
48
+ const data = await ${PascalCaseName}Service.create(req.body);
49
+ return successResponse(res, data, '${PascalCaseName} created successfully', 201);
50
+ } catch (error) {
51
+ return errorResponse(res, error as Error);
52
+ }
53
+ },
54
+
55
+ async update(req: Request, res: Response) {
56
+ try {
57
+ const { id } = req.params;
58
+ const data = await ${PascalCaseName}Service.update(id, req.body);
59
+ return successResponse(res, data, '${PascalCaseName} updated successfully');
60
+ } catch (error) {
61
+ return errorResponse(res, error as Error);
62
+ }
63
+ },
64
+
65
+ async delete(req: Request, res: Response) {
66
+ try {
67
+ const { id } = req.params;
68
+ await ${PascalCaseName}Service.delete(id);
69
+ return successResponse(res, null, '${PascalCaseName} deleted successfully');
70
+ } catch (error) {
71
+ return errorResponse(res, error as Error);
72
+ }
73
+ }
74
+ };
75
+ `;
76
+
77
+ // 2. Create Service
78
+ const serviceContent = `// import prisma from '../prisma'; // Uncomment this line if you use Prisma
79
+
80
+ export const ${PascalCaseName}Service = {
81
+ async getAll() {
82
+ // return prisma.${camelCaseName}.findMany();
83
+ return [{ id: 1, name: 'Sample ${PascalCaseName}' }]; // Placeholder
84
+ },
85
+
86
+ async getById(id: string) {
87
+ // return prisma.${camelCaseName}.findUnique({ where: { id } });
88
+ return { id, name: 'Sample ${PascalCaseName}' }; // Placeholder
89
+ },
90
+
91
+ async create(data: any) {
92
+ // return prisma.${camelCaseName}.create({ data });
93
+ return { id: Date.now(), ...data }; // Placeholder
94
+ },
95
+
96
+ async update(id: string, data: any) {
97
+ // return prisma.${camelCaseName}.update({ where: { id }, data });
98
+ return { id, ...data }; // Placeholder
99
+ },
100
+
101
+ async delete(id: string) {
102
+ // return prisma.${camelCaseName}.delete({ where: { id } });
103
+ return true; // Placeholder
104
+ }
105
+ };
106
+ `;
107
+
108
+ // 3. Create Route
109
+ const routeContent = `import { Router } from 'express';
110
+ import { ${PascalCaseName}Controller } from '../controllers/${camelCaseName}Controller';
111
+ import { authenticateToken } from '../middleware/auth';
112
+
113
+ const router = Router();
114
+
115
+ router.get('/', authenticateToken, ${PascalCaseName}Controller.getAll);
116
+ router.get('/:id', authenticateToken, ${PascalCaseName}Controller.getById);
117
+ router.post('/', authenticateToken, ${PascalCaseName}Controller.create);
118
+ router.put('/:id', authenticateToken, ${PascalCaseName}Controller.update);
119
+ router.delete('/:id', authenticateToken, ${PascalCaseName}Controller.delete);
120
+
121
+ export default router;
122
+ `;
123
+
124
+ const paths = {
125
+ controller: path.join(srcDir, 'controllers', `${camelCaseName}Controller.ts`),
126
+ service: path.join(srcDir, 'services', `${camelCaseName}Service.ts`),
127
+ route: path.join(srcDir, 'routes', `${camelCaseName}.ts`),
128
+ };
129
+
130
+ // Helper to create directory if not exists
131
+ function ensureDir(filePath) {
132
+ const dirname = path.dirname(filePath);
133
+ if (!fs.existsSync(dirname)) {
134
+ fs.mkdirSync(dirname, { recursive: true });
135
+ }
136
+ }
137
+
138
+ try {
139
+ ensureDir(paths.controller);
140
+ fs.writeFileSync(paths.controller, controllerContent);
141
+ console.log(`✅ Created Controller: src/controllers/${camelCaseName}Controller.ts`);
142
+
143
+ ensureDir(paths.service);
144
+ fs.writeFileSync(paths.service, serviceContent);
145
+ console.log(`✅ Created Service: src/services/${camelCaseName}Service.ts`);
146
+
147
+ ensureDir(paths.route);
148
+ fs.writeFileSync(paths.route, routeContent);
149
+ console.log(`✅ Created Route: src/routes/${camelCaseName}.ts`);
150
+
151
+ console.log('\n⚠️ Don\'t forget to register the new route in src/index.ts or src/server.ts!');
152
+ console.log(` import ${camelCaseName}Routes from './routes/${camelCaseName}';`);
153
+ console.log(` app.use('/${camelCaseName}s', ${camelCaseName}Routes);`);
154
+
155
+ } catch (error) {
156
+ console.error('❌ Error creating module:', error);
157
+ process.exit(1);
158
+ }
@@ -0,0 +1,347 @@
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";
14
+
15
+ export const ACCESS_TOKEN_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
16
+
17
+ 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 sudah terdaftar, silakan gunakan email lain",
30
+ });
31
+ return;
32
+ }
33
+ const hash = await bcrypt.hash(password, 10);
34
+ const user = await prisma.users.create({
35
+ data: {
36
+ email,
37
+ name,
38
+ password: hash,
39
+ uuid: uuidv4(),
40
+ created_at: new Date(),
41
+ updated_at: new Date(),
42
+ },
43
+ });
44
+
45
+ const defaultRole = await prisma.roles.findUnique({
46
+ where: { slug: "user" },
47
+ });
48
+ if (defaultRole) {
49
+ await prisma.user_roles.create({
50
+ data: {
51
+ user_id: user.id,
52
+ role_id: defaultRole.id,
53
+ created_at: new Date(),
54
+ },
55
+ });
56
+ }
57
+
58
+ sendSuccess(res, 200, "Registrasi berhasil", {
59
+ id: user.id.toString(),
60
+ email: user.email,
61
+ name: user.name,
62
+ role: defaultRole ? defaultRole.slug : "user",
63
+ });
64
+ }
65
+
66
+ 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);
71
+ return;
72
+ }
73
+ const { email, password } = parsed.data;
74
+ const user = await prisma.users.findUnique({
75
+ where: { email },
76
+ include: {
77
+ user_roles: {
78
+ include: {
79
+ roles: true,
80
+ },
81
+ },
82
+ },
83
+ });
84
+ if (!user) {
85
+ sendError(res, 401, "Email not registered", {
86
+ field: "email",
87
+ message: "Email belum terdaftar, silakan registrasi terlebih dahulu",
88
+ });
89
+ return;
90
+ }
91
+ const ok = await bcrypt.compare(password, user.password);
92
+ if (!ok) {
93
+ sendError(res, 401, "Invalid credentials", {
94
+ field: "password",
95
+ message: "Password yang Anda masukkan salah",
96
+ });
97
+ return;
98
+ }
99
+ const secret = process.env.JWT_SECRET;
100
+ if (!secret) {
101
+ sendError(res, 500, "Server misconfigured");
102
+ return;
103
+ }
104
+ const primaryUserRole =
105
+ user.user_roles && user.user_roles.length > 0 && user.user_roles[0].roles
106
+ ? user.user_roles[0].roles.slug
107
+ : "user";
108
+ const accessExpiresInSeconds = ACCESS_TOKEN_EXPIRES_IN_SECONDS;
109
+ const accessExpiresAt = new Date(
110
+ Date.now() + accessExpiresInSeconds * 1000
111
+ ).toISOString();
112
+ const token = jwt.sign(
113
+ { userId: user.id.toString(), role: primaryUserRole },
114
+ secret,
115
+ { expiresIn: accessExpiresInSeconds }
116
+ );
117
+ const refreshExpiresInSeconds = 30 * 24 * 60 * 60;
118
+ const refreshToken = jwt.sign(
119
+ {
120
+ userId: user.id.toString(),
121
+ role: primaryUserRole,
122
+ tokenType: "refresh",
123
+ },
124
+ secret,
125
+ { expiresIn: refreshExpiresInSeconds }
126
+ );
127
+ sendSuccess(res, 200, "Login berhasil", {
128
+ token,
129
+ refreshToken,
130
+ expiresIn: accessExpiresInSeconds,
131
+ expiresAt: accessExpiresAt,
132
+ name: user.name,
133
+ role: primaryUserRole,
134
+ });
135
+ }
136
+
137
+ export async function me(req: Request, res: Response) {
138
+ const payload = (req as any).user as { userId: string; role: string };
139
+ if (!payload || !payload.userId) {
140
+ sendError(res, 401, "Unauthorized");
141
+ return;
142
+ }
143
+ const user = await prisma.users.findUnique({
144
+ where: { id: BigInt(payload.userId) },
145
+ include: {
146
+ user_roles: {
147
+ include: {
148
+ roles: true,
149
+ },
150
+ },
151
+ },
152
+ });
153
+ if (!user) {
154
+ sendError(res, 404, "User not found");
155
+ return;
156
+ }
157
+ const { password, remember_token, ...rest } = user as any;
158
+ sendSuccess(res, 200, "User profile", {
159
+ ...rest,
160
+ id: user.id.toString(),
161
+ role:
162
+ user.user_roles && user.user_roles.length > 0 && user.user_roles[0].roles
163
+ ? user.user_roles[0].roles.slug
164
+ : "user",
165
+ });
166
+ }
167
+
168
+ export async function logout(req: Request, res: Response) {
169
+ sendSuccess(res, 200, "Logout berhasil", null);
170
+ }
171
+
172
+ 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);
177
+ return;
178
+ }
179
+ const secret = process.env.JWT_SECRET;
180
+ if (!secret) {
181
+ sendError(res, 500, "Server misconfigured");
182
+ return;
183
+ }
184
+ try {
185
+ const decoded = jwt.verify(parsed.data.refreshToken, secret) as {
186
+ userId: string;
187
+ role: string;
188
+ tokenType?: string;
189
+ iat: number;
190
+ exp: number;
191
+ };
192
+ if (decoded.tokenType !== "refresh") {
193
+ sendError(res, 401, "Invalid refresh token");
194
+ return;
195
+ }
196
+ const user = await prisma.users.findUnique({
197
+ where: { id: BigInt(decoded.userId) },
198
+ include: {
199
+ user_roles: {
200
+ include: {
201
+ roles: true,
202
+ },
203
+ },
204
+ },
205
+ });
206
+ if (!user) {
207
+ sendError(res, 401, "Invalid refresh token");
208
+ return;
209
+ }
210
+ const primaryUserRole =
211
+ user.user_roles && user.user_roles.length > 0 && user.user_roles[0].roles
212
+ ? user.user_roles[0].roles.slug
213
+ : "user";
214
+ const accessExpiresInSeconds = ACCESS_TOKEN_EXPIRES_IN_SECONDS;
215
+ const accessExpiresAt = new Date(
216
+ Date.now() + accessExpiresInSeconds * 1000
217
+ ).toISOString();
218
+ const token = jwt.sign(
219
+ { userId: user.id.toString(), role: primaryUserRole },
220
+ secret,
221
+ { expiresIn: accessExpiresInSeconds }
222
+ );
223
+ sendSuccess(res, 200, "Token refreshed", {
224
+ token,
225
+ expiresIn: accessExpiresInSeconds,
226
+ expiresAt: accessExpiresAt,
227
+ name: user.name,
228
+ role: primaryUserRole,
229
+ });
230
+ } catch {
231
+ sendError(res, 401, "Invalid refresh token");
232
+ }
233
+ }
234
+
235
+ export async function updateAvatar(req: Request, res: Response) {
236
+ const payload = (req as any).user as { userId: string; role: string };
237
+ if (!payload || !payload.userId) {
238
+ sendError(res, 401, "Unauthorized");
239
+ return;
240
+ }
241
+ const file = (req as any).file as {
242
+ filename: string;
243
+ path: string;
244
+ } | null;
245
+ if (!file) {
246
+ sendError(res, 400, "Avatar file wajib diupload");
247
+ return;
248
+ }
249
+ const userId = BigInt(payload.userId);
250
+ const avatar = file.filename;
251
+ const avatar_url =
252
+ process.env.AVATAR_BASE_URL || `/uploads/avatars/${file.filename}`;
253
+ const updated = await prisma.users.update({
254
+ where: { id: userId },
255
+ data: {
256
+ avatar,
257
+ avatar_url,
258
+ updated_at: new Date(),
259
+ },
260
+ });
261
+ const { password, remember_token, ...rest } = updated as any;
262
+ sendSuccess(res, 200, "Avatar berhasil diperbarui", {
263
+ ...rest,
264
+ id: updated.id.toString(),
265
+ });
266
+ }
267
+
268
+ export async function updatePassword(req: Request, res: Response) {
269
+ const payload = (req as any).user as { userId: string; role: string };
270
+ if (!payload || !payload.userId) {
271
+ sendError(res, 401, "Unauthorized");
272
+ return;
273
+ }
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);
278
+ return;
279
+ }
280
+ const { currentPassword, newPassword } = parsed.data;
281
+ const user = await prisma.users.findUnique({
282
+ where: { id: BigInt(payload.userId) },
283
+ });
284
+ if (!user) {
285
+ sendError(res, 404, "User not found");
286
+ return;
287
+ }
288
+ const ok = await bcrypt.compare(currentPassword, user.password);
289
+ if (!ok) {
290
+ sendError(res, 401, "Invalid credentials", {
291
+ field: "currentPassword",
292
+ message: "Password saat ini tidak sesuai",
293
+ });
294
+ return;
295
+ }
296
+ const hash = await bcrypt.hash(newPassword, 10);
297
+ await prisma.users.update({
298
+ where: { id: user.id },
299
+ data: {
300
+ password: hash,
301
+ updated_at: new Date(),
302
+ },
303
+ });
304
+ sendSuccess(res, 200, "Password berhasil diperbarui", null);
305
+ }
306
+
307
+ export async function updateProfile(req: Request, res: Response) {
308
+ const payload = (req as any).user as { userId: string; role: string };
309
+ if (!payload || !payload.userId) {
310
+ sendError(res, 401, "Unauthorized");
311
+ return;
312
+ }
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
+ },
326
+ });
327
+ if (existing) {
328
+ sendError(res, 409, "Email already used", {
329
+ field: "email",
330
+ message: "Email sudah terdaftar, silakan gunakan email lain",
331
+ });
332
+ return;
333
+ }
334
+ const updated = await prisma.users.update({
335
+ where: { id: userId },
336
+ data: {
337
+ name,
338
+ email,
339
+ updated_at: new Date(),
340
+ },
341
+ });
342
+ const { password, remember_token, ...rest } = updated as any;
343
+ sendSuccess(res, 200, "Profil berhasil diperbarui", {
344
+ ...rest,
345
+ id: updated.id.toString(),
346
+ });
347
+ }