create-craftjs 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/README.md +137 -0
- package/bin/index.js +158 -0
- package/package.json +24 -0
- package/template/.dockerignore +4 -0
- package/template/Dockerfile +12 -0
- package/template/babel.config.json +3 -0
- package/template/craft/commands/build.js +15 -0
- package/template/craft/commands/db-fresh.js +22 -0
- package/template/craft/commands/db-generate.js +23 -0
- package/template/craft/commands/db-migrate.js +22 -0
- package/template/craft/commands/dev.js +16 -0
- package/template/craft/commands/key-generate.js +41 -0
- package/template/craft/commands/make-apidocs.js +121 -0
- package/template/craft/commands/make-command.js +38 -0
- package/template/craft/commands/make-dto.js +39 -0
- package/template/craft/commands/make-middleware.js +46 -0
- package/template/craft/commands/make-repository.js +36 -0
- package/template/craft/commands/make-route.js +88 -0
- package/template/craft/commands/make-service.js +39 -0
- package/template/craft/commands/make-test.js +48 -0
- package/template/craft/commands/make-utils.js +30 -0
- package/template/craft/commands/make-validation.js +42 -0
- package/template/craft/commands/make-view.js +42 -0
- package/template/craft/commands/start.js +29 -0
- package/template/craft/commands/test.js +20 -0
- package/template/craft.js +256 -0
- package/template/nodemon.json +6 -0
- package/template/package-lock.json +8777 -0
- package/template/package.json +79 -0
- package/template/prisma/migrations/20250518142257_create_table_users/migration.sql +13 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +22 -0
- package/template/prisma/seed.ts +29 -0
- package/template/public/assets/images/default-user.png +0 -0
- package/template/src/apidocs/auth-docs.ts +314 -0
- package/template/src/apidocs/users-docs.ts +240 -0
- package/template/src/config/database.ts +90 -0
- package/template/src/config/env.ts +29 -0
- package/template/src/config/logger.ts +116 -0
- package/template/src/config/web.ts +40 -0
- package/template/src/controllers/auth-controller.ts +88 -0
- package/template/src/controllers/user-controller.ts +79 -0
- package/template/src/dtos/list-dto.ts +12 -0
- package/template/src/dtos/user-dto.ts +57 -0
- package/template/src/main.ts +28 -0
- package/template/src/middleware/auth-middleware.ts +44 -0
- package/template/src/middleware/error-middleware.ts +27 -0
- package/template/src/middleware/http-logger-middleware.ts +31 -0
- package/template/src/repositories/user-repository.ts +75 -0
- package/template/src/routes/auth-route.ts +20 -0
- package/template/src/routes/main-route.ts +25 -0
- package/template/src/routes/user-route.ts +35 -0
- package/template/src/services/auth-service.ts +162 -0
- package/template/src/services/user-service.ts +102 -0
- package/template/src/types/type-request.ts +6 -0
- package/template/src/utils/async-handler.ts +9 -0
- package/template/src/utils/response-error.ts +10 -0
- package/template/src/utils/response.ts +60 -0
- package/template/src/utils/swagger.ts +135 -0
- package/template/src/utils/validation.ts +7 -0
- package/template/src/validations/user-validation.ts +127 -0
- package/template/src/views/index.ejs +6 -0
- package/template/src/views/layouts/main.ejs +14 -0
- package/template/src/views/partials/header.ejs +3 -0
- package/template/test/user.test.ts +16 -0
- package/template/tsconfig.json +13 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { logger } from "./logger";
|
|
3
|
+
import { DateTime } from "luxon";
|
|
4
|
+
import { dbLogger } from "./logger";
|
|
5
|
+
import { env } from "./env";
|
|
6
|
+
|
|
7
|
+
export const prismaClient = new PrismaClient({
|
|
8
|
+
log: [
|
|
9
|
+
{
|
|
10
|
+
emit: "event",
|
|
11
|
+
level: "query",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
emit: "event",
|
|
15
|
+
level: "error",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
emit: "event",
|
|
19
|
+
level: "info",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
emit: "event",
|
|
23
|
+
level: "warn",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
prismaClient.$use(async (params, next) => {
|
|
29
|
+
const result = await next(params);
|
|
30
|
+
|
|
31
|
+
const convertToYourTimeZone = (date: unknown): string => {
|
|
32
|
+
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return DateTime.fromJSDate(date)
|
|
37
|
+
.setZone(env.TZ || "UTC")
|
|
38
|
+
.toFormat(env.DATETIME_FORMAT);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const transformDates = (item: any): any => {
|
|
42
|
+
if (!item) return item;
|
|
43
|
+
|
|
44
|
+
if (Array.isArray(item)) {
|
|
45
|
+
return item.map(transformDates);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof item === "object") {
|
|
49
|
+
const newItem: any = { ...item };
|
|
50
|
+
for (const key in newItem) {
|
|
51
|
+
const value = newItem[key];
|
|
52
|
+
if (
|
|
53
|
+
["created_at", "updated_at", "deleted_at"].includes(key) &&
|
|
54
|
+
value instanceof Date
|
|
55
|
+
) {
|
|
56
|
+
newItem[key] = convertToYourTimeZone(value);
|
|
57
|
+
} else if (typeof value === "object" && value !== null) {
|
|
58
|
+
newItem[key] = transformDates(value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return newItem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return item;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return transformDates(result);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
prismaClient.$on("query", (e) => {
|
|
71
|
+
dbLogger.info(`${e.query} - ${e.params}`);
|
|
72
|
+
});
|
|
73
|
+
prismaClient.$on("warn", (e) => {
|
|
74
|
+
logger.warn(e);
|
|
75
|
+
});
|
|
76
|
+
prismaClient.$on("info", (e) => {
|
|
77
|
+
logger.info(e);
|
|
78
|
+
});
|
|
79
|
+
prismaClient.$on("error", (e) => {
|
|
80
|
+
logger.error(e);
|
|
81
|
+
});
|
|
82
|
+
export const connectDatabase = async () => {
|
|
83
|
+
try {
|
|
84
|
+
await prismaClient.$connect();
|
|
85
|
+
logger.info("✅ Connected Database");
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.error("❌ Failed Connect To Database", error);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
|
|
4
|
+
const envSchema = z.object({
|
|
5
|
+
APP_NAME: z.string().default("Craft JS"),
|
|
6
|
+
APP_SECRET: z.string(),
|
|
7
|
+
NODE_ENV: z
|
|
8
|
+
.enum(["development", "production", "test"])
|
|
9
|
+
.default("development"),
|
|
10
|
+
TZ: z.string().default("Asia/Jakarta"),
|
|
11
|
+
DATETIME_FORMAT: z.string().default("dd-MM-yyyy HH:mm:ss"),
|
|
12
|
+
DATABASE_URL: z
|
|
13
|
+
.string()
|
|
14
|
+
.url({ message: "DATABASE_URL harus URL yang valid" }),
|
|
15
|
+
BASE_URL: z.string().url(),
|
|
16
|
+
BASE_API_URL: z.string().url(),
|
|
17
|
+
CLIENT_URL: z.string().url().optional(),
|
|
18
|
+
PORT: z.coerce.number().default(3000),
|
|
19
|
+
JWT_SECRET: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const _env = envSchema.safeParse(process.env);
|
|
23
|
+
|
|
24
|
+
if (!_env.success) {
|
|
25
|
+
console.error("❌ ENV ERROR:", _env.error.format());
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const env = _env.data;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import winston from "winston";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import DailyRotateFile from "winston-daily-rotate-file";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
const customFormat = winston.format.printf(({ level, message, timestamp }) => {
|
|
8
|
+
const formattedMessage =
|
|
9
|
+
typeof message === "object" ? JSON.stringify(message, null, 2) : message;
|
|
10
|
+
|
|
11
|
+
let coloredLevel = level.toUpperCase();
|
|
12
|
+
|
|
13
|
+
switch (level) {
|
|
14
|
+
case "error":
|
|
15
|
+
coloredLevel = chalk.redBright.bold(level.toUpperCase());
|
|
16
|
+
break;
|
|
17
|
+
case "warn":
|
|
18
|
+
coloredLevel = chalk.yellowBright.bold(level.toUpperCase());
|
|
19
|
+
break;
|
|
20
|
+
case "info":
|
|
21
|
+
coloredLevel = chalk.greenBright.bold(level.toUpperCase());
|
|
22
|
+
break;
|
|
23
|
+
case "debug":
|
|
24
|
+
coloredLevel = chalk.bgBlueBright.bold(level.toUpperCase());
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
coloredLevel = chalk.bgBlueBright.bold(level.toUpperCase());
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `[${chalk.gray(timestamp)}] ${coloredLevel} - ${formattedMessage}`;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const logger = winston.createLogger({
|
|
35
|
+
level: "debug",
|
|
36
|
+
format: winston.format.combine(
|
|
37
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
38
|
+
customFormat
|
|
39
|
+
),
|
|
40
|
+
transports: [new winston.transports.Console()],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const logDir = path.join(__dirname, "../../logs");
|
|
44
|
+
if (!fs.existsSync(logDir)) {
|
|
45
|
+
fs.mkdirSync(logDir);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const dbLogger = winston.createLogger({
|
|
49
|
+
level: "debug",
|
|
50
|
+
format: winston.format.combine(
|
|
51
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
52
|
+
winston.format.printf(
|
|
53
|
+
({ timestamp, level, message }) =>
|
|
54
|
+
`[${timestamp}] ${level.toUpperCase()}: ${message}`
|
|
55
|
+
)
|
|
56
|
+
),
|
|
57
|
+
transports: [
|
|
58
|
+
new DailyRotateFile({
|
|
59
|
+
filename: path.join(logDir, "database-log-%DATE%.log"),
|
|
60
|
+
datePattern: "YYYY-MM-DD",
|
|
61
|
+
zippedArchive: false,
|
|
62
|
+
maxFiles: "30d",
|
|
63
|
+
maxSize: "10m",
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Format untuk FILE logs — tanpa warna
|
|
69
|
+
const plainFormat = winston.format.printf(({ timestamp, level, message }) => {
|
|
70
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Format untuk CONSOLE logs — dengan warna
|
|
74
|
+
const coloredHttpFormat = winston.format.printf(
|
|
75
|
+
({ timestamp, level, message }) => {
|
|
76
|
+
let coloredLevel = level.toUpperCase();
|
|
77
|
+
switch (level) {
|
|
78
|
+
case "error":
|
|
79
|
+
coloredLevel = chalk.red.bold(level.toUpperCase());
|
|
80
|
+
break;
|
|
81
|
+
case "warn":
|
|
82
|
+
coloredLevel = chalk.yellow.bold(level.toUpperCase());
|
|
83
|
+
break;
|
|
84
|
+
case "info":
|
|
85
|
+
coloredLevel = chalk.blue.bold(level.toUpperCase());
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
coloredLevel = chalk.white(level.toUpperCase());
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
return `[${chalk.gray(timestamp)}] ${coloredLevel}: ${message}`;
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const httpAccessLogger = winston.createLogger({
|
|
96
|
+
level: "debug",
|
|
97
|
+
transports: [
|
|
98
|
+
new DailyRotateFile({
|
|
99
|
+
filename: path.join(logDir, "access-log-%DATE%.log"),
|
|
100
|
+
datePattern: "YYYY-MM-DD",
|
|
101
|
+
zippedArchive: false,
|
|
102
|
+
maxFiles: "30d",
|
|
103
|
+
maxSize: "10m",
|
|
104
|
+
format: winston.format.combine(
|
|
105
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
106
|
+
plainFormat
|
|
107
|
+
),
|
|
108
|
+
}),
|
|
109
|
+
new winston.transports.Console({
|
|
110
|
+
format: winston.format.combine(
|
|
111
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
112
|
+
coloredHttpFormat
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import cookieParser from "cookie-parser";
|
|
4
|
+
import { env } from "./env";
|
|
5
|
+
// import expressLayouts from "express-ejs-layouts";
|
|
6
|
+
// import path from "path";
|
|
7
|
+
|
|
8
|
+
import { errorMiddleware } from "../middleware/error-middleware";
|
|
9
|
+
import { httpLogger } from "../middleware/http-logger-middleware";
|
|
10
|
+
import { errorResponse } from "../utils/response";
|
|
11
|
+
import { setupSwagger } from "../utils/swagger";
|
|
12
|
+
import { mainRouter } from "../routes/main-route";
|
|
13
|
+
|
|
14
|
+
export const web = express();
|
|
15
|
+
// EJS View Engine Setup
|
|
16
|
+
// web.set("view engine", "ejs");
|
|
17
|
+
// web.set("views", path.join(__dirname, "..", "views"));
|
|
18
|
+
// web.use(expressLayouts);
|
|
19
|
+
// web.set("layout", "layouts/main");
|
|
20
|
+
|
|
21
|
+
// Middleware
|
|
22
|
+
web.use(express.json());
|
|
23
|
+
web.use(cookieParser());
|
|
24
|
+
web.use(cors({ credentials: true, origin: `${env.CLIENT_URL}` }));
|
|
25
|
+
web.use(express.static("public"));
|
|
26
|
+
web.use(httpLogger);
|
|
27
|
+
|
|
28
|
+
// Swagger Setup
|
|
29
|
+
setupSwagger(web);
|
|
30
|
+
|
|
31
|
+
// Routes
|
|
32
|
+
web.use(mainRouter);
|
|
33
|
+
|
|
34
|
+
// 404 Handler
|
|
35
|
+
web.use((req, res) => {
|
|
36
|
+
res.status(404).json(errorResponse("Request Tidak Ada", 404));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Global Error Handler
|
|
40
|
+
web.use(errorMiddleware);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import {
|
|
3
|
+
loginRequest,
|
|
4
|
+
CreateUserRequest,
|
|
5
|
+
UpdateUserRequest,
|
|
6
|
+
} from "../dtos/user-dto";
|
|
7
|
+
import { successResponse, successUpdateResponse } from "../utils/response";
|
|
8
|
+
import { AuthService } from "../services/auth-service";
|
|
9
|
+
import { UserRequest } from "../types/type-request";
|
|
10
|
+
import { env } from "../config/env";
|
|
11
|
+
|
|
12
|
+
export class AuthController {
|
|
13
|
+
static async register(req: Request, res: Response, next: NextFunction) {
|
|
14
|
+
try {
|
|
15
|
+
const request: CreateUserRequest = req.body as CreateUserRequest;
|
|
16
|
+
const response = await AuthService.register(request);
|
|
17
|
+
res.status(201).json(successResponse("Register Berhasil", 201, response));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
next(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static async login(req: Request, res: Response, next: NextFunction) {
|
|
24
|
+
try {
|
|
25
|
+
const request: loginRequest = req.body as loginRequest;
|
|
26
|
+
const response = await AuthService.login(request);
|
|
27
|
+
res.cookie("refresh_token", response.refreshToken, {
|
|
28
|
+
httpOnly: true,
|
|
29
|
+
maxAge: 24 * 60 * 60 * 1000,
|
|
30
|
+
secure: env.NODE_ENV === "production",
|
|
31
|
+
sameSite: "lax",
|
|
32
|
+
path: "/",
|
|
33
|
+
});
|
|
34
|
+
res.status(200).json(
|
|
35
|
+
successResponse("Login Berhasil", 200, {
|
|
36
|
+
user: response.user,
|
|
37
|
+
accessToken: response.accessToken,
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
next(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async me(req: UserRequest, res: Response, next: NextFunction) {
|
|
46
|
+
try {
|
|
47
|
+
const response = await AuthService.me(req.user!);
|
|
48
|
+
res
|
|
49
|
+
.status(200)
|
|
50
|
+
.json(successResponse("Get Detail User Berhasil", 200, response));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
next(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static async updateProfile(
|
|
57
|
+
req: UserRequest,
|
|
58
|
+
res: Response,
|
|
59
|
+
next: NextFunction
|
|
60
|
+
) {
|
|
61
|
+
try {
|
|
62
|
+
const request: UpdateUserRequest = req.body as UpdateUserRequest;
|
|
63
|
+
const response = await AuthService.updateProfile(req.user!, request);
|
|
64
|
+
res.status(200).json(successUpdateResponse(response));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
next(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static async logout(req: UserRequest, res: Response, next: NextFunction) {
|
|
71
|
+
await AuthService.logout(req);
|
|
72
|
+
res.clearCookie("refresh_token");
|
|
73
|
+
res.status(200).json(successResponse("Logout berhasil", 200));
|
|
74
|
+
}
|
|
75
|
+
static async refreshToken(req: Request, res: Response, next: NextFunction) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await AuthService.refreshToken(req);
|
|
78
|
+
res.status(200).json(
|
|
79
|
+
successResponse("Get Access Token Berhasil", 200, {
|
|
80
|
+
user: response.user,
|
|
81
|
+
accessToken: response.accessToken,
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
next(error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import {
|
|
3
|
+
CreateUserRequest,
|
|
4
|
+
ListUserRequest,
|
|
5
|
+
UpdateUserRequest,
|
|
6
|
+
} from "../dtos/user-dto";
|
|
7
|
+
import { UserService } from "../services/user-service";
|
|
8
|
+
import {
|
|
9
|
+
successCreateResponse,
|
|
10
|
+
successDeleteResponse,
|
|
11
|
+
successResponse,
|
|
12
|
+
successUpdateResponse,
|
|
13
|
+
} from "../utils/response";
|
|
14
|
+
import { UserRequest } from "../types/type-request";
|
|
15
|
+
|
|
16
|
+
export class UserController {
|
|
17
|
+
static async create(req: Request, res: Response, next: NextFunction) {
|
|
18
|
+
try {
|
|
19
|
+
const request: CreateUserRequest = req.body as CreateUserRequest;
|
|
20
|
+
console.log(request);
|
|
21
|
+
const response = await UserService.create(request);
|
|
22
|
+
res.status(201).json(successCreateResponse(response));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
next(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static async getAll(req: Request, res: Response, next: NextFunction) {
|
|
29
|
+
try {
|
|
30
|
+
const page = Number(req.query.page) || 1;
|
|
31
|
+
const take = Number(req.query.take) || 10;
|
|
32
|
+
const request: ListUserRequest = {
|
|
33
|
+
page: page,
|
|
34
|
+
take: take,
|
|
35
|
+
skip: (page - 1) * take,
|
|
36
|
+
name: req.query.name as string,
|
|
37
|
+
};
|
|
38
|
+
const response = await UserService.getAll(request);
|
|
39
|
+
res
|
|
40
|
+
.status(200)
|
|
41
|
+
.json(successResponse("Berhasil Get All Data", 200, response));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
next(e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static async getOne(req: Request, res: Response, next: NextFunction) {
|
|
48
|
+
try {
|
|
49
|
+
const id = req.params.id;
|
|
50
|
+
const response = await UserService.getOne(id);
|
|
51
|
+
res
|
|
52
|
+
.status(200)
|
|
53
|
+
.json(successResponse("Berhasil Get Detail Data", 200, response));
|
|
54
|
+
} catch (e) {
|
|
55
|
+
next(e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async update(req: UserRequest, res: Response, next: NextFunction) {
|
|
60
|
+
try {
|
|
61
|
+
const id = req.params.id;
|
|
62
|
+
const request: UpdateUserRequest = req.body as UpdateUserRequest;
|
|
63
|
+
const response = await UserService.update(id, request);
|
|
64
|
+
res.status(200).json(successUpdateResponse(response));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
next(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static async delete(req: Request, res: Response, next: NextFunction) {
|
|
71
|
+
try {
|
|
72
|
+
const id = req.params.id;
|
|
73
|
+
await UserService.delete(id);
|
|
74
|
+
res.status(200).json(successDeleteResponse());
|
|
75
|
+
} catch (e) {
|
|
76
|
+
next(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { User } from "@prisma/client";
|
|
2
|
+
export type loginRequest = {
|
|
3
|
+
email: string;
|
|
4
|
+
password: string;
|
|
5
|
+
};
|
|
6
|
+
export type CreateUserRequest = {
|
|
7
|
+
fullName: string;
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type UpdateUserRequest = {
|
|
13
|
+
fullName?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ListUserRequest = {
|
|
19
|
+
page: number;
|
|
20
|
+
take: number;
|
|
21
|
+
skip: number;
|
|
22
|
+
name?: string;
|
|
23
|
+
};
|
|
24
|
+
export type UserDetailResponse = {
|
|
25
|
+
id: string;
|
|
26
|
+
fullName: string;
|
|
27
|
+
email: string;
|
|
28
|
+
image_id?: string;
|
|
29
|
+
image_url?: string;
|
|
30
|
+
created_at: Date;
|
|
31
|
+
updated_at: Date;
|
|
32
|
+
deleted_at?: Date;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type UserResponse = {
|
|
36
|
+
id: string;
|
|
37
|
+
fullName: string;
|
|
38
|
+
email: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function toUserDetailResponse(user: User): UserDetailResponse {
|
|
42
|
+
return {
|
|
43
|
+
id: user.id,
|
|
44
|
+
fullName: user.fullName,
|
|
45
|
+
email: user.email,
|
|
46
|
+
created_at: user.created_at,
|
|
47
|
+
updated_at: user.updated_at,
|
|
48
|
+
deleted_at: user.deleted_at!,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function toUserResponse(user: User): UserResponse {
|
|
52
|
+
return {
|
|
53
|
+
id: user.id,
|
|
54
|
+
fullName: user.fullName,
|
|
55
|
+
email: user.email,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { web } from "./config/web";
|
|
2
|
+
import { connectDatabase } from "./config/database";
|
|
3
|
+
import { env } from "./config/env";
|
|
4
|
+
import { logger } from "./config/logger";
|
|
5
|
+
|
|
6
|
+
async function startServer() {
|
|
7
|
+
try {
|
|
8
|
+
if (
|
|
9
|
+
!env.APP_SECRET ||
|
|
10
|
+
env.APP_SECRET.trim() === "" ||
|
|
11
|
+
!env.JWT_SECRET ||
|
|
12
|
+
env.JWT_SECRET.trim() === ""
|
|
13
|
+
) {
|
|
14
|
+
logger.error("❌ APP_SECRET or JWT_SECRET is missing in your .env file.");
|
|
15
|
+
logger.error("👉 Please run `node craft key:generate` to create them.");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
await connectDatabase();
|
|
19
|
+
web.listen(env.PORT, () => {
|
|
20
|
+
logger.info(`🚀 Server is listening on: ${env.BASE_URL}`);
|
|
21
|
+
logger.info(`🔗 API Docs available at: ${env.BASE_API_URL}/docs`);
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
startServer();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextFunction, Response } from "express";
|
|
2
|
+
import { errorResponse } from "../utils/response";
|
|
3
|
+
import { prismaClient } from "../config/database";
|
|
4
|
+
import jwt from "jsonwebtoken";
|
|
5
|
+
import { UserRequest } from "../types/type-request";
|
|
6
|
+
import { ResponseError } from "../utils/response-error";
|
|
7
|
+
import { env } from "../config/env";
|
|
8
|
+
|
|
9
|
+
export const authMiddleware = async (
|
|
10
|
+
req: UserRequest,
|
|
11
|
+
res: Response,
|
|
12
|
+
next: NextFunction
|
|
13
|
+
) => {
|
|
14
|
+
const refreshToken = req.cookies.refresh_token;
|
|
15
|
+
if (!refreshToken) {
|
|
16
|
+
throw new ResponseError(401, "Unauthorized: Anda Belum Login.");
|
|
17
|
+
}
|
|
18
|
+
const token = req.get("Authorization")?.split(" ")[1];
|
|
19
|
+
if (!token) {
|
|
20
|
+
return res
|
|
21
|
+
.status(401)
|
|
22
|
+
.json(errorResponse("Unauthorized: Access Token Tidak Valid.", 401));
|
|
23
|
+
}
|
|
24
|
+
let payload;
|
|
25
|
+
try {
|
|
26
|
+
payload = jwt.verify(token, env.JWT_SECRET as string) as {
|
|
27
|
+
user_id: string;
|
|
28
|
+
user_email: string;
|
|
29
|
+
user_fullName: string;
|
|
30
|
+
};
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new ResponseError(401, "Unauthorized: Access Token Tidak Valid.");
|
|
33
|
+
}
|
|
34
|
+
const user = await prismaClient.user.findUnique({
|
|
35
|
+
where: { id: payload.user_id },
|
|
36
|
+
});
|
|
37
|
+
if (!user) {
|
|
38
|
+
return res
|
|
39
|
+
.status(401)
|
|
40
|
+
.json(errorResponse("Unauthorized: Anda belum login", 401));
|
|
41
|
+
}
|
|
42
|
+
req.user = user;
|
|
43
|
+
next();
|
|
44
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import { errorResponse } from "../utils/response";
|
|
4
|
+
import { ResponseError } from "../utils/response-error";
|
|
5
|
+
|
|
6
|
+
export const errorMiddleware = async (
|
|
7
|
+
error: Error,
|
|
8
|
+
req: Request,
|
|
9
|
+
res: Response,
|
|
10
|
+
next: NextFunction
|
|
11
|
+
) => {
|
|
12
|
+
if (error instanceof ZodError) {
|
|
13
|
+
let formattedErrors = error.errors.map((err) => ({
|
|
14
|
+
field: err.path.join("."),
|
|
15
|
+
message: err.message,
|
|
16
|
+
}));
|
|
17
|
+
res
|
|
18
|
+
.status(400)
|
|
19
|
+
.json(errorResponse("Validation Error", 400, formattedErrors));
|
|
20
|
+
} else if (error instanceof ResponseError) {
|
|
21
|
+
res
|
|
22
|
+
.status(error.status_code)
|
|
23
|
+
.json(errorResponse(error.message, error.status_code));
|
|
24
|
+
} else {
|
|
25
|
+
res.status(500).json(errorResponse(error.message, 500));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { httpAccessLogger } from "../config/logger";
|
|
3
|
+
|
|
4
|
+
export const httpLogger = async (
|
|
5
|
+
req: Request,
|
|
6
|
+
res: Response,
|
|
7
|
+
next: NextFunction
|
|
8
|
+
) => {
|
|
9
|
+
const start = process.hrtime();
|
|
10
|
+
|
|
11
|
+
res.on("finish", () => {
|
|
12
|
+
const diff = process.hrtime(start);
|
|
13
|
+
const timeInMs = (diff[0] * 1e3 + diff[1] / 1e6).toFixed(3);
|
|
14
|
+
|
|
15
|
+
const logMessage = `${req.method} ${req.originalUrl} ${res.statusCode} ${timeInMs} ms - ${res.get("Content-Length") || 0}`;
|
|
16
|
+
|
|
17
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
18
|
+
httpAccessLogger.info(logMessage);
|
|
19
|
+
} else if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
20
|
+
httpAccessLogger.info(logMessage);
|
|
21
|
+
} else if (res.statusCode >= 400 && res.statusCode < 500) {
|
|
22
|
+
httpAccessLogger.warn(logMessage);
|
|
23
|
+
} else if (res.statusCode >= 500) {
|
|
24
|
+
httpAccessLogger.error(logMessage);
|
|
25
|
+
} else {
|
|
26
|
+
httpAccessLogger.debug(logMessage);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
next();
|
|
31
|
+
};
|