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.
Files changed (66) hide show
  1. package/README.md +137 -0
  2. package/bin/index.js +158 -0
  3. package/package.json +24 -0
  4. package/template/.dockerignore +4 -0
  5. package/template/Dockerfile +12 -0
  6. package/template/babel.config.json +3 -0
  7. package/template/craft/commands/build.js +15 -0
  8. package/template/craft/commands/db-fresh.js +22 -0
  9. package/template/craft/commands/db-generate.js +23 -0
  10. package/template/craft/commands/db-migrate.js +22 -0
  11. package/template/craft/commands/dev.js +16 -0
  12. package/template/craft/commands/key-generate.js +41 -0
  13. package/template/craft/commands/make-apidocs.js +121 -0
  14. package/template/craft/commands/make-command.js +38 -0
  15. package/template/craft/commands/make-dto.js +39 -0
  16. package/template/craft/commands/make-middleware.js +46 -0
  17. package/template/craft/commands/make-repository.js +36 -0
  18. package/template/craft/commands/make-route.js +88 -0
  19. package/template/craft/commands/make-service.js +39 -0
  20. package/template/craft/commands/make-test.js +48 -0
  21. package/template/craft/commands/make-utils.js +30 -0
  22. package/template/craft/commands/make-validation.js +42 -0
  23. package/template/craft/commands/make-view.js +42 -0
  24. package/template/craft/commands/start.js +29 -0
  25. package/template/craft/commands/test.js +20 -0
  26. package/template/craft.js +256 -0
  27. package/template/nodemon.json +6 -0
  28. package/template/package-lock.json +8777 -0
  29. package/template/package.json +79 -0
  30. package/template/prisma/migrations/20250518142257_create_table_users/migration.sql +13 -0
  31. package/template/prisma/migrations/migration_lock.toml +3 -0
  32. package/template/prisma/schema.prisma +22 -0
  33. package/template/prisma/seed.ts +29 -0
  34. package/template/public/assets/images/default-user.png +0 -0
  35. package/template/src/apidocs/auth-docs.ts +314 -0
  36. package/template/src/apidocs/users-docs.ts +240 -0
  37. package/template/src/config/database.ts +90 -0
  38. package/template/src/config/env.ts +29 -0
  39. package/template/src/config/logger.ts +116 -0
  40. package/template/src/config/web.ts +40 -0
  41. package/template/src/controllers/auth-controller.ts +88 -0
  42. package/template/src/controllers/user-controller.ts +79 -0
  43. package/template/src/dtos/list-dto.ts +12 -0
  44. package/template/src/dtos/user-dto.ts +57 -0
  45. package/template/src/main.ts +28 -0
  46. package/template/src/middleware/auth-middleware.ts +44 -0
  47. package/template/src/middleware/error-middleware.ts +27 -0
  48. package/template/src/middleware/http-logger-middleware.ts +31 -0
  49. package/template/src/repositories/user-repository.ts +75 -0
  50. package/template/src/routes/auth-route.ts +20 -0
  51. package/template/src/routes/main-route.ts +25 -0
  52. package/template/src/routes/user-route.ts +35 -0
  53. package/template/src/services/auth-service.ts +162 -0
  54. package/template/src/services/user-service.ts +102 -0
  55. package/template/src/types/type-request.ts +6 -0
  56. package/template/src/utils/async-handler.ts +9 -0
  57. package/template/src/utils/response-error.ts +10 -0
  58. package/template/src/utils/response.ts +60 -0
  59. package/template/src/utils/swagger.ts +135 -0
  60. package/template/src/utils/validation.ts +7 -0
  61. package/template/src/validations/user-validation.ts +127 -0
  62. package/template/src/views/index.ejs +6 -0
  63. package/template/src/views/layouts/main.ejs +14 -0
  64. package/template/src/views/partials/header.ejs +3 -0
  65. package/template/test/user.test.ts +16 -0
  66. package/template/tsconfig.json +13 -0
