create-craftjs 2.0.6 → 2.1.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 (34) hide show
  1. package/README.md +3 -0
  2. package/bin/index.js +10 -1
  3. package/package.json +5 -2
  4. package/template/Craft JS.postman_collection.json +1500 -0
  5. package/template/craft/commands/key-generate.js +12 -4
  6. package/template/craft/commands/make-controller.js +3 -3
  7. package/template/craft/commands/make-middleware.js +7 -7
  8. package/template/craft/commands/make-route.js +0 -3
  9. package/template/craft/commands/make-test.js +5 -4
  10. package/template/package-lock.json +95 -2
  11. package/template/package.json +3 -1
  12. package/template/src/config/database.ts +5 -3
  13. package/template/src/config/env.ts +10 -1
  14. package/template/src/config/logger.ts +2 -4
  15. package/template/src/config/redis.ts +77 -0
  16. package/template/src/config/web.ts +1 -1
  17. package/template/src/controllers/auth-controller.ts +22 -15
  18. package/template/src/controllers/user-controller.ts +1 -2
  19. package/template/src/database/seeders/seed.ts +14 -4
  20. package/template/src/dtos/auth-dto.ts +28 -0
  21. package/template/src/dtos/user-dto.ts +2 -5
  22. package/template/src/interfaces/auth-session.ts +16 -0
  23. package/template/src/interfaces/type-request.ts +11 -0
  24. package/template/src/main.ts +6 -4
  25. package/template/src/middleware/auth-middleware.ts +19 -28
  26. package/template/src/providers/auth-session-provider.ts +12 -0
  27. package/template/src/providers/db-auth-session.ts +23 -0
  28. package/template/src/providers/redis-auth-session.ts +33 -0
  29. package/template/src/repositories/auth-token-repository.ts +1 -1
  30. package/template/src/services/auth-service.ts +136 -78
  31. package/template/src/services/user-service.ts +8 -2
  32. package/template/test/user.test.ts +3 -2
  33. package/template/tsconfig.json +2 -1
  34. package/template/src/utils/type-request.ts +0 -6
@@ -0,0 +1,11 @@
1
+ import { Request } from "express";
2
+
3
+ export interface IUser {
4
+ user_id: string;
5
+ user_full_name: string;
6
+ user_email: string;
7
+ jti: string;
8
+ }
9
+ export interface UserRequest extends Request {
10
+ user?: IUser;
11
+ }
@@ -1,7 +1,8 @@
1
- import { web } from "./config/web";
2
- import { connectDatabase } from "./config/database";
3
- import { env } from "./config/env";
4
- import { logger } from "./config/logger";
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
+ import { initRedis } from "@config/redis";
5
6
 
