Spaces:
Configuration error
Configuration error
Add module otp, add forgot password and change password
Browse files- mvnw +0 -0
- pom.xml +4 -0
- src/main/java/com/attendenceSystem/annotation/RegisterPasswordMatch.java +23 -0
- src/main/java/com/attendenceSystem/annotation/{PasswordMatch.java → UpdatePasswordMatch.java} +4 -5
- src/main/java/com/attendenceSystem/constant/Routes.java +3 -0
- src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java +5 -0
- src/main/java/com/attendenceSystem/module/otp/README.md +85 -0
- src/main/java/com/attendenceSystem/module/otp/api/OtpApiController.java +36 -0
- src/main/java/com/attendenceSystem/module/otp/dto/request/SendOtpRequest.java +22 -0
- src/main/java/com/attendenceSystem/module/otp/dto/request/VerifyOtpRequest.java +25 -0
- src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java +15 -0
- src/main/java/com/attendenceSystem/module/otp/entity/Otp.java +47 -0
- src/main/java/com/attendenceSystem/module/otp/entity/converter/OtpPurposeConverter.java +20 -0
- src/main/java/com/attendenceSystem/module/otp/entity/enums/OtpPurpose.java +32 -0
- src/main/java/com/attendenceSystem/module/otp/exception/OtpExpiredException.java +7 -0
- src/main/java/com/attendenceSystem/module/otp/exception/OtpInvalidException.java +7 -0
- src/main/java/com/attendenceSystem/module/otp/exception/OtpNotFoundException.java +7 -0
- src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java +28 -0
- src/main/java/com/attendenceSystem/module/otp/mapper/request/VerifyOtpRequestMapper.java +9 -0
- src/main/java/com/attendenceSystem/module/otp/mapper/response/OtpResponseMapper.java +22 -0
- src/main/java/com/attendenceSystem/module/otp/repository/OtpRepository.java +30 -0
- src/main/java/com/attendenceSystem/module/otp/service/OptService.java +12 -0
- src/main/java/com/attendenceSystem/module/otp/service/OtpSender.java +6 -0
- src/main/java/com/attendenceSystem/module/otp/service/impl/EmailOtpSenderImpl.java +35 -0
- src/main/java/com/attendenceSystem/module/otp/service/impl/OtpServiceImpl.java +76 -0
- src/main/java/com/attendenceSystem/module/user/controller/AuthController.java +47 -0
- src/main/java/com/attendenceSystem/module/user/controller/UserController.java +42 -1
- src/main/java/com/attendenceSystem/module/user/dto/request/ChangePasswordRequest.java +14 -0
- src/main/java/com/attendenceSystem/module/user/dto/request/RegisterRequest.java +2 -2
- src/main/java/com/attendenceSystem/module/user/dto/request/UpdatePasswordRequest.java +13 -0
- src/main/java/com/attendenceSystem/module/user/dto/request/UpdatePasswordWithOtpRequest.java +13 -0
- src/main/java/com/attendenceSystem/module/user/service/AuthService.java +2 -1
- src/main/java/com/attendenceSystem/module/user/service/UserService.java +9 -0
- src/main/java/com/attendenceSystem/module/user/service/impl/AuthServiceImpl.java +38 -1
- src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java +42 -3
- src/main/java/com/attendenceSystem/validator/{PasswordMatchValidator.java → RegisterPasswordMatchValidator.java} +3 -3
- src/main/java/com/attendenceSystem/validator/UpdatePasswordMatchValidator.java +20 -0
- src/main/resources/application.properties +7 -6
- src/main/resources/templates/cms/absent/absent-create.html +10 -3
- src/main/resources/templates/cms/absent/absent-detail.html +13 -0
- src/main/resources/templates/cms/absent/absent-list.html +10 -7
- src/main/resources/templates/cms/attendance/attendance-history.html +17 -29
- src/main/resources/templates/cms/auth/verify-otp.html +2 -2
- src/main/resources/templates/cms/password/change-password.html +21 -15
- src/main/resources/templates/cms/sidebar/sidebar-admin.html +4 -4
- src/main/resources/templates/cms/sidebar/sidebar-employee.html +5 -5
- src/main/resources/templates/cms/sidebar/sidebar-manage.html +5 -5
- 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.
|
| 9 |
|
| 10 |
import jakarta.validation.Constraint;
|
| 11 |
import jakarta.validation.Payload;
|
| 12 |
|
| 13 |
@Target(ElementType.TYPE)
|
| 14 |
@Retention(RetentionPolicy.RUNTIME)
|
| 15 |
-
@Constraint(validatedBy =
|
| 16 |
-
public @interface
|
| 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.
|
| 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.
|
| 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 |
-
@
|
| 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.
|
| 4 |
import com.attendenceSystem.module.user.dto.request.RegisterRequest;
|
| 5 |
|
| 6 |
import jakarta.validation.ConstraintValidator;
|
| 7 |
import jakarta.validation.ConstraintValidatorContext;
|
| 8 |
|
| 9 |
-
public class
|
| 10 |
-
implements ConstraintValidator<
|
| 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 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|
| 42 |
# JPA configuration
|
| 43 |
spring.jpa.hibernate.ddl-auto=update
|
|
@@ -50,8 +52,7 @@ attendance.end-work=17:00
|
|
| 50 |
|
| 51 |
|
| 52 |
|
| 53 |
-
|
| 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"
|
|
|
|
| 48 |
</div>
|
| 49 |
|
| 50 |
<div class="form-group">
|
| 51 |
<label>Đến ngày</label>
|
| 52 |
-
<input type="date"
|
|
|
|
| 53 |
</div>
|
| 54 |
|
| 55 |
</div>
|
|
@@ -57,8 +63,9 @@
|
|
| 57 |
<!-- Lý do -->
|
| 58 |
<div class="form-group">
|
| 59 |
<label>Lý do</label>
|
| 60 |
-
<textarea
|
| 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>
|
| 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
|
| 39 |
-
<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 |
-
|
| 120 |
-
<td>
|
| 121 |
-
<td>
|
| 122 |
-
<td>17:35</td>
|
| 123 |
<td>
|
| 124 |
-
<span class="badge
|
| 125 |
-
|
| 126 |
-
</span>
|
|
|
|
| 127 |
</td>
|
| 128 |
-
<td>8h 34p</td>
|
| 129 |
-
<td>-</td>
|
| 130 |
</tr>
|
| 131 |
-
|
| 132 |
-
|
| 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>
|
| 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="@{/
|
| 48 |
|
| 49 |
-
<!--
|
| 50 |
-
<
|
| 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="
|
| 59 |
-
<input id="
|
| 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="@{/
|
| 80 |
-
← Quay lại
|
| 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 |
-
<
|
| 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="/
|
| 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 |
-
<
|
| 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="/
|
| 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 |
-
<
|
| 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
|
| 47 |
</div>
|
| 48 |
|
| 49 |
<div class="info-item">
|
| 50 |
<label>Email</label>
|
| 51 |
-
<p
|
| 52 |
</div>
|
| 53 |
|
| 54 |
<div class="info-item">
|
| 55 |
<label>Số điện thoại</label>
|
| 56 |
-
<p>
|
| 57 |
</div>
|
| 58 |
|
| 59 |
<div class="info-item">
|
| 60 |
<label>Phòng ban</label>
|
| 61 |
-
<p>
|
| 62 |
</div>
|
| 63 |
|
| 64 |
</div>
|
|
@@ -76,7 +76,7 @@
|
|
| 76 |
|
| 77 |
<div class="card-body">
|
| 78 |
|
| 79 |
-
<form method="post" th:action="@{/
|
| 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 có </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>
|