QPAT commited on
Commit
076931b
·
1 Parent(s): 79fbc37

Add module otp, add forgot password and change password

Browse files
Files changed (48) hide show
  1. mvnw +0 -0
  2. pom.xml +4 -0
  3. src/main/java/com/attendenceSystem/annotation/RegisterPasswordMatch.java +23 -0
  4. src/main/java/com/attendenceSystem/annotation/{PasswordMatch.java → UpdatePasswordMatch.java} +4 -5
  5. src/main/java/com/attendenceSystem/constant/Routes.java +3 -0
  6. src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java +5 -0
  7. src/main/java/com/attendenceSystem/module/otp/README.md +85 -0
  8. src/main/java/com/attendenceSystem/module/otp/api/OtpApiController.java +36 -0
  9. src/main/java/com/attendenceSystem/module/otp/dto/request/SendOtpRequest.java +22 -0
  10. src/main/java/com/attendenceSystem/module/otp/dto/request/VerifyOtpRequest.java +25 -0
  11. src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java +15 -0
  12. src/main/java/com/attendenceSystem/module/otp/entity/Otp.java +47 -0
  13. src/main/java/com/attendenceSystem/module/otp/entity/converter/OtpPurposeConverter.java +20 -0
  14. src/main/java/com/attendenceSystem/module/otp/entity/enums/OtpPurpose.java +32 -0
  15. src/main/java/com/attendenceSystem/module/otp/exception/OtpExpiredException.java +7 -0
  16. src/main/java/com/attendenceSystem/module/otp/exception/OtpInvalidException.java +7 -0
  17. src/main/java/com/attendenceSystem/module/otp/exception/OtpNotFoundException.java +7 -0
  18. src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java +28 -0
  19. src/main/java/com/attendenceSystem/module/otp/mapper/request/VerifyOtpRequestMapper.java +9 -0
  20. src/main/java/com/attendenceSystem/module/otp/mapper/response/OtpResponseMapper.java +22 -0
  21. src/main/java/com/attendenceSystem/module/otp/repository/OtpRepository.java +30 -0
  22. src/main/java/com/attendenceSystem/module/otp/service/OptService.java +12 -0
  23. src/main/java/com/attendenceSystem/module/otp/service/OtpSender.java +6 -0
  24. src/main/java/com/attendenceSystem/module/otp/service/impl/EmailOtpSenderImpl.java +35 -0
  25. src/main/java/com/attendenceSystem/module/otp/service/impl/OtpServiceImpl.java +76 -0
  26. src/main/java/com/attendenceSystem/module/user/controller/AuthController.java +47 -0
  27. src/main/java/com/attendenceSystem/module/user/controller/UserController.java +42 -1
  28. src/main/java/com/attendenceSystem/module/user/dto/request/ChangePasswordRequest.java +14 -0
  29. src/main/java/com/attendenceSystem/module/user/dto/request/RegisterRequest.java +2 -2
  30. src/main/java/com/attendenceSystem/module/user/dto/request/UpdatePasswordRequest.java +13 -0
  31. src/main/java/com/attendenceSystem/module/user/dto/request/UpdatePasswordWithOtpRequest.java +13 -0
  32. src/main/java/com/attendenceSystem/module/user/service/AuthService.java +2 -1
  33. src/main/java/com/attendenceSystem/module/user/service/UserService.java +9 -0
  34. src/main/java/com/attendenceSystem/module/user/service/impl/AuthServiceImpl.java +38 -1
  35. src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java +42 -3
  36. src/main/java/com/attendenceSystem/validator/{PasswordMatchValidator.java → RegisterPasswordMatchValidator.java} +3 -3
  37. src/main/java/com/attendenceSystem/validator/UpdatePasswordMatchValidator.java +20 -0
  38. src/main/resources/application.properties +7 -6
  39. src/main/resources/templates/cms/absent/absent-create.html +10 -3
  40. src/main/resources/templates/cms/absent/absent-detail.html +13 -0
  41. src/main/resources/templates/cms/absent/absent-list.html +10 -7
  42. src/main/resources/templates/cms/attendance/attendance-history.html +17 -29
  43. src/main/resources/templates/cms/auth/verify-otp.html +2 -2
  44. src/main/resources/templates/cms/password/change-password.html +21 -15
  45. src/main/resources/templates/cms/sidebar/sidebar-admin.html +4 -4
  46. src/main/resources/templates/cms/sidebar/sidebar-employee.html +5 -5
  47. src/main/resources/templates/cms/sidebar/sidebar-manage.html +5 -5
  48. src/main/resources/templates/cms/user/user-information.html +5 -5
mvnw CHANGED
File without changes
pom.xml CHANGED
@@ -35,6 +35,10 @@
35
  <groupId>org.springframework.boot</groupId>
36
  <artifactId>spring-boot-starter-data-jpa</artifactId>
37
  </dependency> -->
 
 
 
 
38
  <dependency>
39
  <groupId>org.thymeleaf.extras</groupId>
40
  <artifactId>thymeleaf-extras-springsecurity6</artifactId>
 
35
  <groupId>org.springframework.boot</groupId>
36
  <artifactId>spring-boot-starter-data-jpa</artifactId>
37
  </dependency> -->
38
+ <dependency>
39
+ <groupId>org.springframework.boot</groupId>
40
+ <artifactId>spring-boot-starter-mail</artifactId>
41
+ </dependency>
42
  <dependency>
43
  <groupId>org.thymeleaf.extras</groupId>
44
  <artifactId>thymeleaf-extras-springsecurity6</artifactId>
src/main/java/com/attendenceSystem/annotation/RegisterPasswordMatch.java ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.annotation;
2
+
3
+ import java.lang.annotation.ElementType;
4
+ import java.lang.annotation.Retention;
5
+ import java.lang.annotation.RetentionPolicy;
6
+ import java.lang.annotation.Target;
7
+
8
+ import com.attendenceSystem.validator.RegisterPasswordMatchValidator;
9
+
10
+ import jakarta.validation.Constraint;
11
+ import jakarta.validation.Payload;
12
+
13
+ @Target(ElementType.TYPE)
14
+ @Retention(RetentionPolicy.RUNTIME)
15
+ @Constraint(validatedBy = RegisterPasswordMatchValidator.class)
16
+ public @interface RegisterPasswordMatch {
17
+
18
+ String message() default "Mật khẩu xác nhận không khớp";
19
+
20
+ Class<?>[] groups() default {};
21
+
22
+ Class<? extends Payload>[] payload() default {};
23
+ }
src/main/java/com/attendenceSystem/annotation/{PasswordMatch.java → UpdatePasswordMatch.java} RENAMED
@@ -5,19 +5,18 @@ import java.lang.annotation.Retention;
5
  import java.lang.annotation.RetentionPolicy;
6
  import java.lang.annotation.Target;
7
 
8
- import com.attendenceSystem.validator.PasswordMatchValidator;
9
 
10
  import jakarta.validation.Constraint;
11
  import jakarta.validation.Payload;
12
 
13
  @Target(ElementType.TYPE)
14
  @Retention(RetentionPolicy.RUNTIME)
15
- @Constraint(validatedBy = PasswordMatchValidator.class)
16
- public @interface PasswordMatch {
17
-
18
  String message() default "Mật khẩu xác nhận không khớp";
19
 
20
  Class<?>[] groups() default {};
21
 
22
  Class<? extends Payload>[] payload() default {};
23
- }
 
5
  import java.lang.annotation.RetentionPolicy;
6
  import java.lang.annotation.Target;
7
 
8
+ import com.attendenceSystem.validator.UpdatePasswordMatchValidator;
9
 
10
  import jakarta.validation.Constraint;
11
  import jakarta.validation.Payload;
12
 
13
  @Target(ElementType.TYPE)
14
  @Retention(RetentionPolicy.RUNTIME)
15
+ @Constraint(validatedBy = UpdatePasswordMatchValidator.class)
16
+ public @interface UpdatePasswordMatch {
 
17
  String message() default "Mật khẩu xác nhận không khớp";
18
 
19
  Class<?>[] groups() default {};
20
 
21
  Class<? extends Payload>[] payload() default {};
22
+ }
src/main/java/com/attendenceSystem/constant/Routes.java CHANGED
@@ -23,12 +23,15 @@ public final class Routes {
23
  public static final String FORGOT_PASSWORD = "/forgot-password";
24
  public static final String VERIFY_OTP = "/verify-otp";
25
  public static final String CHANGE_PASSWORD = "/change-password";
 
26
  public static final String LOGOUT = "/logout";
27
  }
28
 
29
  public static final class User {
30
  public static final String ROOT = "/user";
31
  public static final String PROFILE = "/profile";
 
 
32
  }
33
 
34
  public static final class Dashboard {
 
23
  public static final String FORGOT_PASSWORD = "/forgot-password";
24
  public static final String VERIFY_OTP = "/verify-otp";
25
  public static final String CHANGE_PASSWORD = "/change-password";
26
+ public static final String UPDATE_PASSWORD = "/update-password";
27
  public static final String LOGOUT = "/logout";
28
  }
29
 
30
  public static final class User {
31
  public static final String ROOT = "/user";
32
  public static final String PROFILE = "/profile";
33
+ public static final String CHANGE_PASSWORD = "/change-password";
34
+ public static final String UPDATE_PASSWORD = "/update-password";
35
  }
36
 
37
  public static final class Dashboard {
src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java CHANGED
@@ -71,4 +71,9 @@ public class AttendanceCalculator {
71
  }
72
  return Duration.between(checkInTime, checkOutTime).toMinutes();
73
  }
 
 
 
 
 