6
7
  async function startServer() {
7
8
  try {
@@ -11,6 +12,7 @@ async function startServer() {
11
12
  process.exit(0);
12
13
  }
13
14
  await connectDatabase();
15
+ await initRedis();
14
16
  web.listen(env.PORT, env.HOST, () => {
15
17
  logger.info(`🚀 Server is listening on: http://${env.HOST}:${env.PORT}`);
16
18
  logger.info(
@@ -1,51 +1,42 @@
1
1
  import { NextFunction, Response } from "express";
2
2
  import jwt from "jsonwebtoken";
3
- import { UserRequest } from "@utils/type-request";
4
- import { errorResponse } from "@utils/response";
3
+ import { UserRequest } from "@interfaces/type-request";
5
4
  import { ResponseError } from "@utils/response-error";
6
5
  import { env } from "@config/env";
7
- import { UserRepository } from "@repositories/user-repository";
8
6
  import { asyncHandler } from "@utils/async-handler";
7
+ import { authSessionProvider } from "@providers/auth-session-provider";
9
8
 
10
9
  export const authMiddleware = asyncHandler(
11
10
  async (req: UserRequest, res: Response, next: NextFunction) => {
12
- const cookie = req.cookies["refresh_token"];
13
- if (!cookie) {
14
- return res
15
- .status(401)
16
- .json(errorResponse("Unauthorized: Anda Belum Login.", 401));
17
- }
18
-
19
11
  const token = req.get("Authorization")?.split(" ")[1];
20
12
  if (!token) {
21
- return res
22
- .status(401)
23
- .json(
24
- errorResponse("Unauthorized: Token Tidak Valid Atau Kadaluarsa.", 401)
25
- );
13
+ throw new ResponseError(
14
+ 401,
15
+ "Unauthorized: Token Tidak Valid Atau Kadaluarsa."
16
+ );
26
17
  }
27
- let payload: {
18
+ const payload = jwt.verify(token, env.JWT_ACCESS_SECRET) as {
28
19
  user_id: string;
29
- user_email: string;
30
- user_full_name: string;
20
+ jti: string;
31
21
  };
32
22
 
33
- try {
34
- payload = jwt.verify(token, env.JWT_SECRET as string) as typeof payload;
35
- } catch (err) {
23
+ const session = await authSessionProvider().get(
24
+ payload.user_id,
25
+ payload.jti
26
+ );
27
+
28
+ if (!session) {
36
29
  throw new ResponseError(
37
30
  401,
38
31
  "Unauthorized: Token Tidak Valid Atau Kadaluarsa."
39
32
  );
40
33
  }
41
34
 
42
- const user = await UserRepository.findUserByEmail(payload.user_email);
43
- if (!user) {
44
- return res
45
- .status(401)
46
- .json(errorResponse("Unauthorized: Anda Belum Login.", 401));
47
- }
48
- req.user = user;
35
+ req.user = {
36
+ ...session,
37
+ jti: payload.jti,
38
+ };
39
+
49
40
  next();
50
41
  }
51
42
  );
@@ -0,0 +1,12 @@
1
+ import { env } from "@config/env";
2
+ import { AuthSessionProvider } from "@interfaces/auth-session";
3
+ import { RedisAuthSessionProvider } from "@providers/redis-auth-session";
4
+ import { DbAuthSessionProvider } from "@providers/db-auth-session";
5
+ import { isRedisHealthy } from "@config/redis";
6
+
7
+ export const authSessionProvider = (): AuthSessionProvider => {
8
+ if (env.REDIS_ENABLED && isRedisHealthy()) {
9
+ return new RedisAuthSessionProvider();
10
+ }
11
+ return new DbAuthSessionProvider();
12
+ };
@@ -0,0 +1,23 @@
1
+ import { AuthSessionProvider } from "@interfaces/auth-session";
2
+ import { UserRepository } from "@repositories/user-repository";
3
+
4
+ export class DbAuthSessionProvider implements AuthSessionProvider {
5
+ async get(userId: string) {
6
+ const user = await UserRepository.findById(userId);
7
+ if (!user) return null;
8
+
9
+ return {
10
+ user_id: user.id,
11
+ user_email: user.email,
12
+ user_full_name: user.full_name,
13
+ };
14
+ }
15
+
16
+ async set() {
17
+ // no-op (DB fallback tidak simpan session)
18
+ }
19
+
20
+ async delete() {
21
+ // ❌ no-op
22
+ }
23
+ }
@@ -0,0 +1,33 @@
1
+ import { AuthSessionProvider } from "@interfaces/auth-session";
2
+ import { getRedis } from "@config/redis";
3
+
4
+ export class RedisAuthSessionProvider implements AuthSessionProvider {
5
+ async get(userId: string, jti: string) {
6
+ try {
7
+ const redis = getRedis();
8
+ const data = await redis.get(`session:${userId}:${jti}`);
9
+ return data ? JSON.parse(data) : null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ async set(userId: string, jti: string, data: any, ttl: number) {
16
+ try {
17
+ const redis = getRedis();
18
+ await redis.set(
19
+ `session:${userId}:${jti}`,
20
+ JSON.stringify(data),
21
+ "EX",
22
+ ttl
23
+ );
24
+ } catch {}
25
+ }
26
+
27
+ async delete(userId: string, jti: string) {
28
+ try {
29
+ const redis = getRedis();
30
+ await redis.del(`session:${userId}:${jti}`);
31
+ } catch {}
32
+ }
33
+ }
@@ -1,5 +1,5 @@
1
1
  import { prismaClient } from "@config/database";
2
-
2
+ import { getRedis } from "@config/redis";
3
3
  export class AuthTokenRepository {
4
4
  static async create(data: any) {
5
5
  return prismaClient.authRefreshToken.create({ data });
@@ -1,28 +1,27 @@
1
- import {
2
- toUserDetailResponse,
3
- toUserResponse,
4
- UserDetailResponse,
5
- UserResponse,
6
- loginRequest,
7
- CreateUserRequest,
8
- UpdateUserRequest,
9
- } from "@dtos/user-dto";
1
+ import { toUserResponse, UserResponse } from "@dtos/user-dto";
10
2
  import { ResponseError } from "@utils/response-error";
11
3
  import { UserValidation } from "@validations/user-validation";
12
4
  import { Validation } from "@utils/validation";
13
5
  import * as argon2 from "argon2";
14
- import { User } from "@prisma/client";
15
6
  import jwt from "jsonwebtoken";
16
7
  import { Request } from "express";
17
8
  import { UserRepository } from "@repositories/user-repository";
18
- import { UserRequest } from "@utils/type-request";
19
- import { prismaClient } from "@config/database";
9
+ import { UserRequest } from "@interfaces/type-request";
20
10
  import { env } from "@config/env";
21
11
  import { decryptCookie, encryptCookie } from "@utils/cookieEncrypt";
22
12
  import { AuthTokenRepository } from "@repositories/auth-token-repository";
13
+ import { authSessionProvider } from "@providers/auth-session-provider";
14
+ import {
15
+ AuthUpdateRequest,
16
+ AuthMeResponse,
17
+ toAuthMeResponse,
18
+ LoginRequest,
19
+ RegisterRequest,
20
+ } from "@dtos/auth-dto";
21
+ const { v4: uuidv4 } = require("uuid");
23
22
 
24
23
  export class AuthService {
25
- static async register(request: CreateUserRequest): Promise<UserResponse> {
24
+ static async register(request: RegisterRequest): Promise<UserResponse> {
26
25
  const data = Validation.validate(UserValidation.REGISTER, request);
27
26
  const emailExits = await UserRepository.countByEmail(data.email);
28
27
 
@@ -40,94 +39,140 @@ export class AuthService {
40
39
  return toUserResponse(response);
41
40
  }
42
41
 
43
- static async login(request: loginRequest) {
42
+ static async login(request: LoginRequest) {
44
43
  const data = Validation.validate(UserValidation.LOGIN, request);
45
- const userExits = await UserRepository.findUserByEmail(data.email);
44
+ const user = await UserRepository.findUserByEmail(data.email);
46
45
 
47
- if (!userExits) {
46
+ if (!user) {
48
47
  throw new ResponseError(401, "Gagal Login! Detail login salah");
49
48
  }
50
49
 
51
- const isPasswordValid = await argon2.verify(
52
- userExits.password,
53
- data.password
54
- );
50
+ const isPasswordValid = await argon2.verify(user.password, data.password);
55
51
  if (!isPasswordValid) {
56
52
  throw new ResponseError(401, "Gagal Login! Detail login salah");
57
53
  }
58
-
59
- const refreshToken = jwt.sign(
54
+ const jti = uuidv4();
55
+ const accessToken = jwt.sign(
60
56
  {
61
- user_id: userExits.id,
62
- user_full_name: userExits.full_name,
63
- user_email: userExits.email,
57
+ user_id: user.id,
58
+ jti: jti,
64
59
  },
65
- env.JWT_SECRET as string,
60
+ env.JWT_ACCESS_SECRET as string,
66
61
  {
67
- expiresIn: "1d",
62
+ expiresIn: "5m",
68
63
  }
69
64
  );
70
-
71
- const accessToken = jwt.sign(
65
+ const refreshToken = jwt.sign(
72
66
  {
73
- user_id: userExits.id,
74
- user_fullName: userExits.full_name,
75
- user_email: userExits.email,
67
+ user_id: user.id,
68
+ jti: jti,
76
69
  },
77
- env.JWT_SECRET as string,
70
+ env.JWT_REFRESH_SECRET as string,
78
71
  {
79
- expiresIn: "5m",
72
+ expiresIn: "1d",
80
73
  }
81
74
  );
75
+ const sessionProvider = authSessionProvider();
76
+ await sessionProvider.set(
77
+ user.id,
78
+ jti,
79
+ {
80
+ user_id: user.id,
81
+ user_email: user.email,
82
+ user_full_name: user.full_name,
83
+ },
84
+ 300
85
+ );
82
86
  const encryptedRefreshToken = encryptCookie(refreshToken);
83
87
  await AuthTokenRepository.create({
84
- user_id: userExits.id,
88
+ user_id: user.id,
85
89
  token: refreshToken,
86
90
  expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000),
87
91
  });
88
- const user = toUserResponse(userExits);
89
- return { user, refreshToken: encryptedRefreshToken, accessToken };
92
+ const dataUser = toUserResponse(user);
93
+ return { dataUser, refreshToken: encryptedRefreshToken, accessToken };
90
94
  }
91
95
 
92
- static async me(user: User): Promise<UserDetailResponse> {
93
- return toUserDetailResponse(user);
96
+ static async me(req: UserRequest): Promise<AuthMeResponse> {
97
+ if (!req.user) {
98
+ throw new ResponseError(401, "Unauthorized: Anda Belum Login.");
99
+ }
100
+ return toAuthMeResponse(req.user);
94
101
  }
95
102
 
96
- static async updateProfile(
97
- user: User,
98
- request: UpdateUserRequest
99
- ): Promise<UserResponse> {
100
- const data = Validation.validate(UserValidation.UPDATE, request);
101
- if (data.full_name) {
102
- user.full_name = data.full_name;
103
+ static async updateProfile(req: UserRequest, request: AuthUpdateRequest) {
104
+ if (!req.user) {
105
+ throw new ResponseError(401, "Unauthorized : Anda Belum Login.");
106
+ }
107
+ const updateData: any = {};
108
+ const validatedData = Validation.validate(UserValidation.UPDATE, request);
109
+ if (validatedData.full_name) {
110
+ updateData.full_name = validatedData.full_name;
103
111
  }
104
- if (data.email && data.email !== user.email) {
105
- const emailExists = await UserRepository.findemailExistsNotUserLoggedIn(
106
- data.email,
107
- user.id
112
+
113
+ let emailChanged = false;
114
+ if (validatedData.email && validatedData.email !== req.user.user_email) {
115
+ const exists = await UserRepository.findemailExistsNotUserLoggedIn(
116
+ validatedData.email,
117
+ req.user.user_id
108
118
  );
109
- if (emailExists != 0) {
110
- throw new ResponseError(409, "Email Sudah Ada");
119
+
120
+ if (exists > 0) {
121
+ throw new ResponseError(409, "Email Sudah Terdaftar");
111
122
  }
112
- user.email = data.email;
123
+ updateData.email = validatedData.email;
124
+ emailChanged = true;
113
125
  }
114
- const result = await UserRepository.updateUser(
115
- {
116
- full_name: user.full_name,
117
- email: user.email,
118
- },
119
- user.id
126
+
127
+ const updatedUser = await UserRepository.update(
128
+ req.user.user_id,
129
+ updateData
120
130
  );
121
- return toUserResponse(result);
131
+ if (emailChanged) {
132
+ const newJti = uuidv4();
133
+ const newAccessToken = jwt.sign(
134
+ { user_id: req.user.user_id, jti: newJti },
135
+ env.JWT_ACCESS_SECRET as string,
136
+ { expiresIn: "5m" }
137
+ );
138
+
139
+ const provider = authSessionProvider();
140
+ await provider.set(
141
+ req.user.user_id,
142
+ newJti,
143
+ {
144
+ ...req.user,
145
+ user_email: updatedUser.email,
146
+ user_full_name: updatedUser.full_name,
147
+ },
148
+ 300
149
+ );
150
+
151
+ await provider.delete(req.user.user_id, req.user.jti);
152
+
153
+ return {
154
+ rotated: true,
155
+ accessToken: newAccessToken,
156
+ };
157
+ }
158
+
159
+ return {
160
+ rotated: false,
161
+ };
122
162
  }
123
163
 
124
164
  static async logout(req: UserRequest) {
125
- const refreshToken = req.cookies.refresh_token;
126
- if (!req.user || !refreshToken) {
165
+ if (!req.user) {
127
166
  throw new ResponseError(401, "Unauthorized: Anda Belum Login.");
128
167
  }
129
- const decryptToken = decryptCookie(refreshToken);
130
- await AuthTokenRepository.revokeToken(decryptToken);
168
+ const { user_id, jti } = req.user;
169
+ await authSessionProvider().delete(user_id, jti);
170
+ const encryptedRefreshToken = req.cookies.refresh_token;
171
+
172
+ if (encryptedRefreshToken) {
173
+ const refreshToken = decryptCookie(encryptedRefreshToken);
174
+ await AuthTokenRepository.revokeToken(refreshToken);
175
+ }
131
176
  return true;
132
177
  }
133
178
 
@@ -137,15 +182,19 @@ export class AuthService {
137
182
  throw new ResponseError(401, "Unauthorized: Anda Belum Login.");
138
183
  }
139
184
  const refreshToken = decryptCookie(encryptedRefreshToken);
140
- let decoded: any;
185
+ let decoded: { user_id: string; jti: string };
141
186
  try {
142
- decoded = jwt.verify(refreshToken, env.JWT_SECRET as string);
143
- } catch (err) {
187
+ decoded = jwt.verify(
188
+ refreshToken,
189
+ env.JWT_REFRESH_SECRET as string
190
+ ) as typeof decoded;
191
+ } catch {
144
192
  throw new ResponseError(
145
193
  401,
146
194
  "Unauthorized: Token Tidak Valid Atau Kadaluarsa."
147
195
  );
148
196
  }
197
+
149
198
  const tokenRecord = await AuthTokenRepository.findValidToken(
150
199
  decoded.user_id,
151
200
  refreshToken
@@ -156,22 +205,31 @@ export class AuthService {
156
205
  "Unauthorized: Token Tidak Valid Atau Kadaluarsa."
157
206
  );
158
207
  }
159
- const user = await prismaClient.user.findUnique({
160
- where: { id: decoded.user_id },
161
- });
208
+
209
+ const user = await UserRepository.findById(decoded.user_id);
162
210
 
163
211
  if (!user) {
164
212
  throw new ResponseError(401, "Unauthorized: Uset Tidak Ditemukan.");
165
213
  }
166
- const payload = {
167
- user_id: user.id,
168
- user_full_name: user.full_name,
169
- user_email: user.email,
170
- };
214
+ const newJti = uuidv4();
215
+ const accessToken = jwt.sign(
216
+ { user_id: user.id, jti: newJti },
217
+ env.JWT_ACCESS_SECRET as string,
218
+ { expiresIn: "5m" }
219
+ );
220
+ const provider = authSessionProvider();
221
+
222
+ await provider.set(
223
+ user.id,
224
+ newJti,
225
+ {
226
+ user_id: user.id,
227
+ user_email: user.email,
228
+ user_full_name: user.full_name,
229
+ },
230
+ 300
231
+ );
171
232
 
172
- const accessToken = jwt.sign(payload, env.JWT_SECRET as string, {
173
- expiresIn: "5m",
174
- });
175
233
  return { accessToken };
176
234
  }
177
235
  }
@@ -26,7 +26,7 @@ export class UserService {
26
26
  data.password = await argon2.hash(data.password);
27
27
 
28
28
  const response = await UserRepository.create({
29
- fullName: data.full_name,
29
+ full_name: data.full_name,
30
30
  email: data.email,
31
31
  password: data.password,
32
32
  });
@@ -117,9 +117,15 @@ export class UserService {
117
117
  }
118
118
  user.email = data.email;
119
119
  }
120
+
121
+ if (data.password) {
122
+ user.password = await argon2.hash(data.password);
123
+ }
124
+
120
125
  const result = await UserRepository.update(id, {
121
- fullName: user.full_name,
126
+ full_name: user.full_name,
122
127
  email: user.email,
128
+ password: user.password,
123
129
  });
124
130
  return toUserResponse(result);
125
131
  }
@@ -1,12 +1,13 @@
1
+ /// <reference types="jest" />
1
2
  import supertest from "supertest";
2
3
  import { web } from "../src/config/web";
3
4
  import { logger } from "../src/config/logger";
4
- describe("POST /api/users", () => {
5
+ describe("POST /api/auth/register", () => {
5
6
  it("should register new user", async () => {
6
7
  const response = await supertest(web).post("/api/auth/register").send({
7
8
  full_name: "test",
8
9
  email: "testing@gmail.com",
9
- password: "12345678",
10
+ password: "123456",
10
11
  });
11
12
  logger.debug(response.body);
12
13
  expect(response.status).toBe(201);
@@ -9,9 +9,10 @@
9
9
  "@services/*": ["./src/services/*"],
10
10
  "@repositories/*": ["./src/repositories/*"],
11
11
  "@routes/*": ["./src/routes/*"],
12
+ "@interfaces/*": ["./src/interfaces/*"],
12
13
  "@middleware/*": ["./src/middleware/*"],
14
+ "@providers/*": ["./src/providers/*"],
13
15
  "@dtos/*": ["./src/dtos/*"],
14
- "@types/*": ["./src/types/*"],
15
16
  "@utils/*": ["./src/utils/*"],
16
17
  "@validations/*": ["./src/validations/*"],
17
18
  "@views/*": ["./src/views/*"],
@@ -1,6 +0,0 @@
1
- import { User } from "@prisma/client";
2
- import { Request } from "express";
3
-
4
- export interface UserRequest extends Request {
5
- user?: User;
6
- }