create-craftjs 1.0.14 → 1.0.15

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 (35) hide show
  1. package/bin/index.js +1 -0
  2. package/package.json +1 -1
  3. package/template/craft/commands/key-generate.js +12 -1
  4. package/template/package-lock.json +2 -2
  5. package/template/package.json +1 -1
  6. package/template/prisma/migrations/20250518142257_create_table_users/migration.sql +1 -1
  7. package/template/prisma/migrations/20260118135332_create_table_auth_refresh_tokens/migration.sql +16 -0
  8. package/template/prisma/schema.prisma +20 -4
  9. package/template/prisma/seed.ts +1 -1
  10. package/template/src/apidocs/auth-docs.ts +8 -28
  11. package/template/src/apidocs/users-docs.ts +87 -57
  12. package/template/src/config/database.ts +0 -4
  13. package/template/src/config/env.ts +2 -1
  14. package/template/src/config/logger.ts +2 -2
  15. package/template/src/controllers/auth-controller.ts +10 -4
  16. package/template/src/controllers/user-controller.ts +21 -17
  17. package/template/src/dtos/pagination-dto.ts +21 -0
  18. package/template/src/dtos/user-dto.ts +37 -16
  19. package/template/src/main.ts +2 -7
  20. package/template/src/middleware/auth-middleware.ts +41 -34
  21. package/template/src/repositories/auth-token-repository.ts +25 -0
  22. package/template/src/repositories/user-repository.ts +20 -9
  23. package/template/src/routes/auth-route.ts +3 -12
  24. package/template/src/routes/user-route.ts +5 -29
  25. package/template/src/services/auth-service.ts +56 -41
  26. package/template/src/services/user-service.ts +60 -30
  27. package/template/src/utils/cookieEncrypt.ts +39 -0
  28. package/template/src/utils/pagination.ts +32 -0
  29. package/template/src/utils/response.ts +9 -31
  30. package/template/src/utils/swagger.ts +10 -9
  31. package/template/src/utils/validation.ts +8 -1
  32. package/template/src/validations/user-validation.ts +25 -27
  33. package/template/test/user.test.ts +2 -2
  34. package/template/tsconfig.json +3 -2
  35. package/template/src/dtos/list-dto.ts +0 -12
package/bin/index.js CHANGED
@@ -90,6 +90,7 @@ BASE_URL="http://localhost:4444"
90
90
  BASE_API_URL="http://localhost:4444/api"
91
91
  PORT=4444
92
92
  JWT_SECRET=
93
+ COOKIE_ENCRYPTION_KEY=
93
94
  `;
94
95
 
95
96
  const envExampleContent = envContent.replace(/=.*/g, "=");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-craftjs",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "A starter kit backend framework powered by Express, TypeScript, EJS Engine, and Prisma — designed for rapid development, simplicity, and scalability.",
5
5
  "bin": {
6
6
  "create-craftjs": "bin/index.js"
@@ -32,10 +32,21 @@ function keyGenerate() {
32
32
  envContent += `\APP_SECRET=${generateKey(32)}`;
33
33
  }
34
34
 
35
+ if (envContent.includes("COOKIE_ENCRYPTION_KEY=")) {
36
+ envContent = envContent.replace(
37
+ /COOKIE_ENCRYPTION_KEY=.*/g,
38
+ `COOKIE_ENCRYPTION_KEY=${generateKey(32)}`
39
+ );
40
+ } else {
41
+ envContent += `\COOKIE_ENCRYPTION_KEY=${generateKey(32)}`;
42
+ }
43
+
35
44
  fs.writeFileSync(envPath, envContent);
36
45
 
37
46
  console.log(
38
- chalk.green("✅ App Secret and JWT Secret generated successfully.")
47
+ chalk.green(
48
+ "✅ App Secret, JWT Secret , and Cookie Encryption Key generated successfully."
49
+ )
39
50
  );
40
51
  }
41
52
  module.exports = keyGenerate;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "craftjs",
3
- "version": "1.0.4",
3
+ "version": "1.0.15",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "craftjs",
9
- "version": "1.0.4",
9
+ "version": "1.0.14",
10
10
  "license": "UNLICENSED",
11
11
  "dependencies": {
12
12
  "@prisma/client": "^6.6.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "craftjs",
3
3
  "description": "A starter kit backend framework powered by Express, TypeScript, EJS Engine, and Prisma — designed for rapid development, simplicity, and scalability.",
4
- "version": "1.0.14",
4
+ "version": "1.0.15",
5
5
  "keywords": [
6
6
  "express",
7
7
  "typescript",
@@ -1,7 +1,7 @@
1
1
  -- CreateTable
2
2
  CREATE TABLE `users` (
3
3
  `id` VARCHAR(191) NOT NULL,
4
- `fullName` VARCHAR(100) NOT NULL,
4
+ `full_name` VARCHAR(100) NOT NULL,
5
5
  `email` VARCHAR(100) NOT NULL,
6
6
  `password` VARCHAR(255) NOT NULL,
7
7
  `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