74
  }
 
71
  }
72
  return Duration.between(checkInTime, checkOutTime).toMinutes();
73
  }
74
+
75
+ public double totalWorkingHours(Instant checkInTime, Instant checkOutTime) {
76
+ long minutes = workingMinutes(checkInTime, checkOutTime);
77
+ return minutes / 60.0;
78
+ }
79
  }
src/main/java/com/attendenceSystem/module/otp/README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OTP Module
2
+
3
+ ## Overview
4
+ Module OTP là module hỗ trợ (support module) xử lý sinh, gửi và xác minh mã OTP (One-Time Password) cho các mục đích:
5
+ - Quên mật khẩu (FORGOT_PASSWORD)
6
+ - Đăng ký tài khoản (REGISTER)
7
+ - Thay đổi email (CHANGE_EMAIL)
8
+
9
+ Module này được các module khác (ví dụ: user module) gọi thông qua REST API.
10
+
11
+ ## Cấu trúc
12
+
13
+ ### DTOs
14
+
15
+ #### Request (Class)
16
+ - `SendOtpRequest` - Yêu cầu gửi OTP
17
+ - destination: email/số điện thoại
18
+ - purpose: mục đích sử dụng OTP
19
+
20
+ - `VerifyOtpRequest` - Yêu cầu xác minh OTP
21
+ - destination: email/số điện thoại
22
+ - code: mã OTP
23
+ - purpose: mục đích sử dụng OTP
24
+
25
+ #### Response (Record)
26
+ - `OtpResponse` - Thông tin OTP đã tạo
27
+ - id: định danh OTP
28
+ - destination: email/số điện thoại
29
+ - purpose: mục đích
30
+ - expiredAt: thời gian hết hạn
31
+ - createdAt: thời gian tạo
32
+ - used: đã sử dụng hay chưa
33
+
34
+ ### Entities
35
+ - `Otp` - Entity lưu trữ OTP trong database
36
+ - `OtpPurpose` - Enum định nghĩa các mục đích sử dụng OTP
37
+
38
+ ### Services
39
+ - `OptService` - Interface chính
40
+ - `OtpSender` - Interface gửi OTP
41
+ - `OtpServiceImpl` - Implementation chính
42
+ - `OtpSenderImpl` - Implementation gửi OTP (stub)
43
+
44
+ ### Repositories
45
+ - `OtpRepository` - JPA Repository với custom methods
46
+
47
+ ### Controllers
48
+ - `OtpApiController` - REST API endpoints (chỉ có REST API, không có MVC controller)
49
+
50
+ ### Mappers
51
+ - `SendOtpRequestMapper` - Chuyển SendOtpRequest → Otp entity
52
+ - `OtpResponseMapper` - Chuyển Otp entity → OtpResponse
53
+
54
+ ## Endpoints
55
+
56
+ ### REST API
57
+ - `POST /api/otp/send` - Gửi OTP
58
+ - `POST /api/otp/verify` - Xác minh OTP
59
+
60
+ ## Cấu hình
61
+ - OTP có độ dài 6 số
62
+ - Thời gian hết hạn: 5 phút
63
+ - Mã OTP được tạo ngẫu nhiên
64
+
65
+ ## Luồng hoạt động
66
+
67
+ ### Gửi OTP
68
+ 1. Nhận request với destination và purpose
69
+ 2. Tạo mã OTP ngẫu nhiên 6 số
70
+ 3. Lưu OTP vào database (status: unused)
71
+ 4. Gửi OTP qua channel phù hợp (email/SMS)
72
+ 5. Trả về thông tin OTP
73
+
74
+ ### Xác minh OTP
75
+ 1. Tìm OTP mới nhất cho destination và purpose (chưa sử dụng)
76
+ 2. Kiểm tra OTP có hết hạn không
77
+ 3. So sánh mã OTP
78
+ 4. Đánh dấu OTP đã sử dụng
79
+ 5. Trả về thông tin OTP đã xác minh
80
+
81
+ ## Patterns
82
+ - Request DTOs: Class
83
+ - Response DTOs: Record
84
+ - Mapper: Static methods
85
+ - Service: Constructor injection với Lombok @RequiredArgsConstructor
src/main/java/com/attendenceSystem/module/otp/api/OtpApiController.java ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.api;
2
+
3
+ import org.springframework.http.ResponseEntity;
4
+ import org.springframework.web.bind.annotation.PostMapping;
5
+ import org.springframework.web.bind.annotation.RequestBody;
6
+ import org.springframework.web.bind.annotation.RequestMapping;
7
+ import org.springframework.web.bind.annotation.RestController;
8
+
9
+ import com.attendenceSystem.constant.Routes;
10
+ import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
11
+ import com.attendenceSystem.module.otp.dto.request.VerifyOtpRequest;
12
+ import com.attendenceSystem.module.otp.dto.response.OtpResponse;
13
+ import com.attendenceSystem.module.otp.service.OptService;
14
+
15
+ import jakarta.validation.Valid;
16
+ import lombok.RequiredArgsConstructor;
17
+
18
+ @RestController
19
+ @RequestMapping(Routes.API + "/otp")
20
+ @RequiredArgsConstructor
21
+ public class OtpApiController {
22
+
23
+ private final OptService otpService;
24
+
25
+ @PostMapping("/send")
26
+ public ResponseEntity<OtpResponse> sendOtp(@Valid @RequestBody SendOtpRequest request) {
27
+ OtpResponse response = otpService.send(request);
28
+ return ResponseEntity.ok(response);
29
+ }
30
+
31
+ @PostMapping("/verify")
32
+ public ResponseEntity<OtpResponse> verifyOtp(@Valid @RequestBody VerifyOtpRequest request) {
33
+ OtpResponse response = otpService.verify(request);
34
+ return ResponseEntity.ok(response);
35
+ }
36
+ }
src/main/java/com/attendenceSystem/module/otp/dto/request/SendOtpRequest.java ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.dto.request;
2
+
3
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
4
+
5
+ import jakarta.validation.constraints.NotBlank;
6
+ import jakarta.validation.constraints.NotNull;
7
+ import lombok.AllArgsConstructor;
8
+ import lombok.Builder;
9
+ import lombok.Data;
10
+ import lombok.NoArgsConstructor;
11
+
12
+ @Data
13
+ @NoArgsConstructor
14
+ @AllArgsConstructor
15
+ @Builder
16
+ public class SendOtpRequest {
17
+ @NotBlank(message = "Điểm đến (email/số điện thoại) không được để trống")
18
+ private String destination;
19
+
20
+ @NotNull(message = "Mục đích OTP không được để trống")
21
+ private OtpPurpose purpose;
22
+ }
src/main/java/com/attendenceSystem/module/otp/dto/request/VerifyOtpRequest.java ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.dto.request;
2
+
3
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
4
+
5
+ import jakarta.validation.constraints.NotBlank;
6
+ import jakarta.validation.constraints.NotNull;
7
+ import lombok.AllArgsConstructor;
8
+ import lombok.Builder;
9
+ import lombok.Data;
10
+ import lombok.NoArgsConstructor;
11
+
12
+ @Data
13
+ @NoArgsConstructor
14
+ @AllArgsConstructor
15
+ @Builder
16
+ public class VerifyOtpRequest {
17
+ @NotBlank(message = "Điểm đến (email/số điện thoại) không được để trống")
18
+ private String destination;
19
+
20
+ @NotBlank(message = "Mã OTP không được để trống")
21
+ private String code;
22
+
23
+ @NotNull(message = "Mục đích OTP không được để trống")
24
+ private OtpPurpose purpose;
25
+ }
src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.dto.response;
2
+
3
+ import java.time.Instant;
4
+
5
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
6
+
7
+ public record OtpResponse(
8
+ Long id,
9
+ String destination,
10
+ OtpPurpose purpose,
11
+ Instant expiredAt,
12
+ Instant createdAt,
13
+ boolean used
14
+ ) {
15
+ }
src/main/java/com/attendenceSystem/module/otp/entity/Otp.java ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.entity;
2
+
3
+ import java.time.Instant;
4
+
5
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
6
+
7
+ import jakarta.persistence.Column;
8
+ import jakarta.persistence.Entity;
9
+ import jakarta.persistence.GeneratedValue;
10
+ import jakarta.persistence.GenerationType;
11
+ import jakarta.persistence.Id;
12
+ import jakarta.persistence.Table;
13
+ import lombok.AllArgsConstructor;
14
+ import lombok.Builder;
15
+ import lombok.Getter;
16
+ import lombok.NoArgsConstructor;
17
+ import lombok.Setter;
18
+
19
+ @Entity
20
+ @Table(name = "otp")
21
+ @NoArgsConstructor
22
+ @AllArgsConstructor
23
+ @Getter
24
+ @Setter
25
+ @Builder
26
+ public class Otp {
27
+ @Id
28
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
29
+ @Column(name = "id")
30
+ private Long id;
31
+ @Column(name = "destination", nullable = false)
32
+ private String destination;
33
+ @Column(name = "code", nullable = false)
34
+ private String code;
35
+ @Column(name = "purpose", nullable = false)
36
+ private OtpPurpose purpose;
37
+ @Column(name = "expired_at", nullable = false)
38
+ private Instant expiredAt;
39
+ @Column(name = "created_at", nullable = false)
40
+ private Instant createdAt;
41
+ @Column(name = "used", nullable = false)
42
+ private boolean used;
43
+
44
+ public boolean isExpired() {
45
+ return Instant.now().isAfter(expiredAt);
46
+ }
47
+ }
src/main/java/com/attendenceSystem/module/otp/entity/converter/OtpPurposeConverter.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.entity.converter;
2
+
3
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
4
+
5
+ import jakarta.persistence.AttributeConverter;
6
+ import jakarta.persistence.Converter;
7
+
8
+ @Converter(autoApply = true)
9
+ public class OtpPurposeConverter implements AttributeConverter<OtpPurpose, Integer> {
10
+ @Override
11
+ public Integer convertToDatabaseColumn(OtpPurpose attribute) {
12
+ return attribute != null ? attribute.getValue() : null;
13
+ }
14
+
15
+ @Override
16
+ public OtpPurpose convertToEntityAttribute(Integer dbData) {
17
+ return dbData != null ? OtpPurpose.fromValue(dbData) : null;
18
+ }
19
+
20
+ }
src/main/java/com/attendenceSystem/module/otp/entity/enums/OtpPurpose.java ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.entity.enums;
2
+
3
+ import lombok.Getter;
4
+ import lombok.RequiredArgsConstructor;
5
+
6
+ @RequiredArgsConstructor
7
+ @Getter
8
+ public enum OtpPurpose {
9
+ FORGOT_PASSWORD(1, "Reset Password"),
10
+ REGISTER(2, "Register Account"),
11
+ CHANGE_EMAIL(3, "Change Email");
12
+
13
+ private final int value;
14
+ private final String name;
15
+
16
+ public static OtpPurpose fromValue(int value) {
17
+ for (OtpPurpose p : OtpPurpose.values()) {
18
+ if (p.value == value) {
19
+ return p;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ public static OtpPurpose fromName(String name) {
25
+ for (OtpPurpose p : OtpPurpose.values()) {
26
+ if (p.name.equals(name)) {
27
+ return p;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ }
src/main/java/com/attendenceSystem/module/otp/exception/OtpExpiredException.java ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.exception;
2
+
3
+ public class OtpExpiredException extends RuntimeException {
4
+ public OtpExpiredException(String message) {
5
+ super(message);
6
+ }
7
+ }
src/main/java/com/attendenceSystem/module/otp/exception/OtpInvalidException.java ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.exception;
2
+
3
+ public class OtpInvalidException extends RuntimeException {
4
+ public OtpInvalidException(String message) {
5
+ super(message);
6
+ }
7
+ }
src/main/java/com/attendenceSystem/module/otp/exception/OtpNotFoundException.java ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.exception;
2
+
3
+ public class OtpNotFoundException extends RuntimeException {
4
+ public OtpNotFoundException(String message) {
5
+ super(message);
6
+ }
7
+ }
src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.mapper.request;
2
+
3
+ import java.time.Instant;
4
+ import java.time.temporal.ChronoUnit;
5
+
6
+ import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
7
+ import com.attendenceSystem.module.otp.entity.Otp;
8
+
9
+ import lombok.AccessLevel;
10
+ import lombok.NoArgsConstructor;
11
+
12
+ @NoArgsConstructor(access = AccessLevel.PRIVATE)
13
+ public class SendOtpRequestMapper {
14
+
15
+ private static final int OTP_EXPIRY_MINUTES = 5;
16
+
17
+ public static Otp toEntity(SendOtpRequest request, String code) {
18
+ Instant now = Instant.now();
19
+ return Otp.builder()
20
+ .destination(request.getDestination())
21
+ .code(code)
22
+ .purpose(request.getPurpose())
23
+ .expiredAt(now.plus(OTP_EXPIRY_MINUTES, ChronoUnit.MINUTES))
24
+ .createdAt(now)
25
+ .used(false)
26
+ .build();
27
+ }
28
+ }
src/main/java/com/attendenceSystem/module/otp/mapper/request/VerifyOtpRequestMapper.java ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.mapper.request;
2
+
3
+ import lombok.AccessLevel;
4
+ import lombok.NoArgsConstructor;
5
+
6
+ @NoArgsConstructor(access = AccessLevel.PRIVATE)
7
+ public class VerifyOtpRequestMapper {
8
+ // No mapping needed for now; verification logic is in the service
9
+ }
src/main/java/com/attendenceSystem/module/otp/mapper/response/OtpResponseMapper.java ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.mapper.response;
2
+
3
+ import com.attendenceSystem.module.otp.dto.response.OtpResponse;
4
+ import com.attendenceSystem.module.otp.entity.Otp;
5
+
6
+ import lombok.AccessLevel;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ @NoArgsConstructor(access = AccessLevel.PRIVATE)
10
+ public class OtpResponseMapper {
11
+
12
+ public static OtpResponse fromEntity(Otp otp) {
13
+ return new OtpResponse(
14
+ otp.getId(),
15
+ otp.getDestination(),
16
+ otp.getPurpose(),
17
+ otp.getExpiredAt(),
18
+ otp.getCreatedAt(),
19
+ otp.isUsed()
20
+ );
21
+ }
22
+ }
src/main/java/com/attendenceSystem/module/otp/repository/OtpRepository.java ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.repository;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Optional;
5
+
6
+ import org.springframework.data.jpa.repository.JpaRepository;
7
+ import org.springframework.data.jpa.repository.Modifying;
8
+ import org.springframework.data.jpa.repository.Query;
9
+ import org.springframework.data.repository.query.Param;
10
+ import org.springframework.stereotype.Repository;
11
+
12
+ import com.attendenceSystem.module.otp.entity.Otp;
13
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
14
+
15
+ @Repository
16
+ public interface OtpRepository extends JpaRepository<Otp, Long> {
17
+
18
+ Optional<Otp> findTopByDestinationAndPurposeAndUsedFalseOrderByCreatedAtDesc(
19
+ String destination, OtpPurpose purpose);
20
+
21
+ @Modifying
22
+ @Query("UPDATE Otp o SET o.used = true WHERE o.destination = :destination AND o.purpose = :purpose")
23
+ void invalidateByDestinationAndPurpose(
24
+ @Param("destination") String destination,
25
+ @Param("purpose") OtpPurpose purpose);
26
+
27
+ @Modifying
28
+ @Query("DELETE FROM Otp o WHERE o.expiredAt < :now")
29
+ void deleteExpiredOtps(@Param("now") Instant now);
30
+ }
src/main/java/com/attendenceSystem/module/otp/service/OptService.java ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.service;
2
+
3
+ import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
4
+ import com.attendenceSystem.module.otp.dto.request.VerifyOtpRequest;
5
+ import com.attendenceSystem.module.otp.dto.response.OtpResponse;
6
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
7
+
8
+ public interface OptService {
9
+ OtpResponse send(SendOtpRequest request);
10
+ OtpResponse verify(VerifyOtpRequest request);
11
+ void invalidate(String destination, OtpPurpose purpose);
12
+ }
src/main/java/com/attendenceSystem/module/otp/service/OtpSender.java ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.service;
2
+
3
+ public interface OtpSender {
4
+ // OtpChannel getChannel();
5
+ void send(String destination, String code);
6
+ }
src/main/java/com/attendenceSystem/module/otp/service/impl/EmailOtpSenderImpl.java ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.service.impl;
2
+
3
+ import org.springframework.mail.javamail.JavaMailSender;
4
+ import org.springframework.mail.javamail.MimeMessageHelper;
5
+ import org.springframework.stereotype.Service;
6
+
7
+ import com.attendenceSystem.module.otp.service.OtpSender;
8
+
9
+ import jakarta.mail.MessagingException;
10
+ import jakarta.mail.internet.MimeMessage;
11
+ import lombok.RequiredArgsConstructor;
12
+
13
+ @Service
14
+ @RequiredArgsConstructor
15
+ public class EmailOtpSenderImpl implements OtpSender {
16
+ private final JavaMailSender mailSender;
17
+ private static final String SUBJECT = "Mã OTP xác thực";
18
+
19
+ @Override
20
+ public void send(String destination, String code) {
21
+ try {
22
+ MimeMessage message = mailSender.createMimeMessage();
23
+ MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
24
+
25
+ helper.setTo(destination);
26
+ helper.setSubject(SUBJECT);
27
+ helper.setText("Mã OTP của bạn là: <strong>" + code + "</strong><br>"
28
+ + "Mã này có hiệu lực trong 5 phút.", true);
29
+
30
+ mailSender.send(message);
31
+ } catch (MessagingException e) {
32
+ throw new RuntimeException("Không thể gửi email OTP: " + e.getMessage(), e);
33
+ }
34
+ }
35
+ }
src/main/java/com/attendenceSystem/module/otp/service/impl/OtpServiceImpl.java ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.otp.service.impl;
2
+
3
+ import java.security.SecureRandom;
4
+
5
+ import org.springframework.stereotype.Service;
6
+ import org.springframework.transaction.annotation.Transactional;
7
+
8
+ import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
9
+ import com.attendenceSystem.module.otp.dto.request.VerifyOtpRequest;
10
+ import com.attendenceSystem.module.otp.dto.response.OtpResponse;
11
+ import com.attendenceSystem.module.otp.entity.Otp;
12
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
13
+ import com.attendenceSystem.module.otp.exception.OtpExpiredException;
14
+ import com.attendenceSystem.module.otp.exception.OtpInvalidException;
15
+ import com.attendenceSystem.module.otp.exception.OtpNotFoundException;
16
+ import com.attendenceSystem.module.otp.mapper.request.SendOtpRequestMapper;
17
+ import com.attendenceSystem.module.otp.mapper.response.OtpResponseMapper;
18
+ import com.attendenceSystem.module.otp.repository.OtpRepository;
19
+ import com.attendenceSystem.module.otp.service.OptService;
20
+ import com.attendenceSystem.module.otp.service.OtpSender;
21
+
22
+ import lombok.RequiredArgsConstructor;
23
+
24
+ @Service
25
+ @RequiredArgsConstructor
26
+ public class OtpServiceImpl implements OptService {
27
+ private final OtpRepository otpRepository;
28
+ private final OtpSender otpSender;
29
+
30
+ private static final SecureRandom RANDOM = new SecureRandom();
31
+
32
+ @Transactional
33
+ @Override
34
+ public OtpResponse send(SendOtpRequest request) {
35
+ invalidate(request.getDestination(), request.getPurpose());
36
+
37
+ String code = generateOtpCode();
38
+
39
+ Otp otp = SendOtpRequestMapper.toEntity(request, code);
40
+ Otp savedOtp = otpRepository.save(otp);
41
+
42
+ otpSender.send(savedOtp.getDestination(), savedOtp.getCode());
43
+
44
+ return OtpResponseMapper.fromEntity(savedOtp);
45
+ }
46
+
47
+ @Transactional
48
+ @Override
49
+ public OtpResponse verify(VerifyOtpRequest request) {
50
+ Otp otp = otpRepository.findTopByDestinationAndPurposeAndUsedFalseOrderByCreatedAtDesc(
51
+ request.getDestination(), request.getPurpose())
52
+ .orElseThrow(() -> new OtpNotFoundException("Không tìm thấy OTP cho mục đích này"));
53
+
54
+ if (otp.isExpired()) {
55
+ throw new OtpExpiredException("Mã OTP đã hết hạn. Vui lòng gửi lại OTP mới.");
56
+ }
57
+
58
+ if (!otp.getCode().equals(request.getCode())) {
59
+ throw new OtpInvalidException("Mã OTP không chính xác.");
60
+ }
61
+
62
+ otp.setUsed(true);
63
+ otpRepository.save(otp);
64
+
65
+ return OtpResponseMapper.fromEntity(otp);
66
+ }
67
+
68
+ @Override
69
+ public void invalidate(String destination, OtpPurpose purpose) {
70
+ otpRepository.invalidateByDestinationAndPurpose(destination, purpose);
71
+ }
72
+
73
+ private String generateOtpCode() {
74
+ return "%06d".formatted(RANDOM.nextInt(1_000_000));
75
+ }
76
+ }
src/main/java/com/attendenceSystem/module/user/controller/AuthController.java CHANGED
@@ -19,6 +19,11 @@ import com.attendenceSystem.module.user.dto.request.LoginRequest;
19
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
20
  import com.attendenceSystem.module.user.dto.response.UserResponse;
21
  import com.attendenceSystem.module.user.service.AuthService;
 
 
 
 
 
22
 
23
  import jakarta.servlet.http.HttpServletRequest;
24
  import jakarta.servlet.http.HttpServletResponse;
@@ -28,6 +33,7 @@ import org.springframework.web.bind.annotation.GetMapping;
28
  import org.springframework.web.bind.annotation.ModelAttribute;
29
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
30
  import org.springframework.web.bind.annotation.PostMapping;
 
31
 
32
  @Controller
33
  @RequestMapping(Routes.Auth.ROOT)
@@ -124,4 +130,45 @@ public class AuthController {
124
 
125
  return Routes.REDIRECT+"/";
126
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
 
19
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
20
  import com.attendenceSystem.module.user.dto.response.UserResponse;
21
  import com.attendenceSystem.module.user.service.AuthService;
22
+ import com.attendenceSystem.module.user.service.UserService;
23
+
24
+ import com.attendenceSystem.module.otp.exception.OtpExpiredException;
25
+ import com.attendenceSystem.module.otp.exception.OtpInvalidException;
26
+ import com.attendenceSystem.module.otp.exception.OtpNotFoundException;
27
 
28
  import jakarta.servlet.http.HttpServletRequest;
29
  import jakarta.servlet.http.HttpServletResponse;
 
33
  import org.springframework.web.bind.annotation.ModelAttribute;
34
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
35
  import org.springframework.web.bind.annotation.PostMapping;
36
+ import org.springframework.web.bind.annotation.RequestParam;
37
 
38
  @Controller
39
  @RequestMapping(Routes.Auth.ROOT)
 
130
 
131
  return Routes.REDIRECT+"/";
132
  }
133
+
134
+ @PostMapping(Routes.Auth.FORGOT_PASSWORD)
135
+ public String forgotPassword(
136
+ @RequestParam("email") String email,
137
+ RedirectAttributes redirectAttributes) {
138
+ try {
139
+ authService.forgotPassword(email);
140
+ redirectAttributes.addFlashAttribute("successMessage",
141
+ "Mã OTP đã được gửi đến email của bạn. Vui lòng kiểm tra email.");
142
+ redirectAttributes.addFlashAttribute("email", email);
143
+ return Routes.REDIRECT + Routes.Auth.ROOT + Routes.Auth.VERIFY_OTP;
144
+ } catch (IllegalArgumentException e) {
145
+ redirectAttributes.addFlashAttribute("errorMessage", e.getMessage());
146
+ return Routes.REDIRECT + Routes.Auth.ROOT + Routes.Auth.FORGOT_PASSWORD;
147
+ }
148
+ }
149
+
150
+ @PostMapping(Routes.Auth.VERIFY_OTP)
151
+ public String verifyOtp(
152
+ @RequestParam("email") String email,
153
+ @RequestParam("otp") String otpCode,
154
+ Model model,
155
+ RedirectAttributes redirectAttributes) {
156
+ try {
157
+ boolean isValid = authService.verifyOtp(email, otpCode);
158
+ if (isValid) {
159
+ redirectAttributes.addFlashAttribute("email", email);
160
+ redirectAttributes.addFlashAttribute("otpVerified", true);
161
+ return Routes.REDIRECT + Routes.Auth.ROOT + Routes.Auth.CHANGE_PASSWORD;
162
+ } else {
163
+ model.addAttribute("errorMessage", "Mã OTP không chính xác hoặc đã hết hạn.");
164
+ model.addAttribute("email", email);
165
+ return Views.Auth.VERIFY_OTP;
166
+ }
167
+ } catch (Exception e) {
168
+ model.addAttribute("errorMessage", e.getMessage());
169
+ model.addAttribute("email", email);
170
+ return Views.Auth.VERIFY_OTP;
171
+ }
172
+ }
173
+
174
  }
src/main/java/com/attendenceSystem/module/user/controller/UserController.java CHANGED
@@ -8,6 +8,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
8
 
9
  import com.attendenceSystem.constant.Routes;
10
  import com.attendenceSystem.constant.Views;
 
 
 
11
  import com.attendenceSystem.module.user.dto.response.UserResponse;
12
  import com.attendenceSystem.module.user.service.UserService;
13
  import com.attendenceSystem.util.SecurityUtil;
@@ -16,7 +19,7 @@ import lombok.RequiredArgsConstructor;
16
  import org.springframework.web.bind.annotation.GetMapping;
17
  import org.springframework.web.bind.annotation.PathVariable;
18
  import org.springframework.web.bind.annotation.PostMapping;
19
- import org.springframework.web.bind.annotation.RequestBody;
20
 
21
  @Controller
22
  @RequestMapping(Routes.User.ROOT)
@@ -56,5 +59,43 @@ public class UserController {
56
  return Routes.REDIRECT + Routes.User.ROOT;
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  }
 
8
 
9
  import com.attendenceSystem.constant.Routes;
10
  import com.attendenceSystem.constant.Views;
11
+ import com.attendenceSystem.module.user.dto.request.ChangePasswordRequest;
12
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordRequest;
13
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordWithOtpRequest;
14
  import com.attendenceSystem.module.user.dto.response.UserResponse;
15
  import com.attendenceSystem.module.user.service.UserService;
16
  import com.attendenceSystem.util.SecurityUtil;
 
19
  import org.springframework.web.bind.annotation.GetMapping;
20
  import org.springframework.web.bind.annotation.PathVariable;
21
  import org.springframework.web.bind.annotation.PostMapping;
22
+ import org.springframework.web.bind.annotation.RequestParam;
23
 
24
  @Controller
25
  @RequestMapping(Routes.User.ROOT)
 
59
  return Routes.REDIRECT + Routes.User.ROOT;
60
  }
61
 
62
+ @PostMapping(Routes.User.CHANGE_PASSWORD)
63
+ public String changePassword(
64
+ @RequestParam("oldPassword") String oldPassword,
65
+ @RequestParam("newPassword") String newPassword,
66
+ @RequestParam("confirmPassword") String confirmPassword,
67
+ Model model) {
68
+ try {
69
+ if (!newPassword.equals(confirmPassword)) {
70
+ throw new IllegalArgumentException("Mật khẩu xác nhận không khớp");
71
+ }
72
+ ChangePasswordRequest request = new ChangePasswordRequest(oldPassword, new UpdatePasswordRequest(newPassword, confirmPassword));
73
+ userService.changePassword(request);
74
+ return Routes.REDIRECT + Routes.User.ROOT + Routes.User.PROFILE + "?success=true";
75
+ } catch (Exception e) {
76
+ model.addAttribute("errorMessage", e.getMessage());
77
+ return Views.User.PROFILE;
78
+ }
79
+ }
80
+
81
+ @PostMapping(Routes.User.UPDATE_PASSWORD)
82
+ public String updatePasswordWithOtp(
83
+ @RequestParam("destination") String destination,
84
+ @RequestParam("password") String password,
85
+ @RequestParam("confirmPassword") String confirmPassword,
86
+ Model model) {
87
+ try {
88
+ if (!password.equals(confirmPassword)) {
89
+ throw new IllegalArgumentException("Mật khẩu xác nhập không khớp");
90
+ }
91
+ UpdatePasswordWithOtpRequest request = new UpdatePasswordWithOtpRequest(destination, new UpdatePasswordRequest(password, confirmPassword));
92
+ userService.updatePasswordWithOtp(request);
93
+ return Routes.REDIRECT + Routes.Auth.ROOT + Routes.Auth.LOGIN+ "?success=true";
94
+ } catch (Exception e) {
95
+ model.addAttribute("errorMessage", e.getMessage());
96
+ model.addAttribute("email", destination);
97
+ return Views.Auth.CHANGE_PASSWORD;
98
+ }
99
+ }
100
 
101
  }
src/main/java/com/attendenceSystem/module/user/dto/request/ChangePasswordRequest.java ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.user.dto.request;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+
7
+ @Data
8
+ @NoArgsConstructor
9
+ @AllArgsConstructor
10
+ public class ChangePasswordRequest {
11
+
12
+ private String oldPassword;
13
+ private UpdatePasswordRequest request;
14
+ }
src/main/java/com/attendenceSystem/module/user/dto/request/RegisterRequest.java CHANGED
@@ -1,6 +1,6 @@
1
  package com.attendenceSystem.module.user.dto.request;
2
 
3
- import com.attendenceSystem.annotation.PasswordMatch;
4
  import com.fasterxml.jackson.annotation.JsonProperty;
5
 
6
  import jakarta.validation.constraints.Email;
@@ -11,7 +11,7 @@ import lombok.AllArgsConstructor;
11
  import lombok.Data;
12
  import lombok.NoArgsConstructor;
13
 
14
- @PasswordMatch
15
  @Data
16
  @NoArgsConstructor
17
  @AllArgsConstructor
 
1
  package com.attendenceSystem.module.user.dto.request;
2
 
3
+ import com.attendenceSystem.annotation.RegisterPasswordMatch;
4
  import com.fasterxml.jackson.annotation.JsonProperty;
5
 
6
  import jakarta.validation.constraints.Email;
 
11
  import lombok.Data;
12
  import lombok.NoArgsConstructor;
13
 
14
+ @RegisterPasswordMatch
15
  @Data
16
  @NoArgsConstructor
17
  @AllArgsConstructor
src/main/java/com/attendenceSystem/module/user/dto/request/UpdatePasswordRequest.java ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.user.dto.request;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+
7
+ @Data
8
+ @NoArgsConstructor
9
+ @AllArgsConstructor
10
+ public class UpdatePasswordRequest {
11
+ private String password;
12
+ private String confirmPassword;
13
+ }
src/main/java/com/attendenceSystem/module/user/dto/request/UpdatePasswordWithOtpRequest.java ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.user.dto.request;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Data;
5
+ import lombok.NoArgsConstructor;
6
+
7
+ @Data
8
+ @NoArgsConstructor
9
+ @AllArgsConstructor
10
+ public class UpdatePasswordWithOtpRequest {
11
+ private String destination;
12
+ private UpdatePasswordRequest request;
13
+ }
src/main/java/com/attendenceSystem/module/user/service/AuthService.java CHANGED
@@ -8,5 +8,6 @@ public interface AuthService {
8
  UserResponse login(LoginRequest request);
9
 
10
  void register(RegisterRequest request);
11
-
 
12
  }
 
8
  UserResponse login(LoginRequest request);
9
 
10
  void register(RegisterRequest request);
11
+ void forgotPassword(String email);
12
+ boolean verifyOtp(String destination, String code);
13
  }
src/main/java/com/attendenceSystem/module/user/service/UserService.java CHANGED
@@ -3,7 +3,11 @@ package com.attendenceSystem.module.user.service;
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
 
 
 
 
6
  import com.attendenceSystem.module.user.dto.response.UserResponse;
 
7
 
8
  public interface UserService {
9
  Page<UserResponse> getUsers(Pageable pageable);
@@ -11,4 +15,9 @@ public interface UserService {
11
  void deleteUser(Long id);
12
  void deactiveUser(Long id);
13
  void activateUser(Long id);
 
 
 
 
 
14
  }
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
 
6
+ import com.attendenceSystem.module.user.dto.request.ChangePasswordRequest;
7
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordRequest;
8
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordWithOtpRequest;
9
  import com.attendenceSystem.module.user.dto.response.UserResponse;
10
+ import com.attendenceSystem.module.user.entity.User;
11
 
12
  public interface UserService {
13
  Page<UserResponse> getUsers(Pageable pageable);
 
15
  void deleteUser(Long id);
16
  void deactiveUser(Long id);
17
  void activateUser(Long id);
18
+
19
+ void changePassword(ChangePasswordRequest request);
20
+ void updatePassword(User user, UpdatePasswordRequest request);
21
+ void updatePasswordWithOtp(UpdatePasswordWithOtpRequest request);
22
+
23
  }
src/main/java/com/attendenceSystem/module/user/service/impl/AuthServiceImpl.java CHANGED
@@ -13,7 +13,15 @@ import org.springframework.security.core.context.SecurityContextHolder;
13
  import org.springframework.security.crypto.password.PasswordEncoder;
14
  import org.springframework.stereotype.Service;
15
  import org.springframework.transaction.annotation.Transactional;
 
16
 
 
 
 
 
 
 
 
17
  import com.attendenceSystem.module.user.dto.request.LoginRequest;
18
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
19
  import com.attendenceSystem.module.user.dto.response.UserResponse;
@@ -31,7 +39,7 @@ public class AuthServiceImpl implements AuthService {
31
  private final UserRepository userRepository;
32
  private final AuthenticationManager authenticationManager;
33
  private final PasswordEncoder passwordEncoder;
34
-
35
 
36
  @Transactional
37
  @Override
@@ -83,4 +91,33 @@ public class AuthServiceImpl implements AuthService {
83
  private boolean existsByKeyword(String keyword) {
84
  return userRepository.existsByUsernameOrEmail(keyword, keyword);
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
 
13
  import org.springframework.security.crypto.password.PasswordEncoder;
14
  import org.springframework.stereotype.Service;
15
  import org.springframework.transaction.annotation.Transactional;
16
+ import org.springframework.mail.javamail.JavaMailSender;
17
 
18
+ import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
19
+ import com.attendenceSystem.module.otp.dto.request.VerifyOtpRequest;
20
+ import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
21
+ import com.attendenceSystem.module.otp.exception.OtpExpiredException;
22
+ import com.attendenceSystem.module.otp.exception.OtpInvalidException;
23
+ import com.attendenceSystem.module.otp.exception.OtpNotFoundException;
24
+ import com.attendenceSystem.module.otp.service.OptService;
25
  import com.attendenceSystem.module.user.dto.request.LoginRequest;
26
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
27
  import com.attendenceSystem.module.user.dto.response.UserResponse;
 
39
  private final UserRepository userRepository;
40
  private final AuthenticationManager authenticationManager;
41
  private final PasswordEncoder passwordEncoder;
42
+ private final OptService otpService;
43
 
44
  @Transactional
45
  @Override
 
91
  private boolean existsByKeyword(String keyword) {
92
  return userRepository.existsByUsernameOrEmail(keyword, keyword);
93
  }
94
+
95
+ @Override
96
+ public void forgotPassword(String email) {
97
+ User user = findUser(email)
98
+ .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng với email: " + email));
99
+
100
+ SendOtpRequest request = SendOtpRequest.builder()
101
+ .destination(user.getEmail())
102
+ .purpose(OtpPurpose.FORGOT_PASSWORD)
103
+ .build();
104
+
105
+ otpService.send(request);
106
+ }
107
+
108
+ @Override
109
+ public boolean verifyOtp(String destination, String code) {
110
+ try {
111
+ VerifyOtpRequest request = VerifyOtpRequest.builder()
112
+ .destination(destination)
113
+ .code(code)
114
+ .purpose(OtpPurpose.FORGOT_PASSWORD)
115
+ .build();
116
+
117
+ otpService.verify(request);
118
+ return true;
119
+ } catch (Exception e) {
120
+ return false;
121
+ }
122
+ }
123
  }
src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java CHANGED
@@ -2,9 +2,15 @@ package com.attendenceSystem.module.user.service.impl;
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
 
 
 
5
  import org.springframework.stereotype.Service;
6
  import org.springframework.transaction.annotation.Transactional;
7
 
 
 
 
8
  import com.attendenceSystem.module.user.dto.response.UserResponse;
9
  import com.attendenceSystem.module.user.entity.User;
10
  import com.attendenceSystem.module.user.entity.enums.Role;
@@ -19,6 +25,7 @@ import lombok.RequiredArgsConstructor;
19
  @Service
20
  @RequiredArgsConstructor
21
  public class UserServiceImpl implements UserService {
 
22
  private final UserRepository userRepository;
23
  private final UserResponseMapper userResponseMapper;
24
 
@@ -45,7 +52,7 @@ public class UserServiceImpl implements UserService {
45
  public void deactiveUser(Long id) {
46
  User user = findById(id);
47
  validateAdminAction(user);
48
- if(user.getStatus() == Status.INACTIVE){
49
  throw new IllegalStateException("Tài khoản đã bị khóa");
50
  }
51
  user.setStatus(Status.INACTIVE);
@@ -56,7 +63,7 @@ public class UserServiceImpl implements UserService {
56
  public void activateUser(Long id) {
57
  User user = findById(id);
58
  validateAdminAction(user);
59
- if(user.getStatus() == Status.ACTIVE){
60
  throw new IllegalStateException("Tài khoản chưa bị khóa");
61
  }
62
  user.setStatus(Status.ACTIVE);
@@ -81,4 +88,36 @@ public class UserServiceImpl implements UserService {
81
  .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng với id: " + id));
82
  }
83
 
84
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
+ import org.springframework.security.core.Authentication;
6
+ import org.springframework.security.core.context.SecurityContextHolder;
7
+ import org.springframework.security.crypto.password.PasswordEncoder;
8
  import org.springframework.stereotype.Service;
9
  import org.springframework.transaction.annotation.Transactional;
10
 
11
+ import com.attendenceSystem.module.user.dto.request.ChangePasswordRequest;
12
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordRequest;
13
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordWithOtpRequest;
14
  import com.attendenceSystem.module.user.dto.response.UserResponse;
15
  import com.attendenceSystem.module.user.entity.User;
16
  import com.attendenceSystem.module.user.entity.enums.Role;
 
25
  @Service
26
  @RequiredArgsConstructor
27
  public class UserServiceImpl implements UserService {
28
+ private final PasswordEncoder passwordEncoder;
29
  private final UserRepository userRepository;
30
  private final UserResponseMapper userResponseMapper;
31
 
 
52
  public void deactiveUser(Long id) {
53
  User user = findById(id);
54
  validateAdminAction(user);
55
+ if (user.getStatus() == Status.INACTIVE) {
56
  throw new IllegalStateException("Tài khoản đã bị khóa");
57
  }
58
  user.setStatus(Status.INACTIVE);
 
63
  public void activateUser(Long id) {
64
  User user = findById(id);
65
  validateAdminAction(user);
66
+ if (user.getStatus() == Status.ACTIVE) {
67
  throw new IllegalStateException("Tài khoản chưa bị khóa");
68
  }
69
  user.setStatus(Status.ACTIVE);
 
88
  .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng với id: " + id));
89
  }
90
 
91
+ @Transactional
92
+ @Override
93
+ public void changePassword(ChangePasswordRequest request) {
94
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
95
+ if (authentication == null || !authentication.isAuthenticated()) {
96
+ throw new IllegalStateException("Người dùng chưa đăng nhập");
97
+ }
98
+ String currentUsername = authentication.getName();
99
+ User user = userRepository.findByUsername(currentUsername)
100
+ .orElseThrow(() -> new IllegalArgumentException(
101
+ "Không tìm thấy người dùng với tên đăng nhập: " + currentUsername));
102
+ if(!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
103
+ throw new IllegalArgumentException("Mật khẩu cũ không đúng");
104
+ }
105
+ updatePassword(user, request.getRequest());
106
+ }
107
+
108
+ @Transactional
109
+ @Override
110
+ public void updatePassword(User user, UpdatePasswordRequest request) {
111
+ user.setPassword(passwordEncoder.encode(request.getPassword()));
112
+ userRepository.save(user);
113
+ }
114
+
115
+ @Transactional
116
+ @Override
117
+ public void updatePasswordWithOtp(UpdatePasswordWithOtpRequest request) {
118
+ User user = userRepository.findUserByUsernameOrEmail(request.getDestination(), request.getDestination())
119
+ .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng với email: " + request.getDestination()));
120
+
121
+ updatePassword(user, request.getRequest());
122
+ }
123
+ }
src/main/java/com/attendenceSystem/validator/{PasswordMatchValidator.java → RegisterPasswordMatchValidator.java} RENAMED
@@ -1,13 +1,13 @@
1
  package com.attendenceSystem.validator;
2
 
3
- import com.attendenceSystem.annotation.PasswordMatch;
4
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
5
 
6
  import jakarta.validation.ConstraintValidator;
7
  import jakarta.validation.ConstraintValidatorContext;
8
 
9
- public class PasswordMatchValidator
10
- implements ConstraintValidator<PasswordMatch, RegisterRequest> {
11
 
12
  @Override
13
  public boolean isValid(RegisterRequest request,
 
1
  package com.attendenceSystem.validator;
2
 
3
+ import com.attendenceSystem.annotation.RegisterPasswordMatch;
4
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
5
 
6
  import jakarta.validation.ConstraintValidator;
7
  import jakarta.validation.ConstraintValidatorContext;
8
 
9
+ public class RegisterPasswordMatchValidator
10
+ implements ConstraintValidator<RegisterPasswordMatch, RegisterRequest> {
11
 
12
  @Override
13
  public boolean isValid(RegisterRequest request,
src/main/java/com/attendenceSystem/validator/UpdatePasswordMatchValidator.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.validator;
2
+
3
+ import com.attendenceSystem.annotation.UpdatePasswordMatch;
4
+ import com.attendenceSystem.module.user.dto.request.UpdatePasswordRequest;
5
+
6
+ import jakarta.validation.ConstraintValidator;
7
+ import jakarta.validation.ConstraintValidatorContext;
8
+
9
+ public class UpdatePasswordMatchValidator implements
10
+ ConstraintValidator<UpdatePasswordMatch, UpdatePasswordRequest> {
11
+
12
+ @Override
13
+ public boolean isValid(UpdatePasswordRequest request, ConstraintValidatorContext context) {
14
+ if (request.getPassword() == null || request.getConfirmPassword() == null) {
15
+ return false;
16
+ }
17
+ return request.getPassword().equals(request.getConfirmPassword());
18
+ }
19
+
20
+ }
src/main/resources/application.properties CHANGED
@@ -34,10 +34,12 @@ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
34
 
35
 
36
  # SMPT EMAIL
37
- # spring.mail.host=smtp.gmail.com
38
- # spring.mail.port=587
39
- # spring.mail.username=donhatquang401@gmail.com
40
- # spring.mail.password=lzzxdgzmirxdwqdv
 
 
41
 
42
  # JPA configuration
43
  spring.jpa.hibernate.ddl-auto=update
@@ -50,8 +52,7 @@ attendance.end-work=17:00
50
 
51
 
52
 
53
- # spring.mail.properties.mail.smtp.auth=true
54
- # spring.mail.properties.mail.smtp.starttls.enable=true
55
 
56
  # Catch exception
57
  # server.error.include-message=never
 
34
 
35
 
36
  # SMPT EMAIL
37
+ spring.mail.host=smtp.gmail.com
38
+ spring.mail.port=587
39
+ spring.mail.username=donhatquang401@gmail.com
40
+ spring.mail.password=vhlnjkkypjcbopcw
41
+ spring.mail.properties.mail.smtp.auth=true
42
+ spring.mail.properties.mail.smtp.starttls.enable=true
43
 
44
  # JPA configuration
45
  spring.jpa.hibernate.ddl-auto=update
 
52
 
53
 
54
 
55
+
 
56
 
57
  # Catch exception
58
  # server.error.include-message=never
src/main/resources/templates/cms/absent/absent-create.html CHANGED
@@ -38,18 +38,24 @@
38
 
39
  <div class="card-body">
40
 
 
 
 
 
41
  <form class="absent-form" method="post" th:action="@{/attendance/leave/create}" th:object="${createLeaveRequest}">
42
  <!-- Date -->
43
  <div class="form-grid">
44
 
45
  <div class="form-group">
46
  <label>Từ ngày</label>
47
- <input type="date" required>
 
48
  </div>
49
 
50
  <div class="form-group">
51
  <label>Đến ngày</label>
52
- <input type="date" required>
 
53
  </div>
54
 
55
  </div>
@@ -57,8 +63,9 @@
57
  <!-- Lý do -->
58
  <div class="form-group">
59
  <label>Lý do</label>
60
- <textarea rows="5" placeholder="Nhập lý do xin nghỉ..."
61
  required></textarea>
 
62
  </div>
63
 
64
  <!-- BUTTON -->
 
38
 
39
  <div class="card-body">
40
 
41
+ <!-- SUCCESS / ERROR MESSAGES -->
42
+ <div th:if="${successMessage}" class="success-message" th:text="${successMessage}"></div>
43
+ <div th:if="${errorMessage}" class="error-message" th:text="${errorMessage}"></div>
44
+
45
  <form class="absent-form" method="post" th:action="@{/attendance/leave/create}" th:object="${createLeaveRequest}">
46
  <!-- Date -->
47
  <div class="form-grid">
48
 
49
  <div class="form-group">
50
  <label>Từ ngày</label>
51
+ <input type="date" name="startDate" th:field="*{startDate}" required>
52
+ <span th:if="${#fields.hasErrors('startDate')}" th:errors="*{startDate}" class="field-error"></span>
53
  </div>
54
 
55
  <div class="form-group">
56
  <label>Đến ngày</label>
57
+ <input type="date" name="endDate" th:field="*{endDate}" required>
58
+ <span th:if="${#fields.hasErrors('endDate')}" th:errors="*{endDate}" class="field-error"></span>
59
  </div>
60
 
61
  </div>
 
63
  <!-- Lý do -->
64
  <div class="form-group">
65
  <label>Lý do</label>
66
+ <textarea rows="5" name="reason" th:field="*{reason}" placeholder="Nhập lý do xin nghỉ..."
67
  required></textarea>
68
+ <span th:if="${#fields.hasErrors('reason')}" th:errors="*{reason}" class="field-error"></span>
69
  </div>
70
 
71
  <!-- BUTTON -->
src/main/resources/templates/cms/absent/absent-detail.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" xmlns:th="http://www.thymeleaf.org">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Document</title>
7
+ </head>
8
+ <body>
9
+ Trong details sẽ có các thông tin: Tên Nhân viên, tên đầy đủ, vai trò, Phòng ban, Được tạo vào thời điểm nào,
10
+ Từ ngày, Đến ngày, Lý do, Trạng thái, Hành động (Approve/Reject)
11
+ Details không phải là table.
12
+ </body>
13
+ </html>
src/main/resources/templates/cms/absent/absent-list.html CHANGED
@@ -99,12 +99,10 @@
99
  <tr>
100
  <th>Nhân viên</th>
101
  <th>Phòng ban</th>
102
- <th>Lý do</th>
103
  <th>Từ ngày</th>
104
  <th>Đến ngày</th>
105
- <th>Chi tiết</th>
106
  <th>Trạng thái</th>
107
- <th>Hành động</th>
108
  </tr>
109
  </thead>
110
 
@@ -118,15 +116,20 @@
118
  <tr th:each="leave : ${leaveRequests.content}">
119
  <td th:text="${leave.fullName()}">Nhân viên</td>
120
  <td>Phòng ban</td>
121
- <td>CỘT NÀY SAI NGHIỆP VỤ. NÓ NÊN NẰM Ở DETAILS</td>
122
  <td th:text="${leave.startDate()}">Từ ngày</td>
123
  <td th:text="${leave.endDate()}">Đến ngày</td>
124
- <td>Cột này nên nhét vào hành động. Phê duyệt và từ chối nên nhét vào details</td>
125
-
126
  <td>
127
  <span class="status pending">Trạng thái</span>
128
  </td>
 
 
 
 
 
 
 
129
 
 
130
  <td>
131
  <div class="action-group">
132
 
@@ -143,7 +146,7 @@
143
  </form>
144
 
145
  </div>
146
- </td>
147
  </tr>
148
 
149
  </tbody>
 
99
  <tr>
100
  <th>Nhân viên</th>
101
  <th>Phòng ban</th>
 
102
  <th>Từ ngày</th>
103
  <th>Đến ngày</th>
 
104
  <th>Trạng thái</th>
105
+ <th>Chi tiết</th>
106
  </tr>
107
  </thead>
108
 
 
116
  <tr th:each="leave : ${leaveRequests.content}">
117
  <td th:text="${leave.fullName()}">Nhân viên</td>
118
  <td>Phòng ban</td>
 
119
  <td th:text="${leave.startDate()}">Từ ngày</td>
120
  <td th:text="${leave.endDate()}">Đến ngày</td>
 
 
121
  <td>
122
  <span class="status pending">Trạng thái</span>
123
  </td>
124
+ <td>
125
+ <a th:href="@{/leave/detail/{id}(id=${leave.id})}" class="btn btn-small btn-primary">
126
+ <i class="fa-solid fa-eye"></i>
127
+ Xem chi tiết
128
+ </a>
129
+ </td>
130
+
131
 
132
+ <!--
133
  <td>
134
  <div class="action-group">
135
 
 
146
  </form>
147
 
148
  </div>
149
+ </td> -->
150
  </tr>
151
 
152
  </tbody>
src/main/resources/templates/cms/attendance/attendance-history.html CHANGED
@@ -33,10 +33,9 @@
33
  <div class="avatar">
34
  <img src="/images/default-avatar.png" alt="">
35
  </div>
36
-
37
  <div class="profile-info">
38
- <h3>Nguyễn Văn A</h3>
39
- <span>Phòng Công nghệ thông tin</span>
40
  </div>
41
 
42
  </div>
@@ -45,7 +44,7 @@
45
  <div class="stats-grid">
46
 
47
  <div class="stats-card">
48
- <span class="stats-value">22</span>
49
  <span class="stats-label">Ngày làm việc</span>
50
  </div>
51
 
@@ -96,7 +95,7 @@
96
 
97
  <div class="table-header">
98
  <h3>Lịch sử điểm danh</h3>
99
- <span>22 bản ghi</span>
100
  </div>
101
 
102
  <div class="table-scroll">
@@ -115,33 +114,22 @@
115
  </thead>
116
 
117
  <tbody>
118
-
119
- <tr>
120
- <td>23/06/2026</td>
121
- <td>08:01</td>
122
- <td>17:35</td>
123
  <td>
124
- <span class="badge success">
125
- Đúng giờ
126
- </span>
 
127
  </td>
128
- <td>8h 34p</td>
129
- <td>-</td>
130
  </tr>
131
-
132
- <tr>
133
- <td>22/06/2026</td>
134
- <td>08:15</td>
135
- <td>17:30</td>
136
- <td>
137
- <span class="badge late">
138
- Đi muộn
139
- </span>
140
- </td>
141
- <td>8h 15p</td>
142
- <td>Muộn 15 phút</td>
143
  </tr>
144
-
145
  </tbody>
146
 
147
  </table>
@@ -154,7 +142,7 @@
154
 
155
  </div>
156
 
157
-
158
 
159
 
160
  </body>
 
33
  <div class="avatar">
34
  <img src="/images/default-avatar.png" alt="">
35
  </div>
 
36
  <div class="profile-info">
37
+ <h3 th:text="${#authentication.principal.fullName}"></h3>
38
+ <span th:text="${#authentication.principal.department}"></span>
39
  </div>
40
 
41
  </div>
 
44
  <div class="stats-grid">
45
 
46
  <div class="stats-card">
47
+ <span class="stats-value" th:text="${attendanceHistory.totalElements}">22</span>
48
  <span class="stats-label">Ngày làm việc</span>
49
  </div>
50
 
 
95
 
96
  <div class="table-header">
97
  <h3>Lịch sử điểm danh</h3>
98
+ <span th:text="${attendanceHistory.totalElements} + ' bản ghi'">22 bản ghi</span>
99
  </div>
100
 
101
  <div class="table-scroll">
 
114
  </thead>
115
 
116
  <tbody>
117
+ <tr th:each="record : ${attendanceHistory.content}">
118
+ <td th:text="${#temporals.format(record.attendanceDate(), 'dd/MM/yyyy')}">23/06/2026</td>
119
+ <td th:text="${record.checkInTime() != null ? #temporals.format(record.checkInTime(), 'HH:mm') : '--'}">08:01</td>
120
+ <td th:text="${record.checkOutTime() != null ? #temporals.format(record.checkOutTime(), 'HH:mm') : '--'}">17:35</td>
 
121
  <td>
122
+ <span th:if="${record.late()}" class="badge late">Đi muộn</span>
123
+ <span th:if="${!record.late() and !record.earlyLeave() and record.checkInTime() != null}" class="badge success">Đúng giờ</span>
124
+ <span th:if="${record.earlyLeave()}" class="badge warning">Về sớm</span>
125
+ <span th:if="${record.checkInTime() == null}" class="badge absent">Vắng mặt</span>
126
  </td>
127
+ <td th:text="${record.workingMinutes() > 0 ? #strings.replace(#strings.replace(#numbers.formatDecimal(record.workingMinutes() / 60, 0, 0), ',', '') + 'h ' + record.workingMinutes() % 60 + 'p', ' 0p', '') : '--'}">8h 34p</td>
128
+ <td th:text="${record.note() != null ? record.note() : '-'}">-</td>
129
  </tr>
130
+ <tr th:if="${attendanceHistory.content.isEmpty()}">
131
+ <td colspan="6" class="text-center">Chưa có dữ liệu điểm danh.</td>
 
 
 
 
 
 
 
 
 
 
132
  </tr>
 
133
  </tbody>
134
 
135
  </table>
 
142
 
143
  </div>
144
 
145
+
146
 
147
 
148
  </body>
src/main/resources/templates/cms/auth/verify-otp.html CHANGED
@@ -44,10 +44,10 @@
44
  <!-- error -->
45
  <div class="error-message"></div>
46
 
47
- <form method="post">
48
 
49
  <!-- hidden email -->
50
- <input type="hidden" name="email" />
51
 
52
  <!-- OTP -->
53
  <div class="auth-group">
 
44
  <!-- error -->
45
  <div class="error-message"></div>
46
 
47
+ <form th:action="@{/auth/verify-otp}" method="post">
48
 
49
  <!-- hidden email -->
50
+ <input type="hidden" name="email" th:value="${email}" />
51
 
52
  <!-- OTP -->
53
  <div class="auth-group">
src/main/resources/templates/cms/password/change-password.html CHANGED
@@ -31,7 +31,7 @@
31
  <div class="dashboard-card" style="max-width: 500px; margin: auto;">
32
 
33
  <div class="card-header">
34
- <h2>Thay đổi mật khẩu</h2>
35
  </div>
36
 
37
  <div class="card-body">
@@ -43,28 +43,24 @@
43
  <div th:if="${errorMessage}" class="error-message" th:text="${errorMessage}">
44
  </div>
45
 
46
- <!-- FORM -->
47
- <form th:action="@{/auth/change-password}" method="post">
48
 
49
- <!-- Mật khẩu -->
50
- <div class="auth-group">
51
- <label for="oldPassword">Mật khẩu hiện tại</label>
52
- <input id="oldPassword" name="oldPassword" type="password"
53
- placeholder="Nhập mật khẩu hiện tại" />
54
- </div>
55
 
56
  <!-- Mật khẩu mới -->
57
  <div class="auth-group">
58
- <label for="newPassword">Mật khẩu mới</label>
59
- <input id="newPassword" name="newPassword" type="password"
60
- placeholder="Nhập mật khẩu mới" />
61
  </div>
62
 
63
  <!-- Xác nhận -->
64
  <div class="auth-group">
65
  <label for="confirmPassword">Xác nhận mật khẩu</label>
66
  <input id="confirmPassword" name="confirmPassword" type="password"
67
- placeholder="Nhập lại mật khẩu mới" />
68
  </div>
69
 
70
  <button class="auth-btn" type="submit">
@@ -73,11 +69,21 @@
73
 
74
  </form>
75
 
 
 
 
 
 
 
 
 
 
 
76
  <hr style="margin: 1rem 0; opacity: 0.2" />
77
 
78
  <div class="other-option">
79
- <a th:href="@{/dashboard}" class="register-link">
80
- ← Quay lại dashboard
81
  </a>
82
  </div>
83
 
 
31
  <div class="dashboard-card" style="max-width: 500px; margin: auto;">
32
 
33
  <div class="card-header">
34
+ <h2>Đặt lại mật khẩu</h2>
35
  </div>
36
 
37
  <div class="card-body">
 
43
  <div th:if="${errorMessage}" class="error-message" th:text="${errorMessage}">
44
  </div>
45
 
46
+ <!-- FORM cho forgot password flow -->
47
+ <form th:action="@{/user/update-password}" method="post" th:if="${otpVerified}">
48
 
49
+ <!-- hidden email -->
50
+ <input type="hidden" name="destination" th:value="${email}" />
 
 
 
 
51
 
52
  <!-- Mật khẩu mới -->
53
  <div class="auth-group">
54
+ <label for="password">Mật khẩu mới</label>
55
+ <input id="password" name="password" type="password"
56
+ placeholder="Nhập mật khẩu mới" required />
57
  </div>
58
 
59
  <!-- Xác nhận -->
60
  <div class="auth-group">
61
  <label for="confirmPassword">Xác nhận mật khẩu</label>
62
  <input id="confirmPassword" name="confirmPassword" type="password"
63
+ placeholder="Nhập lại mật khẩu mới" required />
64
  </div>
65
 
66
  <button class="auth-btn" type="submit">
 
69
 
70
  </form>
71
 
72
+ <!-- Thông báo nếu chưa verify OTP -->
73
+ <div th:if="${!otpVerified}">
74
+ <div class="alert alert-warning">
75
+ Vui lòng xác minh OTP trước khi đổi mật khẩu.
76
+ </div>
77
+ <a th:href="@{/auth/verify-otp}" class="auth-btn" style="display: inline-block; text-decoration: none; text-align: center;">
78
+ Xác minh OTP
79
+ </a>
80
+ </div>
81
+
82
  <hr style="margin: 1rem 0; opacity: 0.2" />
83
 
84
  <div class="other-option">
85
+ <a th:href="@{/auth/login}" class="register-link">
86
+ ← Quay lại đăng nhập
87
  </a>
88
  </div>
89
 
src/main/resources/templates/cms/sidebar/sidebar-admin.html CHANGED
@@ -53,18 +53,18 @@
53
  <i class="bi bi-person-circle"></i>
54
  </div>
55
 
56
- <!-- <div>
57
  <h3 th:text="${#authentication.principal.fullName}"></h3>
58
  <div class="header-description" th:text="${#authentication.principal.role}">
59
  Vai trò
60
  </div>
61
- </div> -->
62
- <div>
63
  <h3>Nguyen van an</h3>
64
  <div class="header-description">
65
  admin
66
  </div>
67
- </div>
68
  </div>
69
 
70
  <form th:action="@{/auth/logout}" method="post" class="logout-form">
 
53
  <i class="bi bi-person-circle"></i>
54
  </div>
55
 
56
+ <div>
57
  <h3 th:text="${#authentication.principal.fullName}"></h3>
58
  <div class="header-description" th:text="${#authentication.principal.role}">
59
  Vai trò
60
  </div>
61
+ </div>
62
+ <!-- <div>
63
  <h3>Nguyen van an</h3>
64
  <div class="header-description">
65
  admin
66
  </div>
67
+ </div> -->
68
  </div>
69
 
70
  <form th:action="@{/auth/logout}" method="post" class="logout-form">
src/main/resources/templates/cms/sidebar/sidebar-employee.html CHANGED
@@ -56,7 +56,7 @@
56
  </li>
57
 
58
  <li class="sidebar-option">
59
- <a href="/userInformation" class="sidebar-link">
60
  <span>🪪</span>
61
  Thông tin cá nhân
62
  </a>
@@ -78,18 +78,18 @@
78
  <i class="bi bi-person-circle"></i>
79
  </div>
80
 
81
- <!-- <div>
82
  <h3 th:text="${#authentication.principal.fullName}"></h3>
83
  <div class="header-description" th:text="${#authentication.principal.role}">
84
  Vai trò
85
  </div>
86
- </div> -->
87
- <div>
88
  <h3>Nguyen van an</h3>
89
  <div class="header-description">
90
  Nhân viên
91
  </div>
92
- </div>
93
  </div>
94
 
95
  <form th:action="@{/auth/logout}" method="post" class="logout-form">
 
56
  </li>
57
 
58
  <li class="sidebar-option">
59
+ <a href="/user/profile" class="sidebar-link">
60
  <span>🪪</span>
61
  Thông tin cá nhân
62
  </a>
 
78
  <i class="bi bi-person-circle"></i>
79
  </div>
80
 
81
+ <div>
82
  <h3 th:text="${#authentication.principal.fullName}"></h3>
83
  <div class="header-description" th:text="${#authentication.principal.role}">
84
  Vai trò
85
  </div>
86
+ </div>
87
+ <!-- <div>
88
  <h3>Nguyen van an</h3>
89
  <div class="header-description">
90
  Nhân viên
91
  </div>
92
+ </div> -->
93
  </div>
94
 
95
  <form th:action="@{/auth/logout}" method="post" class="logout-form">
src/main/resources/templates/cms/sidebar/sidebar-manage.html CHANGED
@@ -60,7 +60,7 @@
60
  </a>
61
  </li>
62
  <li class="sidebar-option">
63
- <a href="/userInformation" class="sidebar-link">
64
  <span>🪪</span>
65
  Thông tin cá nhân
66
  </a>
@@ -80,18 +80,18 @@
80
  <i class="bi bi-person-circle"></i>
81
  </div>
82
 
83
- <!-- <div>
84
  <h3 th:text="${#authentication.principal.fullName}"></h3>
85
  <div class="header-description" th:text="${#authentication.principal.role}">
86
  Vai trò
87
  </div>
88
- </div> -->
89
- <div>
90
  <h3>Nguyen van an</h3>
91
  <div class="header-description">
92
  Manager
93
  </div>
94
- </div>
95
  </div>
96
 
97
  <form th:action="@{/auth/logout}" method="post" class="logout-form">
 
60
  </a>
61
  </li>
62
  <li class="sidebar-option">
63
+ <a href="/user/profile" class="sidebar-link">
64
  <span>🪪</span>
65
  Thông tin cá nhân
66
  </a>
 
80
  <i class="bi bi-person-circle"></i>
81
  </div>
82
 
83
+ <div>
84
  <h3 th:text="${#authentication.principal.fullName}"></h3>
85
  <div class="header-description" th:text="${#authentication.principal.role}">
86
  Vai trò
87
  </div>
88
+ </div>
89
+ <!-- <div>
90
  <h3>Nguyen van an</h3>
91
  <div class="header-description">
92
  Manager
93
  </div>
94
+ </div> -->
95
  </div>
96
 
97
  <form th:action="@{/auth/logout}" method="post" class="logout-form">
src/main/resources/templates/cms/user/user-information.html CHANGED
@@ -43,22 +43,22 @@
43
 
44
  <div class="info-item">
45
  <label>Họ và tên</label>
46
- <p>Nguyễn Văn A</p>
47
  </div>
48
 
49
  <div class="info-item">
50
  <label>Email</label>
51
- <p>abc@gmail.com</p>
52
  </div>
53
 
54
  <div class="info-item">
55
  <label>Số điện thoại</label>
56
- <p>0987654321</p>
57
  </div>
58
 
59
  <div class="info-item">
60
  <label>Phòng ban</label>
61
- <p>Phòng Lab 213</p>
62
  </div>
63
 
64
  </div>
@@ -76,7 +76,7 @@
76
 
77
  <div class="card-body">
78
 
79
- <form method="post" th:action="@{/profile/change-password}">
80
 
81
  <div class="form-group">
82
  <label>Mật khẩu hiện tại</label>
 
43
 
44
  <div class="info-item">
45
  <label>Họ và tên</label>
46
+ <p th:text="${#authentication.principal.fullName}"></p>
47
  </div>
48
 
49
  <div class="info-item">
50
  <label>Email</label>
51
+ <p th:text="${#authentication.principal.email}"></p>
52
  </div>
53
 
54
  <div class="info-item">
55
  <label>Số điện thoại</label>
56
+ <p th:text="${#authentication.principal.phone}">Không có</p>
57
  </div>
58
 
59
  <div class="info-item">
60
  <label>Phòng ban</label>
61
+ <p th:text="${#authentication.principal.department}">Không </p>
62
  </div>
63
 
64
  </div>
 
76
 
77
  <div class="card-body">
78
 
79
+ <form method="post" th:action="@{/user/change-password}">
80
 
81
  <div class="form-group">
82
  <label>Mật khẩu hiện tại</label>