@@ -0,0 +1,75 @@
1
+ import { prismaClient } from "../config/database";
2
+
3
+ export class UserRepository {
4
+ static async countByEmail(email: string): Promise<number> {
5
+ return prismaClient.user.count({ where: { email } });
6
+ }
7
+
8
+ static async create(data: any) {
9
+ return prismaClient.user.create({ data });
10
+ }
11
+
12
+ static async findMany(filters: any, skip: number, take: number) {
13
+ return prismaClient.user.findMany({
14
+ where: {
15
+ AND: filters,
16
+ deleted_at: null,
17
+ },
18
+ skip,
19
+ take,
20
+ orderBy: { updated_at: "desc" },
21
+ });
22
+ }
23
+
24
+ static async count(filters: any) {
25
+ return prismaClient.user.count({
26
+ where: {
27
+ AND: filters,
28
+ deleted_at: null,
29
+ },
30
+ });
31
+ }
32
+
33
+ static async findById(id: string) {
34
+ return prismaClient.user.findUnique({
35
+ where: { id },
36
+ });
37
+ }
38
+
39
+ static async update(id: string, data: any) {
40
+ return prismaClient.user.update({
41
+ where: { id },
42
+ data,
43
+ });
44
+ }
45
+
46
+ static async delete(id: string) {
47
+ return prismaClient.user.delete({ where: { id } });
48
+ }
49
+
50
+ static async findUserByEmail(login: string) {
51
+ return prismaClient.user.findFirst({
52
+ where: {
53
+ email: login,
54
+ },
55
+ });
56
+ }
57
+
58
+ static async updateUser(data: any, id: string) {
59
+ return prismaClient.user.update({
60
+ where: { id },
61
+ data,
62
+ });
63
+ }
64
+
65
+ static async findemailExistsNotUserLoggedIn(email: string, idUser: string) {
66
+ return prismaClient.user.count({
67
+ where: {
68
+ email: email,
69
+ NOT: {
70
+ id: idUser,
71
+ },
72
+ },
73
+ });
74
+ }
75
+ }
@@ -0,0 +1,20 @@
1
+ import express from "express";
2
+ import { asyncHandler } from "../utils/async-handler";
3
+ import { authMiddleware } from "../middleware/auth-middleware";
4
+
5
+ import { AuthController } from "../controllers/auth-controller";
6
+ export const authRouter = express.Router();
7
+ authRouter.post("/api/auth/register", AuthController.register);
8
+ authRouter.post("/api/auth/login", AuthController.login);
9
+ authRouter.get("/api/auth/me", asyncHandler(authMiddleware), AuthController.me);
10
+ authRouter.post(
11
+ "/api/auth/logout",
12
+ asyncHandler(authMiddleware),
13
+ AuthController.logout
14
+ );
15
+ authRouter.get("/api/auth/refresh-token", AuthController.refreshToken);
16
+ authRouter.put(
17
+ "/api/auth/update-profile",
18
+ asyncHandler(authMiddleware),
19
+ AuthController.updateProfile
20
+ );
@@ -0,0 +1,25 @@
1
+ import express from "express";
2
+ import { successResponse } from "../utils/response";
3
+ import { authRouter } from "./auth-route";
4
+ import { userRouter } from "./user-route";
5
+
6
+ export const mainRouter = express.Router();
7
+
8
+ // mainRouter.get("/", (req, res) => {
9
+ // res.render("index", { title: "Home Page" });
10
+ // });
11
+
12
+ mainRouter.get("/", (req, res) => {
13
+ res
14
+ .status(200)
15
+ .json(successResponse(`${process.env.APP_NAME} is running`, 200))
16
+ .end();
17
+ });
18
+ mainRouter.get("/api", (req, res) => {
19
+ res
20
+ .status(200)
21
+ .json(successResponse(`${process.env.APP_NAME} api is running`, 200))
22
+ .end();
23
+ });
24
+ mainRouter.use(authRouter);
25
+ mainRouter.use(userRouter);
@@ -0,0 +1,35 @@
1
+ import express from "express";
2
+ import { UserController } from "../controllers/user-controller";
3
+ import { asyncHandler } from "../utils/async-handler";
4
+ import { authMiddleware } from "../middleware/auth-middleware";
5
+
6
+ export const userRouter = express.Router();
7
+
8
+ userRouter.post(
9
+ "/api/users",
10
+ asyncHandler(authMiddleware),
11
+ UserController.create
12
+ );
13
+
14
+ userRouter.get(
15
+ "/api/users",
16
+ asyncHandler(authMiddleware),
17
+ UserController.getAll
18
+ );
19
+ userRouter.get(
20
+ "/api/users/:id",
21
+ asyncHandler(authMiddleware),
22
+ UserController.getOne
23
+ );
24
+
25
+ userRouter.put(
26
+ "/api/users/:id",
27
+ asyncHandler(authMiddleware),
28
+ UserController.update
29
+ );
30
+
31
+ userRouter.delete(
32
+ "/api/users/:id",
33
+ asyncHandler(authMiddleware),
34
+ UserController.delete
35
+ );
@@ -0,0 +1,162 @@
1
+ import {
2
+ toUserDetailResponse,
3
+ toUserResponse,
4
+ UserDetailResponse,
5
+ UserResponse,
6
+ loginRequest,
7
+ CreateUserRequest,
8
+ UpdateUserRequest,
9
+ } from "../dtos/user-dto";
10
+ import { ResponseError } from "../utils/response-error";
11
+ import { UserValidation } from "../validations/user-validation";
12
+ import { Validation } from "../utils/validation";
13
+ import * as argon2 from "argon2";
14
+ import { User } from "@prisma/client";
15
+ import jwt from "jsonwebtoken";
16
+ import { Request } from "express";
17
+ import { UserRepository } from "../repositories/user-repository";
18
+ import { UserRequest } from "../types/type-request";
19
+ import { prismaClient } from "../config/database";
20
+ import { env } from "../config/env";
21
+
22
+ export class AuthService {
23
+ static async register(request: CreateUserRequest): Promise<UserResponse> {
24
+ const data = Validation.validate(UserValidation.REGISTER, request);
25
+ const emailExits = await UserRepository.countByEmail(data.email);
26
+
27
+ if (emailExits != 0) {
28
+ throw new ResponseError(409, "Akun Sudah Terdaftar!");
29
+ }
30
+
31
+ data.password = await argon2.hash(data.password);
32
+
33
+ const response = await UserRepository.create({
34
+ fullName: data.fullName,
35
+ email: data.email,
36
+ password: data.password,
37
+ });
38
+ return toUserResponse(response);
39
+ }
40
+
41
+ static async login(request: loginRequest) {
42
+ const data = Validation.validate(UserValidation.LOGIN, request);
43
+ const userExits = await UserRepository.findUserByEmail(data.email);
44
+
45
+ if (!userExits) {
46
+ throw new ResponseError(401, "Gagal Login! Detail login salah");
47
+ }
48
+
49
+ const isPasswordValid = await argon2.verify(
50
+ userExits.password,
51
+ data.password
52
+ );
53
+ if (!isPasswordValid) {
54
+ throw new ResponseError(401, "Gagal Login! Detail login salah");
55
+ }
56
+
57
+ const refreshToken = jwt.sign(
58
+ {
59
+ user_id: userExits.id,
60
+ user_fullName: userExits.fullName,
61
+ user_email: userExits.email,
62
+ },
63
+ env.JWT_SECRET as string,
64
+ {
65
+ expiresIn: "1d",
66
+ }
67
+ );
68
+
69
+ const accessToken = jwt.sign(
70
+ {
71
+ user_id: userExits.id,
72
+ user_fullName: userExits.fullName,
73
+ user_email: userExits.email,
74
+ },
75
+ env.JWT_SECRET as string,
76
+ {
77
+ expiresIn: "5m",
78
+ }
79
+ );
80
+
81
+ const user = toUserResponse(userExits);
82
+ return { user, refreshToken, accessToken };
83
+ }
84
+
85
+ static async me(user: User): Promise<UserDetailResponse> {
86
+ return toUserDetailResponse(user);
87
+ }
88
+
89
+ static async updateProfile(
90
+ user: User,
91
+ request: UpdateUserRequest
92
+ ): Promise<UserResponse> {
93
+ const data = Validation.validate(UserValidation.UPDATE, request);
94
+ if (data.fullName) {
95
+ user.fullName = data.fullName;
96
+ }
97
+ if (data.password) {
98
+ user.password = await argon2.hash(data.password);
99
+ }
100
+
101
+ if (data.email && data.email !== user.email) {
102
+ const emailExists = await UserRepository.findemailExistsNotUserLoggedIn(
103
+ data.email,
104
+ user.id
105
+ );
106
+ if (emailExists != 0) {
107
+ throw new ResponseError(409, "Email Sudah Ada");
108
+ }
109
+ user.email = data.email;
110
+ }
111
+ const result = await UserRepository.updateUser(
112
+ {
113
+ fullName: user.fullName,
114
+ password: user.password,
115
+ email: user.email,
116
+ },
117
+ user.id
118
+ );
119
+ return toUserResponse(result);
120
+ }
121
+
122
+ static async logout(req: UserRequest) {
123
+ const refreshToken = req.cookies.refresh_token;
124
+ if (!req.user) {
125
+ throw new ResponseError(401, "Unauthorized: Anda Belum Login.");
126
+ }
127
+ if (!refreshToken) {
128
+ throw new ResponseError(401, "Unauthorized: Anda Belum Login.");
129
+ }
130
+ return refreshToken;
131
+ }
132
+
133
+ static async refreshToken(req: Request) {
134
+ const refreshToken = req.cookies.refresh_token;
135
+ if (!refreshToken) {
136
+ throw new ResponseError(401, "Unauthorized, Anda Belum Login");
137
+ }
138
+
139
+ try {
140
+ const decoded = jwt.verify(refreshToken, env.JWT_SECRET as string) as any;
141
+
142
+ const user = await prismaClient.user.findUnique({
143
+ where: { id: decoded.user_id },
144
+ });
145
+
146
+ if (!user) {
147
+ throw new ResponseError(401, "Unauthorized, Anda Belum Login.");
148
+ }
149
+ const payload = {
150
+ user_id: user.id,
151
+ user_fullName: user.fullName,
152
+ user_email: user.email,
153
+ };
154
+ const accessToken = jwt.sign(payload, env.JWT_SECRET as string, {
155
+ expiresIn: "6m",
156
+ });
157
+ return { accessToken, user: payload };
158
+ } catch (err) {
159
+ throw new ResponseError(401, "Token Tidak Valid Atau Kadaluarsa");
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,102 @@
1
+ import {
2
+ CreateUserRequest,
3
+ ListUserRequest,
4
+ UpdateUserRequest,
5
+ toUserResponse,
6
+ UserResponse,
7
+ } from "../dtos/user-dto";
8
+ import { ResponseError } from "../utils/response-error";
9
+ import { UserValidation } from "../validations/user-validation";
10
+ import { Validation } from "../utils/validation";
11
+ import * as argon2 from "argon2";
12
+ import { listResponse, tolistResponse } from "../dtos/list-dto";
13
+ import { UserRepository } from "../repositories/user-repository";
14
+ import { env } from "../config/env";
15
+ export class UserService {
16
+ static async create(request: CreateUserRequest): Promise<UserResponse> {
17
+ const data = Validation.validate(UserValidation.CREATE, request);
18
+ const emailExits = await UserRepository.countByEmail(data.email);
19
+ if (emailExits != 0) {
20
+ throw new ResponseError(409, "Email Sudah Terdaftar");
21
+ }
22
+ data.password = await argon2.hash(data.password);
23
+
24
+ const response = await UserRepository.create({
25
+ fullName: data.fullName,
26
+ email: data.email,
27
+ password: data.password,
28
+ });
29
+ return toUserResponse(response);
30
+ }
31
+ static async getAll(request: ListUserRequest): Promise<listResponse> {
32
+ const requestList = Validation.validate(UserValidation.LIST, request);
33
+ const filters = [];
34
+ if (requestList.name) {
35
+ filters.push({
36
+ fullName: {
37
+ contains: requestList.name,
38
+ },
39
+ });
40
+ }
41
+ const data = await UserRepository.findMany(
42
+ filters,
43
+ requestList.skip,
44
+ requestList.take
45
+ );
46
+ const totalData = await UserRepository.count(filters);
47
+ const result = {
48
+ data,
49
+ total_data: totalData,
50
+ paging: {
51
+ current_page: requestList.page,
52
+ total_page: Math.ceil(totalData / requestList.take),
53
+ },
54
+ };
55
+ return tolistResponse(result);
56
+ }
57
+
58
+ static async getOne(id: string): Promise<UserResponse> {
59
+ const data = await UserRepository.findById(id);
60
+ if (!data) {
61
+ throw new ResponseError(404, "Data Tidak Ditemukan");
62
+ }
63
+ return toUserResponse(data);
64
+ }
65
+ static async update(
66
+ id: string,
67
+ request: UpdateUserRequest
68
+ ): Promise<UserResponse> {
69
+ const data = Validation.validate(UserValidation.UPDATE, request);
70
+ const user = await UserRepository.findById(id);
71
+ if (!user) {
72
+ throw new ResponseError(404, "Data Tidak Ditemukan");
73
+ }
74
+ if (data.fullName) {
75
+ user.fullName = data.fullName;
76
+ }
77
+ if (data.password) {
78
+ user.password = await argon2.hash(data.password);
79
+ }
80
+ if (data.email && data.email !== user.email) {
81
+ const emailExists = await UserRepository.countByEmail(data.email);
82
+ if (emailExists != 0) {
83
+ throw new ResponseError(409, "Email Sudah Ada");
84
+ }
85
+ user.email = data.email;
86
+ }
87
+ const result = await UserRepository.update(id, {
88
+ fullName: user.fullName,
89
+ password: user.password,
90
+ email: user.email,
91
+ });
92
+ return toUserResponse(result);
93
+ }
94
+
95
+ static async delete(id: string) {
96
+ const data = await UserRepository.findById(id);
97
+ if (!data) {
98
+ throw new ResponseError(404, "Data Tidak Ditemukan");
99
+ }
100
+ await UserRepository.delete(id);
101
+ }
102
+ }
@@ -0,0 +1,6 @@
1
+ import { User } from "@prisma/client";
2
+ import { Request } from "express";
3
+
4
+ export interface UserRequest extends Request {
5
+ user?: User;
6
+ }
@@ -0,0 +1,9 @@
1
+ import { NextFunction, Request, Response } from "express";
2
+
3
+ export const asyncHandler = (
4
+ fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
5
+ ) => {
6
+ return (req: Request, res: Response, next: NextFunction) => {
7
+ fn(req, res, next).catch(next);
8
+ };
9
+ };
@@ -0,0 +1,10 @@
1
+ export class ResponseError extends Error {
2
+ public status: boolean;
3
+ constructor(
4
+ public status_code: number,
5
+ public message: string
6
+ ) {
7
+ super(message);
8
+ this.status = false;
9
+ }
10
+ }
@@ -0,0 +1,60 @@
1
+ export function successResponse(
2
+ message: string,
3
+ statusCode: number = 200,
4
+ data?: any
5
+ ) {
6
+ return {
7
+ status: true,
8
+ status_code: statusCode,
9
+ message,
10
+ ...(data !== undefined && { data }),
11
+ };
12
+ }
13
+
14
+ export function successCreateResponse(data?: any) {
15
+ return {
16
+ status: true,
17
+ status_code: 201,
18
+ message: "Data Berhasil Ditambahkan",
19
+ ...(data !== undefined && { data }),
20
+ };
21
+ }
22
+
23
+ export function successUpdateResponse(data?: any) {
24
+ return {
25
+ status: true,
26
+ status_code: 200,
27
+ message: "Data Berhasil Diupdate",
28
+ ...(data !== undefined && { data }),
29
+ };
30
+ }
31
+
32
+ export function successDeleteResponse() {
33
+ return {
34
+ status: true,
35
+ status_code: 200,
36
+ message: "Data Berhasil Dihapus",
37
+ data: null,
38
+ };
39
+ }
40
+ export function successRestoreResponse() {
41
+ return {
42
+ status: true,
43
+ status_code: 200,
44
+ message: "Data Berhasil Direstore",
45
+ data: null,
46
+ };
47
+ }
48
+
49
+ export function errorResponse(
50
+ message: string,
51
+ statusCode: number = 400,
52
+ errors?: any
53
+ ) {
54
+ return {
55
+ status: false,
56
+ status_code: statusCode,
57
+ message,
58
+ ...(errors !== undefined && { errors }),
59
+ };
60
+ }
@@ -0,0 +1,135 @@
1
+ import swaggerJSDoc from "swagger-jsdoc";
2
+ import swaggerUi from "swagger-ui-express";
3
+ import { Express } from "express";
4
+ import { SwaggerTheme } from "swagger-themes";
5
+ import { env } from "../config/env";
6
+
7
+ const swaggerOptions: swaggerJSDoc.Options = {
8
+ definition: {
9
+ openapi: "3.0.0",
10
+ info: {
11
+ title: `${env.APP_NAME} Api Documentation`,
12
+ version: "1.0.0",
13
+ },
14
+ servers: [
15
+ {
16
+ url: `${env.BASE_URL}`,
17
+ description: "Local server",
18
+ },
19
+ ],
20
+ components: {
21
+ securitySchemes: {
22
+ bearerAuth: {
23
+ type: "http",
24
+ scheme: "bearer",
25
+ bearerFormat: "JWT",
26
+ },
27
+ },
28
+ schemas: {
29
+ User: {
30
+ type: "object",
31
+ properties: {
32
+ id: {
33
+ type: "string",
34
+ format: "uuid",
35
+ },
36
+ fullName: {
37
+ type: "string",
38
+ },
39
+ email: {
40
+ type: "string",
41
+ format: "email",
42
+ },
43
+ password: {
44
+ type: "string",
45
+ },
46
+ created_at: {
47
+ type: "string",
48
+ format: "date-time",
49
+ },
50
+ updated_at: {
51
+ type: "string",
52
+ format: "date-time",
53
+ },
54
+ deleted_at: {
55
+ type: "string",
56
+ format: "date-time",
57
+ },
58
+ },
59
+ required: ["fullName", "email", "password"],
60
+ },
61
+ },
62
+ responses: {
63
+ UnauthorizedATError: {
64
+ description: "Unauthorized – token tidak valid atau tidak ada",
65
+ content: {
66
+ "application/json": {
67
+ example: {
68
+ status: false,
69
+ status_code: 401,
70
+ message: "Unauthorized: Access Token Tidak Valid.",
71
+ },
72
+ },
73
+ },
74
+ },
75
+ UnauthorizedNotLoginError: {
76
+ description: "Unauthorized - Belum Login.",
77
+ content: {
78
+ "application/json": {
79
+ example: {
80
+ status: false,
81
+ status_code: 401,
82
+ message: "Unauthorized: Anda Belum Login.",
83
+ },
84
+ },
85
+ },
86
+ },
87
+ ValidationError: {
88
+ description: "Validation Error",
89
+ content: {
90
+ "application/json": {
91
+ example: {
92
+ status: false,
93
+ status_code: 400,
94
+ message: "Validation Error",
95
+ errors: {
96
+ field: "string",
97
+ message: "string",
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+ NotFoundError: {
104
+ description: "Data tidak ditemukan",
105
+ content: {
106
+ "application/json": {
107
+ example: {
108
+ status: false,
109
+ status_code: 404,
110
+ message: "Data Tidak Ditemukan",
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ security: [{ bearerAuth: [] }],
118
+ },
119
+ // apis: ["./src/route/**/*.ts"],
120
+ apis: ["./src/apidocs/**/*.ts"],
121
+ };
122
+
123
+ const swaggerSpec = swaggerJSDoc(swaggerOptions);
124
+ const theme = new SwaggerTheme();
125
+ const themeCss = theme.getBuffer("dark-monokai" as any);
126
+ export function setupSwagger(app: Express) {
127
+ app.use(
128
+ "/api/docs",
129
+ swaggerUi.serve,
130
+ swaggerUi.setup(swaggerSpec, {
131
+ customCss: themeCss,
132
+ customSiteTitle: `${env.APP_NAME} Api Documentation`,
133
+ })
134
+ );
135
+ }
@@ -0,0 +1,7 @@
1
+ import { ZodType } from "zod";
2
+
3
+ export class Validation {
4
+ static validate<T>(schema: ZodType, data: T): T {
5
+ return schema.parse(data);
6
+ }
7
+ }