@@ -0,0 +1,16 @@
1
+ -- CreateTable
2
+ CREATE TABLE `auth_refresh_tokens` (
3
+ `id` VARCHAR(191) NOT NULL,
4
+ `token` TEXT NOT NULL,
5
+ `user_id` VARCHAR(191) NOT NULL,
6
+ `expires_at` DATETIME(3) NOT NULL,
7
+ `revoked` BOOLEAN NOT NULL DEFAULT false,
8
+ `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
9
+ `updated_at` TIMESTAMP(0) NOT NULL,
10
+ `deleted_at` TIMESTAMP(0) NULL,
11
+
12
+ PRIMARY KEY (`id`)
13
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
14
+
15
+ -- AddForeignKey
16
+ ALTER TABLE `auth_refresh_tokens` ADD CONSTRAINT `auth_refresh_tokens_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -10,13 +10,29 @@ datasource db {
10
10
  }
11
11
 
12
12
  model User {
13
+ id String @id @default(uuid())
14
+ full_name String @db.VarChar(100)
15
+ email String @unique @db.VarChar(100)
16
+ password String @db.VarChar(255)
17
+ created_at DateTime @default(now()) @db.Timestamp(0)
18
+ updated_at DateTime @updatedAt @db.Timestamp(0)
19
+ deleted_at DateTime? @db.Timestamp(0)
20
+ authRefreshTokens AuthRefreshToken[]
21
+
22
+ @@map("users")
23
+ }
24
+
25
+ model AuthRefreshToken {
13
26
  id String @id @default(uuid())
14
- fullName String @db.VarChar(100)
15
- email String @unique @db.VarChar(100)
16
- password String @db.VarChar(255)
27
+ token String @db.Text
28
+ user_id String
29
+ expires_at DateTime
30
+ revoked Boolean @default(false)
17
31
  created_at DateTime @default(now()) @db.Timestamp(0)
18
32
  updated_at DateTime @updatedAt @db.Timestamp(0)
19
33
  deleted_at DateTime? @db.Timestamp(0)
20
34
 
21
- @@map("users")
35
+ user User @relation(fields: [user_id], references: [id])
36
+
37
+ @@map("auth_refresh_tokens")
22
38
  }
@@ -11,7 +11,7 @@ async function main() {
11
11
  where: { email: "tes@gmail.com" },
12
12
  update: {},
13
13
  create: {
14
- fullName: "Tes",
14
+ full_name: "Tes",
15
15
  email: "tes@gmail.com",
16
16
  password: await argon2.hash("12345678"),
17
17
  },
@@ -11,11 +11,11 @@
11
11
  * schema:
12
12
  * type: object
13
13
  * required:
14
- * - fullName
14
+ * - full_name
15
15
  * - email
16
16
  * - password
17
17
  * properties:
18
- * fullName:
18
+ * full_name:
19
19
  * type: string
20
20
  * email:
21
21
  * type: string
@@ -42,7 +42,7 @@
42
42
  * id:
43
43
  * type: string
44
44
  * format: uuid
45
- * fullName:
45
+ * full_name:
46
46
  * type: string
47
47
  * email:
48
48
  * type: string
@@ -110,18 +110,16 @@
110
110
  * properties:
111
111
  * id:
112
112
  * type: string
113
- * fullName:
113
+ * full_name:
114
114
  * type: string
115
115
  * email:
116
116
  * type: string
117
117
  * accessToken:
118
118
  * type: string
119
- * refreshToken:
120
- * type: string
121
119
  * 400:
122
120
  * $ref: '#/components/responses/ValidationError'
123
121
  * 401:
124
- * description: Login gagal - username,email atau password salah
122
+ * description: Login gagal - email atau password salah
125
123
  * content:
126
124
  * application/json:
127
125
  * example:
@@ -158,7 +156,7 @@
158
156
  * properties:
159
157
  * id:
160
158
  * type: string
161
- * fullName:
159
+ * full_name:
162
160
  * type: string
163
161
  * email:
164
162
  * type: string
@@ -232,24 +230,6 @@
232
230
  * data:
233
231
  * type: object
234
232
  * properties:
235
- * user:
236
- * type: object
237
- * properties:
238
- * id:
239
- * type: string
240
- * fullName:
241
- * type: string
242
- * email:
243
- * type: string
244
- * password:
245
- * type: string
246
- * created_at:
247
- * type: string
248
- * updated_at:
249
- * type: string
250
- * deleted_at:
251
- * type: string
252
- * nullable: true
253
233
  * accessToken:
254
234
  * type: string
255
235
  * 401:
@@ -271,7 +251,7 @@
271
251
  * schema:
272
252
  * type: object
273
253
  * properties:
274
- * fullName:
254
+ * full_name:
275
255
  * type: string
276
256
  * example: tes
277
257
  * email:
@@ -301,7 +281,7 @@
301
281
  * type: string
302
282
  * format: uuid
303
283
  * example: c1fa015f-48b0-456f-8c41-baf54ce092f5
304
- * fullName:
284
+ * full_name:
305
285
  * type: string
306
286
  * example: tes update
307
287
  * email:
@@ -11,20 +11,33 @@
11
11
  * name: page
12
12
  * schema:
13
13
  * type: integer
14
- * required: false
15
14
  * description: Nomor halaman
16
15
  * - in: query
17
16
  * name: take
18
17
  * schema:
19
18
  * type: integer
20
- * required: false
21
19
  * description: Jumlah data per halaman
22
20
  * - in: query
23
- * name: name
21
+ * name: search
24
22
  * schema:
25
23
  * type: string
26
- * required: false
27
- * description: Filter berdasarkan nama (fullName)
24
+ * description: Filter berdasarkan nama atau email
25
+ * - in: query
26
+ * name: sort_field
27
+ * schema:
28
+ * type: string
29
+ * description: Urutkan berdasarkan field
30
+ * - in: query
31
+ * name: sort_order
32
+ * schema:
33
+ * type: string
34
+ * enum: [asc, desc]
35
+ * description: Urutkan berdasarkan asc atau desc
36
+ * - in: query
37
+ * name: no_paginate
38
+ * schema:
39
+ * type: boolean
40
+ * description: Menampilkan semua data tanpa paginasi
28
41
  * responses:
29
42
  * 200:
30
43
  * description: Berhasil Get All Data
@@ -35,61 +48,78 @@
35
48
  * properties:
36
49
  * status:
37
50
  * type: boolean
51
+ * example: true
38
52
  * status_code:
39
53
  * type: integer
54
+ * example: 200
40
55
  * message:
41
56
  * type: string
57
+ * example: Berhasil Get All Data
42
58
  * data:
59
+ * type: array
60
+ * items:
61
+ * type: object
62
+ * properties:
63
+ * id:
64
+ * type: string
65
+ * format: uuid
66
+ * example: da613a6c-86e2-45f2-b583-157d9e7214a8
67
+ * full_name:
68
+ * type: string
69
+ * example: Tes
70
+ * email:
71
+ * type: string
72
+ * example: tes@gmail.com
73
+ * created_at:
74
+ * type: string
75
+ * example: 18-01-2026 21:55:22
76
+ * updated_at:
77
+ * type: string
78
+ * example: 18-01-2026 21:55:22
79
+ * deleted_at:
80
+ * type: string
81
+ * nullable: true
82
+ * example: ""
83
+ * meta:
43
84
  * type: object
44
85
  * properties:
45
- * data:
46
- * type: array
47
- * items:
48
- * type: object
49
- * properties:
50
- * id:
51
- * type: string
52
- * format: uuid
53
- * fullName:
54
- * type: string
55
- * email:
56
- * type: string
57
- * username:
58
- * type: string
59
- * password:
60
- * type: string
61
- * description: (Hashed password)
62
- * image_id:
63
- * type: string
64
- * image_url:
65
- * type: string
66
- * format: uri
67
- * is_verify:
68
- * type: boolean
69
- * created_at:
70
- * type: string
71
- * format: date-time
72
- * updated_at:
73
- * type: string
74
- * format: date-time
75
- * deleted_at:
76
- * type: string
77
- * format: date-time
78
- * nullable: true
79
- * role_id:
80
- * type: string
81
- * format: uuid
82
- * total_data:
86
+ * current_page:
83
87
  * type: integer
84
- * paging:
85
- * type: object
86
- * properties:
87
- * current_page:
88
- * type: integer
89
- * total_page:
90
- * type: integer
91
-
88
+ * example: 1
89
+ * from:
90
+ * type: integer
91
+ * example: 1
92
+ * last_page:
93
+ * type: integer
94
+ * example: 1
95
+ * per_page:
96
+ * type: integer
97
+ * example: 10
98
+ * to:
99
+ * type: integer
100
+ * example: 1
101
+ * total:
102
+ * type: integer
103
+ * example: 1
104
+ * links:
105
+ * type: object
106
+ * properties:
107
+ * first:
108
+ * type: string
109
+ * example: http://localhost:4444/api/users?page=1
110
+ * last:
111
+ * type: string
112
+ * example: http://localhost:4444/api/users?page=1
113
+ * prev:
114
+ * type: string
115
+ * nullable: true
116
+ * example: null
117
+ * next:
118
+ * type: string
119
+ * nullable: true
120
+ * example: null
92
121
  */
122
+
93
123
  /**
94
124
  * @swagger
95
125
  * /api/users/{id}:
@@ -121,7 +151,7 @@
121
151
  * type: object
122
152
  * properties:
123
153
  * id: { type: string, format: uuid }
124
- * fullName: { type: string }
154
+ * full_name: { type: string }
125
155
  * email: { type: string, format: email }
126
156
  * username: { type: string }
127
157
  * role_id: { type: string, format: uuid }
@@ -141,9 +171,9 @@
141
171
  * application/json:
142
172
  * schema:
143
173
  * type: object
144
- * required: [ fullName, email, password ]
174
+ * required: [ full_name, email, password ]
145
175
  * properties:
146
- * fullName: { type: string }
176
+ * full_name: { type: string }
147
177
  * email: { type: string, format: email }
148
178
  * password: { type: string, format: password }
149
179
  * responses:
@@ -161,7 +191,7 @@
161
191
  * type: object
162
192
  * properties:
163
193
  * id: { type: string, format: uuid }
164
- * fullName: { type: string }
194
+ * full_name: { type: string }
165
195
  * email: { type: string }
166
196
  */
167
197
  /**
@@ -187,7 +217,7 @@
187
217
  * schema:
188
218
  * type: object
189
219
  * properties:
190
- * fullName: { type: string }
220
+ * full_name: { type: string }
191
221
  * email: { type: string, format: email }
192
222
  * responses:
193
223
  * 200:
@@ -204,7 +234,7 @@
204
234
  * type: object
205
235
  * properties:
206
236
  * id: { type: string, format: uuid }
207
- * fullName: { type: string }
237
+ * full_name: { type: string }
208
238
  * email: { type: string }
209
239
  */
210
240
  /**
@@ -1,8 +1,6 @@
1
1
  import { PrismaClient } from "@prisma/client";
2
2
  import { logger } from "./logger";
3
- import { DateTime } from "luxon";
4
3
  import { dbLogger } from "./logger";
5
- import { env } from "./env";
6
4
 
7
5
  export const prismaClient = new PrismaClient({
8
6
  log: [
@@ -25,8 +23,6 @@ export const prismaClient = new PrismaClient({
25
23
  ],
26
24
  });
27
25
 
28
-
29
-
30
26
  prismaClient.$on("query", (e) => {
31
27
  dbLogger.info(`${e.query} - ${e.params}`);
32
28
  });
@@ -34,7 +34,8 @@ const envSchema = z.object({
34
34
  "CLIENT_URL harus berupa URL valid, dipisah koma jika lebih dari satu",
35
35
  }
36
36
  ),
37
- PORT: z.coerce.number().default(3000),
37
+ PORT: z.coerce.number().default(4444),
38
+ COOKIE_ENCRYPTION_KEY: z.string(),
38
39
  JWT_SECRET: z.string(),
39
40
  CLOUDINARY_CLOUD_NAME: z.string().optional(),
40
41
  CLOUDINARY_API_KEY: z.string().optional(),
@@ -65,7 +65,7 @@ export const dbLogger = winston.createLogger({
65
65
  ],
66
66
  });
67
67
 
68
- // Format untuk FILE logs — tanpa warna
68
+
69
69
  const plainFormat = winston.format.printf(({ timestamp, level, message }) => {
70
70
  return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
71
71
  });
@@ -93,7 +93,7 @@ const coloredHttpFormat = winston.format.printf(
93
93
  break;
94
94
  }
95
95
 
96
- // Pewarnaan method HTTP
96
+
97
97
  const methodMatch = msg.match(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)/);
98
98
  const methodColors = {
99
99
  GET: chalk.green.bold("GET"),
@@ -4,7 +4,7 @@ import {
4
4
  CreateUserRequest,
5
5
  UpdateUserRequest,
6
6
  } from "../dtos/user-dto";
7
- import { successResponse, successUpdateResponse } from "../utils/response";
7
+ import { successResponse } from "../utils/response";
8
8
  import { AuthService } from "../services/auth-service";
9
9
  import { UserRequest } from "../types/type-request";
10
10
  import { env } from "../config/env";
@@ -61,7 +61,9 @@ export class AuthController {
61
61
  try {
62
62
  const request: UpdateUserRequest = req.body as UpdateUserRequest;
63
63
  const response = await AuthService.updateProfile(req.user!, request);
64
- res.status(200).json(successUpdateResponse(response));
64
+ res
65
+ .status(200)
66
+ .json(successResponse("Update User Berhasil", 200, response));
65
67
  } catch (error) {
66
68
  next(error);
67
69
  }
@@ -69,7 +71,12 @@ export class AuthController {
69
71
 
70
72
  static async logout(req: UserRequest, res: Response, next: NextFunction) {
71
73
  await AuthService.logout(req);
72
- res.clearCookie("refresh_token");
74
+ res.clearCookie("refresh_token", {
75
+ httpOnly: true,
76
+ secure: process.env.NODE_ENV === "production",
77
+ sameSite: "lax",
78
+ path: "/",
79
+ });
73
80
  res.status(200).json(successResponse("Logout berhasil", 200));
74
81
  }
75
82
  static async refreshToken(req: Request, res: Response, next: NextFunction) {
@@ -77,7 +84,6 @@ export class AuthController {
77
84
  const response = await AuthService.refreshToken(req);
78
85
  res.status(200).json(
79
86
  successResponse("Get Access Token Berhasil", 200, {
80
- user: response.user,
81
87
  accessToken: response.accessToken,
82
88
  })
83
89
  );
@@ -5,16 +5,11 @@ import {
5
5
  UpdateUserRequest,
6
6
  } from "../dtos/user-dto";
7
7
  import { UserService } from "../services/user-service";
8
- import {
9
- successCreateResponse,
10
- successDeleteResponse,
11
- successResponse,
12
- successUpdateResponse,
13
- } from "../utils/response";
8
+ import { successResponse, paginateResponse } from "../utils/response";
14
9
  import { UserRequest } from "../types/type-request";
15
-
10
+ import { env } from "../config/env";
16
11
  export class UserController {
17
- static async getAll(req: Request, res: Response, next: NextFunction) {
12
+ static async get(req: Request, res: Response, next: NextFunction) {
18
13
  try {
19
14
  const page = Number(req.query.page) || 1;
20
15
  const take = Number(req.query.take) || 10;
@@ -22,21 +17,27 @@ export class UserController {
22
17
  page: page,
23
18
  take: take,
24
19
  skip: (page - 1) * take,
25
- name: req.query.name as string,
20
+ search: req.query.search as string,
21
+ sort_field: req.query.sort_field as any,
22
+ sort_order: req.query.sort_order as any,
23
+ no_paginate: req.query.no_paginate === "true",
26
24
  };
27
- const response = await UserService.getAll(request);
25
+
26
+ const path = req.originalUrl.split("?")[0];
27
+ const baseUrl = `${env.BASE_URL}${path}`;
28
+ const response = await UserService.get(request, baseUrl, req.query);
28
29
  res
29
30
  .status(200)
30
- .json(successResponse("Berhasil Get All Data", 200, response));
31
+ .json(paginateResponse("Berhasil Get All Data", 200, response));
31
32
  } catch (e) {
32
33
  next(e);
33
34
  }
34
35
  }
35
36
 
36
- static async getOne(req: Request, res: Response, next: NextFunction) {
37
+ static async detail(req: Request, res: Response, next: NextFunction) {
37
38
  try {
38
39
  const id = req.params.id;
39
- const response = await UserService.getOne(id);
40
+ const response = await UserService.detail(id);
40
41
  res
41
42
  .status(200)
42
43
  .json(successResponse("Berhasil Get Detail Data", 200, response));
@@ -48,9 +49,10 @@ export class UserController {
48
49
  static async create(req: Request, res: Response, next: NextFunction) {
49
50
  try {
50
51
  const request: CreateUserRequest = req.body as CreateUserRequest;
51
- console.log(request);
52
52
  const response = await UserService.create(request);
53
- res.status(201).json(successCreateResponse(response));
53
+ res
54
+ .status(201)
55
+ .json(successResponse("Create User Berhasil", 201, response));
54
56
  } catch (error) {
55
57
  next(error);
56
58
  }
@@ -61,7 +63,9 @@ export class UserController {
61
63
  const id = req.params.id;
62
64
  const request: UpdateUserRequest = req.body as UpdateUserRequest;
63
65
  const response = await UserService.update(id, request);
64
- res.status(200).json(successUpdateResponse(response));
66
+ res
67
+ .status(200)
68
+ .json(successResponse("Update User Berhasil", 200, response));
65
69
  } catch (error) {
66
70
  next(error);
67
71
  }
@@ -71,7 +75,7 @@ export class UserController {
71
75
  try {
72
76
  const id = req.params.id;
73
77
  await UserService.delete(id);
74
- res.status(200).json(successDeleteResponse());
78
+ res.status(200).json(successResponse("Delete User Berhasil", 200));
75
79
  } catch (e) {
76
80
  next(e);
77
81
  }
@@ -0,0 +1,21 @@
1
+ export type PaginationMeta = {
2
+ current_page: number;
3
+ from: number | null;
4
+ last_page: number;
5
+ per_page: number;
6
+ to: number | null;
7
+ total: number;
8
+ };
9
+
10
+ export type PaginationLinks = {
11
+ first: string;
12
+ last: string;
13
+ prev: string | null;
14
+ next: string | null;
15
+ };
16
+
17
+ export type PaginatedResponse<T> = {
18
+ data: T[];
19
+ meta: PaginationMeta;
20
+ links: PaginationLinks;
21
+ };