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.
- package/.env.example +16 -0
- package/bin/index.js +105 -0
- package/docker-compose.yml +17 -0
- package/package.json +70 -0
- package/prisma/base.prisma +8 -0
- package/prisma/migrations/20251225163737_init/migration.sql +236 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +174 -0
- package/prisma/seed.ts +350 -0
- package/prisma.config.ts +15 -0
- package/readme.md +120 -0
- package/scripts/compile-schema.js +29 -0
- package/scripts/generate-jwt-secret.js +38 -0
- package/scripts/make-model.js +42 -0
- package/scripts/make-module.js +158 -0
- package/src/controllers/authController.ts +347 -0
- package/src/controllers/rbacController.ts +353 -0
- package/src/index.ts +29 -0
- package/src/middleware/auth.ts +56 -0
- package/src/middleware/error.ts +8 -0
- package/src/middleware/visitor.ts +180 -0
- package/src/models/schema.prisma +159 -0
- package/src/prisma.ts +32 -0
- package/src/realtime.ts +34 -0
- package/src/redis.ts +69 -0
- package/src/routes/auth.ts +74 -0
- package/src/routes/rbac.ts +55 -0
- package/src/schema/auth-schema.ts +62 -0
- package/src/schema/user-schema.ts +57 -0
- package/src/server.ts +32 -0
- package/src/utils/pagination.ts +56 -0
- package/src/utils/response.ts +59 -0
- package/tsconfig.json +12 -0
|
@@ -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
|
+
}
|