feat : thêm swagger

This commit is contained in:
nguyennt1 2025-12-02 22:24:10 +07:00
parent 744a2fa40b
commit abd31bf956
32 changed files with 1564 additions and 47 deletions

View File

@ -0,0 +1,74 @@
package com.vega.hrm.controller;
import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.request.function.CreateFunctionRequest;
import com.vega.hrm.request.function.UpdateFunctionRequest;
import com.vega.hrm.response.FunctionDto;
import com.vega.hrm.service.FunctionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api/auth/function")
@RequiredArgsConstructor
@Tag(name = "Auth - Function/Menu", description = "Quản lý menu và chức năng hệ thống")
public class FunctionController {
private final FunctionService functionService;
@PostMapping("/create")
@Operation(summary = "Tạo menu mới", description = "Tạo menu/chức năng mới trong hệ thống.")
public ResponseEntity<BaseResponse<FunctionDto>> createFunction(
@Valid @RequestBody CreateFunctionRequest request,
@Parameter(description = "Người tạo (từ token)") @RequestParam(defaultValue = "admin") String currentUser
) {
return ResponseEntity.ok(functionService.createFunction(request, currentUser));
}
@PutMapping("/update")
@Operation(summary = "Cập nhật menu", description = "Cập nhật thông tin menu/chức năng.")
public ResponseEntity<BaseResponse<FunctionDto>> updateFunction(@Valid @RequestBody UpdateFunctionRequest request) {
return ResponseEntity.ok(functionService.updateFunction(request));
}
@DeleteMapping("/delete")
@Operation(summary = "Xóa menu", description = "Xóa menu/chức năng (không thể xóa nếu có menu con).")
public ResponseEntity<BaseResponse<Boolean>> deleteFunction(
@Parameter(description = "ID của menu") @RequestParam String functionId
) {
return ResponseEntity.ok(functionService.deleteFunction(functionId));
}
@GetMapping("/list")
@Operation(summary = "Lấy danh sách menu", description = "Lấy tất cả menu dạng flat list.")
public ResponseEntity<BaseResponse<List<FunctionDto>>> getAllFunctions() {
return ResponseEntity.ok(functionService.getAllFunctions());
}
@GetMapping("/tree")
@Operation(summary = "Lấy cây menu", description = "Lấy menu dạng cây phân cấp (parent-children).")
public ResponseEntity<BaseResponse<List<FunctionDto>>> getMenuTree() {
return ResponseEntity.ok(functionService.getMenuTree());
}
@GetMapping("/detail")
@Operation(summary = "Lấy chi tiết menu", description = "Lấy thông tin chi tiết của menu/chức năng.")
public ResponseEntity<BaseResponse<FunctionDto>> getFunctionDetail(
@Parameter(description = "ID của menu") @RequestParam String functionId
) {
return ResponseEntity.ok(functionService.getFunctionDetail(functionId));
}
}

View File

@ -0,0 +1,78 @@
package com.vega.hrm.controller;
import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.request.role.AssignPermissionsRequest;
import com.vega.hrm.request.role.CreateRoleRequest;
import com.vega.hrm.request.role.UpdateRoleRequest;
import com.vega.hrm.response.RoleDto;
import com.vega.hrm.service.RoleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api/auth/role")
@RequiredArgsConstructor
@Tag(name = "Auth - Role", description = "Quản lý vai trò và phân quyền")
public class RoleController {
private final RoleService roleService;
@PostMapping("/create")
@Operation(summary = "Tạo role mới", description = "Tạo vai trò mới và gán quyền menu.")
public ResponseEntity<BaseResponse<RoleDto>> createRole(
@Valid @RequestBody CreateRoleRequest request,
@Parameter(description = "Người tạo (từ token)") @RequestParam(defaultValue = "admin") String currentUser
) {
return ResponseEntity.ok(roleService.createRole(request, currentUser));
}
@PutMapping("/update")
@Operation(summary = "Cập nhật role", description = "Cập nhật thông tin vai trò.")
public ResponseEntity<BaseResponse<RoleDto>> updateRole(@Valid @RequestBody UpdateRoleRequest request) {
return ResponseEntity.ok(roleService.updateRole(request));
}
@DeleteMapping("/delete")
@Operation(summary = "Xóa role", description = "Xóa vai trò (soft delete).")
public ResponseEntity<BaseResponse<Boolean>> deleteRole(
@Parameter(description = "ID của role") @RequestParam String roleId
) {
return ResponseEntity.ok(roleService.deleteRole(roleId));
}
@GetMapping("/list")
@Operation(summary = "Lấy danh sách role", description = "Lấy tất cả vai trò trong hệ thống.")
public ResponseEntity<BaseResponse<List<RoleDto>>> getAllRoles() {
return ResponseEntity.ok(roleService.getAllRoles());
}
@GetMapping("/detail")
@Operation(summary = "Lấy chi tiết role", description = "Lấy thông tin chi tiết vai trò và quyền.")
public ResponseEntity<BaseResponse<RoleDto>> getRoleDetail(
@Parameter(description = "ID của role") @RequestParam String roleId
) {
return ResponseEntity.ok(roleService.getRoleDetail(roleId));
}
@PostMapping("/assign-permissions")
@Operation(summary = "Gán quyền cho role", description = "Gán danh sách menu/function cho vai trò.")
public ResponseEntity<BaseResponse<Boolean>> assignPermissions(
@Valid @RequestBody AssignPermissionsRequest request,
@Parameter(description = "Người thực hiện (từ token)") @RequestParam(defaultValue = "admin") String currentUser
) {
return ResponseEntity.ok(roleService.assignPermissions(request, currentUser));
}
}

View File

@ -1,22 +1,33 @@
package com.vega.hrm.controller; package com.vega.hrm.controller;
import com.vega.hrm.core.models.responses.BaseResponse; import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.request.user.ChangePasswordRequest;
import com.vega.hrm.request.user.CreateUserRequest; import com.vega.hrm.request.user.CreateUserRequest;
import com.vega.hrm.request.user.ForgotPasswordRequest;
import com.vega.hrm.request.user.LoginRequest; import com.vega.hrm.request.user.LoginRequest;
import com.vega.hrm.request.user.RegisterRequest;
import com.vega.hrm.request.user.UpdateProfileRequest;
import com.vega.hrm.response.UserInfoResponse;
import com.vega.hrm.service.UserService; import com.vega.hrm.service.UserService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController @RestController
@RequestMapping("api/auth/user") @RequestMapping("api/auth/user")
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "Auth - User", description = "Các API liên quan đến xác thực và quản lý người dùng nội bộ") @Tag(name = "Auth - User", description = "Các API liên quan đến xác thực và quản lý người dùng")
public class UserController { public class UserController {
private final UserService userService; private final UserService userService;
@ -27,8 +38,52 @@ public class UserController {
} }
@PostMapping("/insert") @PostMapping("/insert")
@Operation(summary = "Tạo người dùng nội bộ", description = "Tạo tài khoản quản trị viên/nhân sự mới.") @Operation(summary = "Tạo người dùng nội bộ (Admin)", description = "Tạo tài khoản quản trị viên/nhân sự mới bởi admin và gán role.")
public ResponseEntity<BaseResponse<Boolean>> insert(@RequestBody CreateUserRequest request) { public ResponseEntity<BaseResponse<Boolean>> insert(@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.ok(userService.insert(request)); return ResponseEntity.ok(userService.insert(request));
} }
@PostMapping("/register")
@Operation(summary = "Đăng ký tài khoản", description = "Người dùng tự đăng ký tài khoản mới.")
public ResponseEntity<BaseResponse<Boolean>> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.ok(userService.register(request));
}
@GetMapping("/info")
@Operation(summary = "Lấy thông tin tài khoản", description = "Lấy thông tin chi tiết của người dùng theo User ID.")
public ResponseEntity<BaseResponse<UserInfoResponse>> getUserInfo(
@Parameter(description = "ID của người dùng")
@RequestParam String userId
) {
return ResponseEntity.ok(userService.getUserInfo(userId));
}
@PutMapping("/profile")
@Operation(summary = "Cập nhật thông tin cá nhân", description = "Cập nhật họ tên, email của người dùng.")
public ResponseEntity<BaseResponse<Boolean>> updateProfile(@Valid @RequestBody UpdateProfileRequest request) {
return ResponseEntity.ok(userService.updateProfile(request));
}
@PutMapping("/change-password")
@Operation(summary = "Đổi mật khẩu", description = "Người dùng đổi mật khẩu khi biết mật khẩu cũ.")
public ResponseEntity<BaseResponse<Boolean>> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
return ResponseEntity.ok(userService.changePassword(request));
}
@PostMapping("/forgot-password")
@Operation(summary = "Quên mật khẩu", description = "Tạo mật khẩu mới và gửi qua email khi người dùng quên mật khẩu.")
public ResponseEntity<BaseResponse<String>> forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) {
return ResponseEntity.ok(userService.forgotPassword(request));
}
@PostMapping("/upload-avatar")
@Operation(summary = "Upload ảnh đại diện", description = "Upload ảnh đại diện cho người dùng (jpg, jpeg, png, gif).")
public ResponseEntity<BaseResponse<String>> uploadAvatar(
@Parameter(description = "ID của người dùng")
@RequestParam String userId,
@Parameter(description = "File ảnh đại diện")
@RequestParam("file") MultipartFile file
) {
return ResponseEntity.ok(userService.uploadAvatar(userId, file));
}
} }

View File

@ -0,0 +1,31 @@
package com.vega.hrm.request.function;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CreateFunctionRequest {
@NotBlank(message = "Tên menu không được để trống")
@Size(max = 200, message = "Tên menu không được vượt quá 200 ký tự")
private String functionName;
@NotNull(message = "Cấp độ menu không được để trống")
private Long functionLevel; // 1: menu cha, 2: menu con, 3: sub menu...
@Size(max = 100, message = "URL không được vượt quá 100 ký tự")
private String functionUrl; // Đường dẫn route
@NotNull(message = "Thứ tự hiển thị không được để trống")
private Long functionOrder; // Thứ tự sắp xếp
private String parentId; // ID menu cha (null nếu menu top level)
@NotBlank(message = "Trạng thái hiển thị không được để trống")
@Size(max = 20, message = "Trạng thái hiển thị không được vượt quá 20 ký tự")
private String functionDisplay; // "show" / "hide"
}

View File

@ -0,0 +1,32 @@
package com.vega.hrm.request.function;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UpdateFunctionRequest {
@NotBlank(message = "Function ID không được để trống")
private String functionId;
@NotBlank(message = "Tên menu không được để trống")
@Size(max = 200, message = "Tên menu không được vượt quá 200 ký tự")
private String functionName;
@NotNull(message = "Cấp độ menu không được để trống")
private Long functionLevel;
@Size(max = 100, message = "URL không được vượt quá 100 ký tự")
private String functionUrl;
@NotNull(message = "Thứ tự hiển thị không được để trống")
private Long functionOrder;
@NotBlank(message = "Trạng thái hiển thị không được để trống")
@Size(max = 20, message = "Trạng thái hiển thị không được vượt quá 20 ký tự")
private String functionDisplay;
}

View File

@ -0,0 +1,19 @@
package com.vega.hrm.request.role;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AssignPermissionsRequest {
@NotBlank(message = "Role ID không được để trống")
private String roleId;
@NotEmpty(message = "Danh sách quyền không được để trống")
private List<UUID> functionIds;
}

View File

@ -0,0 +1,22 @@
package com.vega.hrm.request.role;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CreateRoleRequest {
@NotBlank(message = "Tên role không được để trống")
@Size(max = 200, message = "Tên role không được vượt quá 200 ký tự")
private String roleName;
@Size(max = 1000, message = "Mô tả không được vượt quá 1000 ký tự")
private String description;
private List<UUID> functionIds; // Danh sách function/menu IDs được gán cho role
}

View File

@ -0,0 +1,21 @@
package com.vega.hrm.request.role;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UpdateRoleRequest {
@NotBlank(message = "Role ID không được để trống")
private String roleId;
@NotBlank(message = "Tên role không được để trống")
@Size(max = 200, message = "Tên role không được vượt quá 200 ký tự")
private String roleName;
@Size(max = 1000, message = "Mô tả không được vượt quá 1000 ký tự")
private String description;
}

View File

@ -0,0 +1,21 @@
package com.vega.hrm.request.user;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ChangePasswordRequest {
@NotBlank(message = "User ID không được để trống")
private String userId;
@NotBlank(message = "Mật khẩu cũ không được để trống")
private String oldPassword;
@NotBlank(message = "Mật khẩu mới không được để trống")
@Size(min = 6, message = "Mật khẩu mới phải có ít nhất 6 ký tự")
private String newPassword;
}

View File

@ -1,14 +1,30 @@
package com.vega.hrm.request.user; package com.vega.hrm.request.user;
import java.util.UUID; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class CreateUserRequest { public class CreateUserRequest {
@NotBlank(message = "Tên đăng nhập không được để trống")
@Size(max = 200, message = "Tên đăng nhập không được vượt quá 200 ký tự")
private String userName; private String userName;
@NotBlank(message = "Mật khẩu không được để trống")
@Size(min = 6, message = "Mật khẩu phải có ít nhất 6 ký tự")
private String password; private String password;
@NotBlank(message = "Họ tên không được để trống")
@Size(max = 200, message = "Họ tên không được vượt quá 200 ký tự")
private String fullName; private String fullName;
private String roleId;
@Email(message = "Email không đúng định dạng")
@Size(max = 255, message = "Email không được vượt quá 255 ký tự")
private String email;
@NotBlank(message = "Role ID không được để trống")
private String roleId; // UUID của role
} }

View File

@ -0,0 +1,15 @@
package com.vega.hrm.request.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ForgotPasswordRequest {
@NotBlank(message = "Email không được để trống")
@Email(message = "Email không đúng định dạng")
private String email;
}

View File

@ -0,0 +1,28 @@
package com.vega.hrm.request.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RegisterRequest {
@NotBlank(message = "Tên đăng nhập không được để trống")
@Size(min = 3, max = 200, message = "Tên đăng nhập phải từ 3-200 ký tự")
private String userName;
@NotBlank(message = "Mật khẩu không được để trống")
@Size(min = 6, message = "Mật khẩu phải có ít nhất 6 ký tự")
private String password;
@NotBlank(message = "Họ tên không được để trống")
@Size(max = 200, message = "Họ tên không được vượt quá 200 ký tự")
private String fullName;
@Email(message = "Email không đúng định dạng")
@Size(max = 255, message = "Email không được vượt quá 255 ký tự")
private String email;
}

View File

@ -0,0 +1,23 @@
package com.vega.hrm.request.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UpdateProfileRequest {
@NotBlank(message = "User ID không được để trống")
private String userId;
@NotBlank(message = "Họ tên không được để trống")
@Size(max = 200, message = "Họ tên không được vượt quá 200 ký tự")
private String fullName;
@Email(message = "Email không đúng định dạng")
@Size(max = 255, message = "Email không được vượt quá 255 ký tự")
private String email;
}

View File

@ -0,0 +1,27 @@
package com.vega.hrm.response;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionDto {
private UUID functionId;
private String functionName;
private Long functionLevel;
private String functionUrl;
private Long functionOrder;
private UUID parentId;
private String functionDisplay;
private String createdBy;
private Instant createTime;
private List<FunctionDto> children; // Menu con (nếu )
}

View File

@ -1,17 +1,55 @@
package com.vega.hrm.response; package com.vega.hrm.response;
import com.vega.hrm.core.entities.BoUser;
import java.util.List; import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.NoArgsConstructor;
import lombok.Setter;
@Data @Data
@Builder @Builder
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class LoginResponse { public class LoginResponse {
private BoUser user; private UserLoginInfo user;
private String token; private String token;
private RoleInfo role;
private List<FunctionInfo> functions;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class UserLoginInfo {
private UUID userId;
private String userName;
private String fullName;
private String email;
private String avatarUrl;
private String status;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RoleInfo {
private UUID roleId;
private String roleName;
private String description;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FunctionInfo {
private UUID functionId;
private String functionName;
private String functionUrl;
private Long functionLevel;
private Long functionOrder;
private UUID parentId;
}
} }

View File

@ -0,0 +1,24 @@
package com.vega.hrm.response;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleDto {
private UUID roleId;
private String roleName;
private String description;
private String status;
private String createdBy;
private Instant createTime;
private List<FunctionDto> functions; // Danh sách menu/quyền của role
}

View File

@ -0,0 +1,25 @@
package com.vega.hrm.response;
import java.time.Instant;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfoResponse {
private UUID userId;
private String userName;
private String fullName;
private String email;
private String avatarUrl;
private String status;
private UUID roleId;
private Instant lastLoginTime;
private Instant createdDate;
}

View File

@ -0,0 +1,199 @@
package com.vega.hrm.service;
import com.vega.hrm.core.entities.BoFunction;
import com.vega.hrm.core.helpers.LogHelper;
import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.core.repositories.CoreFunctionRepository;
import com.vega.hrm.request.function.CreateFunctionRequest;
import com.vega.hrm.request.function.UpdateFunctionRequest;
import com.vega.hrm.response.FunctionDto;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class FunctionService {
private final CoreFunctionRepository functionRepository;
/**
* Tạo menu/function mới
*/
@Transactional
public BaseResponse<FunctionDto> createFunction(CreateFunctionRequest request, String currentUser) {
var function = new BoFunction();
function.setId(UUID.randomUUID());
function.setFunctionName(request.getFunctionName());
function.setFunctionLevel(request.getFunctionLevel());
function.setFunctionUrl(request.getFunctionUrl());
function.setFunctionOrder(request.getFunctionOrder());
function.setFunctionDisplay(request.getFunctionDisplay());
function.setCreatedBy(currentUser);
function.setCreateTime(Instant.now());
// Xử parent
if (request.getParentId() != null && !request.getParentId().isEmpty()) {
try {
function.setParentId(UUID.fromString(request.getParentId()));
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Parent ID không hợp lệ");
}
} else {
// Menu top level - dùng UUID zero
function.setParentId(UUID.fromString("00000000-0000-0000-0000-000000000000"));
}
functionRepository.save(function);
LogHelper.info("Tạo menu thành công: " + function.getFunctionName());
return BaseResponse.success("Tạo menu thành công", mapToFunctionDto(function));
}
/**
* Cập nhật menu/function
*/
@Transactional
public BaseResponse<FunctionDto> updateFunction(UpdateFunctionRequest request) {
try {
UUID functionId = UUID.fromString(request.getFunctionId());
var function = functionRepository.findById(functionId).orElse(null);
if (function == null) {
return BaseResponse.notFound("Không tìm thấy menu");
}
function.setFunctionName(request.getFunctionName());
function.setFunctionLevel(request.getFunctionLevel());
function.setFunctionUrl(request.getFunctionUrl());
function.setFunctionOrder(request.getFunctionOrder());
function.setFunctionDisplay(request.getFunctionDisplay());
functionRepository.save(function);
LogHelper.info("Cập nhật menu thành công: " + function.getFunctionName());
return BaseResponse.success("Cập nhật menu thành công", mapToFunctionDto(function));
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Function ID không hợp lệ");
}
}
/**
* Xóa menu/function
*/
@Transactional
public BaseResponse<Boolean> deleteFunction(String functionId) {
try {
UUID id = UUID.fromString(functionId);
var function = functionRepository.findById(id).orElse(null);
if (function == null) {
return BaseResponse.notFound("Không tìm thấy menu");
}
// Kiểm tra menu con không
List<BoFunction> children = functionRepository.findByParentId(id);
if (!children.isEmpty()) {
return BaseResponse.invalid("Không thể xóa menu có menu con. Vui lòng xóa menu con trước.");
}
functionRepository.delete(function);
LogHelper.info("Xóa menu: " + function.getFunctionName());
return BaseResponse.success("Xóa menu thành công", true);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Function ID không hợp lệ");
}
}
/**
* Lấy tất cả menu dạng flat list
*/
public BaseResponse<List<FunctionDto>> getAllFunctions() {
List<BoFunction> functions = functionRepository.findAllOrderByLevelAndOrder();
List<FunctionDto> functionDtos = functions.stream()
.map(this::mapToFunctionDto)
.collect(Collectors.toList());
return BaseResponse.success("Lấy danh sách menu thành công", functionDtos);
}
/**
* Lấy menu dạng tree (phân cấp)
*/
public BaseResponse<List<FunctionDto>> getMenuTree() {
List<BoFunction> allFunctions = functionRepository.findAllOrderByLevelAndOrder();
// Group theo parent
Map<UUID, List<BoFunction>> functionsByParent = new HashMap<>();
for (BoFunction function : allFunctions) {
functionsByParent
.computeIfAbsent(function.getParentId(), k -> new ArrayList<>())
.add(function);
}
// Build tree từ root (parentId = 00000000-0000-0000-0000-000000000000)
UUID rootId = UUID.fromString("00000000-0000-0000-0000-000000000000");
List<FunctionDto> tree = buildTree(rootId, functionsByParent);
return BaseResponse.success("Lấy cây menu thành công", tree);
}
/**
* Lấy chi tiết menu
*/
public BaseResponse<FunctionDto> getFunctionDetail(String functionId) {
try {
UUID id = UUID.fromString(functionId);
var function = functionRepository.findById(id).orElse(null);
if (function == null) {
return BaseResponse.notFound("Không tìm thấy menu");
}
return BaseResponse.success("Lấy thông tin menu thành công", mapToFunctionDto(function));
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Function ID không hợp lệ");
}
}
/**
* Helper: Build tree từ flat list
*/
private List<FunctionDto> buildTree(UUID parentId, Map<UUID, List<BoFunction>> functionsByParent) {
List<BoFunction> children = functionsByParent.getOrDefault(parentId, new ArrayList<>());
return children.stream()
.map(function -> {
FunctionDto dto = mapToFunctionDto(function);
// Đệ quy build children
dto.setChildren(buildTree(function.getId(), functionsByParent));
return dto;
})
.collect(Collectors.toList());
}
/**
* Map entity sang DTO
*/
private FunctionDto mapToFunctionDto(BoFunction function) {
return FunctionDto.builder()
.functionId(function.getId())
.functionName(function.getFunctionName())
.functionLevel(function.getFunctionLevel())
.functionUrl(function.getFunctionUrl())
.functionOrder(function.getFunctionOrder())
.parentId(function.getParentId())
.functionDisplay(function.getFunctionDisplay())
.createdBy(function.getCreatedBy())
.createTime(function.getCreateTime())
.build();
}
}

View File

@ -0,0 +1,235 @@
package com.vega.hrm.service;
import com.vega.hrm.core.entities.BoFunction;
import com.vega.hrm.core.entities.BoRole;
import com.vega.hrm.core.entities.BoRoleFunc;
import com.vega.hrm.core.helpers.LogHelper;
import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.core.repositories.BoRoleFuncRepository;
import com.vega.hrm.core.repositories.CoreFunctionRepository;
import com.vega.hrm.core.repositories.CoreRoleRepository;
import com.vega.hrm.request.role.AssignPermissionsRequest;
import com.vega.hrm.request.role.CreateRoleRequest;
import com.vega.hrm.request.role.UpdateRoleRequest;
import com.vega.hrm.response.FunctionDto;
import com.vega.hrm.response.RoleDto;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class RoleService {
private final CoreRoleRepository roleRepository;
private final CoreFunctionRepository functionRepository;
private final BoRoleFuncRepository roleFuncRepository;
/**
* Tạo role mới gán quyền
*/
@Transactional
public BaseResponse<RoleDto> createRole(CreateRoleRequest request, String currentUser) {
// Kiểm tra tên role đã tồn tại
var existingRole = roleRepository.findByRoleName(request.getRoleName());
if (existingRole != null) {
return BaseResponse.invalid("Tên role đã tồn tại");
}
// Tạo role mới
var role = new BoRole();
role.setId(UUID.randomUUID());
role.setRoleName(request.getRoleName());
role.setDescription(request.getDescription());
role.setStatus("1"); // Active
role.setCreatedBy(currentUser);
role.setCreateTime(Instant.now());
roleRepository.save(role);
// Gán quyền nếu
if (request.getFunctionIds() != null && !request.getFunctionIds().isEmpty()) {
assignFunctionsToRole(role.getId(), request.getFunctionIds(), currentUser);
}
LogHelper.info("Tạo role thành công: " + role.getRoleName());
return BaseResponse.success("Tạo role thành công", mapToRoleDto(role));
}
/**
* Cập nhật thông tin role
*/
@Transactional
public BaseResponse<RoleDto> updateRole(UpdateRoleRequest request) {
try {
UUID roleId = UUID.fromString(request.getRoleId());
var role = roleRepository.findById(roleId).orElse(null);
if (role == null) {
return BaseResponse.notFound("Không tìm thấy role");
}
// Kiểm tra tên role trùng (nếu thay đổi)
if (!role.getRoleName().equals(request.getRoleName())) {
var existingRole = roleRepository.findByRoleName(request.getRoleName());
if (existingRole != null) {
return BaseResponse.invalid("Tên role đã tồn tại");
}
}
role.setRoleName(request.getRoleName());
role.setDescription(request.getDescription());
roleRepository.save(role);
LogHelper.info("Cập nhật role thành công: " + role.getRoleName());
return BaseResponse.success("Cập nhật role thành công", mapToRoleDto(role));
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Role ID không hợp lệ");
}
}
/**
* Xóa role (soft delete - chuyển status)
*/
@Transactional
public BaseResponse<Boolean> deleteRole(String roleId) {
try {
UUID id = UUID.fromString(roleId);
var role = roleRepository.findById(id).orElse(null);
if (role == null) {
return BaseResponse.notFound("Không tìm thấy role");
}
role.setStatus("0"); // Inactive
roleRepository.save(role);
LogHelper.info("Xóa role: " + role.getRoleName());
return BaseResponse.success("Xóa role thành công", true);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Role ID không hợp lệ");
}
}
/**
* Lấy danh sách tất cả role
*/
public BaseResponse<List<RoleDto>> getAllRoles() {
List<BoRole> roles = roleRepository.findAll();
List<RoleDto> roleDtos = roles.stream()
.map(this::mapToRoleDto)
.collect(Collectors.toList());
return BaseResponse.success("Lấy danh sách role thành công", roleDtos);
}
/**
* Lấy chi tiết role quyền
*/
public BaseResponse<RoleDto> getRoleDetail(String roleId) {
try {
UUID id = UUID.fromString(roleId);
var role = roleRepository.findById(id).orElse(null);
if (role == null) {
return BaseResponse.notFound("Không tìm thấy role");
}
// Lấy danh sách function của role
List<UUID> functionIds = roleFuncRepository.findFunctionIdsByRoleId(id);
List<BoFunction> functions = functionRepository.findByIdIn(functionIds);
RoleDto roleDto = mapToRoleDto(role);
roleDto.setFunctions(functions.stream()
.map(this::mapToFunctionDto)
.collect(Collectors.toList()));
return BaseResponse.success("Lấy thông tin role thành công", roleDto);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Role ID không hợp lệ");
}
}
/**
* Gán quyền cho role
*/
@Transactional
public BaseResponse<Boolean> assignPermissions(AssignPermissionsRequest request, String currentUser) {
try {
UUID roleId = UUID.fromString(request.getRoleId());
var role = roleRepository.findById(roleId).orElse(null);
if (role == null) {
return BaseResponse.notFound("Không tìm thấy role");
}
// Xóa tất cả quyền
roleFuncRepository.deleteByRoleId(roleId);
// Gán quyền mới
assignFunctionsToRole(roleId, request.getFunctionIds(), currentUser);
LogHelper.info("Gán quyền cho role: " + role.getRoleName()
+ " (" + request.getFunctionIds().size() + " quyền)");
return BaseResponse.success("Gán quyền thành công", true);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Role ID không hợp lệ");
}
}
/**
* Helper: Gán functions cho role
*/
private void assignFunctionsToRole(UUID roleId, List<UUID> functionIds, String currentUser) {
var role = roleRepository.findById(roleId).orElse(null);
if (role == null) return;
for (UUID functionId : functionIds) {
var function = functionRepository.findById(functionId).orElse(null);
if (function == null) continue;
var roleFunc = new BoRoleFunc();
roleFunc.setId(UUID.randomUUID());
roleFunc.setRole(role);
roleFunc.setFunction(function);
roleFunc.setCreateTime(Instant.now());
roleFunc.setCreateUser(currentUser);
roleFuncRepository.save(roleFunc);
}
}
/**
* Map entity sang DTO
*/
private RoleDto mapToRoleDto(BoRole role) {
return RoleDto.builder()
.roleId(role.getId())
.roleName(role.getRoleName())
.description(role.getDescription())
.status(role.getStatus())
.createdBy(role.getCreatedBy())
.createTime(role.getCreateTime())
.build();
}
private FunctionDto mapToFunctionDto(BoFunction function) {
return FunctionDto.builder()
.functionId(function.getId())
.functionName(function.getFunctionName())
.functionLevel(function.getFunctionLevel())
.functionUrl(function.getFunctionUrl())
.functionOrder(function.getFunctionOrder())
.parentId(function.getParentId())
.functionDisplay(function.getFunctionDisplay())
.createdBy(function.getCreatedBy())
.createTime(function.getCreateTime())
.build();
}
}

View File

@ -2,17 +2,32 @@ package com.vega.hrm.service;
import com.vega.hrm.AuthHelper; import com.vega.hrm.AuthHelper;
import com.vega.hrm.core.constants.CommonConst; import com.vega.hrm.core.constants.CommonConst;
import com.vega.hrm.core.entities.BoFunction;
import com.vega.hrm.core.entities.BoRole;
import com.vega.hrm.core.entities.BoUser; import com.vega.hrm.core.entities.BoUser;
import com.vega.hrm.core.helpers.JwtHelper; import com.vega.hrm.core.helpers.JwtHelper;
import com.vega.hrm.core.helpers.LogHelper;
import com.vega.hrm.core.models.responses.BaseResponse; import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.core.repositories.BoRoleFuncRepository;
import com.vega.hrm.core.repositories.CoreFunctionRepository;
import com.vega.hrm.core.repositories.CoreRoleRepository;
import com.vega.hrm.core.repositories.CoreUserRepository; import com.vega.hrm.core.repositories.CoreUserRepository;
import com.vega.hrm.core.service.FileStorageService;
import com.vega.hrm.core.service.RedisService; import com.vega.hrm.core.service.RedisService;
import com.vega.hrm.request.user.ChangePasswordRequest;
import com.vega.hrm.request.user.CreateUserRequest; import com.vega.hrm.request.user.CreateUserRequest;
import com.vega.hrm.request.user.ForgotPasswordRequest;
import com.vega.hrm.request.user.LoginRequest; import com.vega.hrm.request.user.LoginRequest;
import com.vega.hrm.request.user.RegisterRequest;
import com.vega.hrm.request.user.UpdateProfileRequest;
import com.vega.hrm.response.LoginResponse; import com.vega.hrm.response.LoginResponse;
import com.vega.hrm.response.UserInfoResponse;
import java.security.SecureRandom;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.util.Strings; import org.apache.logging.log4j.util.Strings;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -22,7 +37,11 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserService { public class UserService {
private final CoreUserRepository userRepository; private final CoreUserRepository userRepository;
private final CoreRoleRepository roleRepository;
private final CoreFunctionRepository functionRepository;
private final BoRoleFuncRepository roleFuncRepository;
private final RedisService redisService; private final RedisService redisService;
private final FileStorageService fileStorageService;
public BaseResponse<Object> login(LoginRequest request) { public BaseResponse<Object> login(LoginRequest request) {
var user = userRepository.findByUserName(request.getUserName()); var user = userRepository.findByUserName(request.getUserName());
@ -35,22 +54,70 @@ public class UserService {
user.setNumberOfFailedLogins(0L); user.setNumberOfFailedLogins(0L);
user.setLastLoginTime(Instant.now()); user.setLastLoginTime(Instant.now());
userRepository.save(user); userRepository.save(user);
var claims = new HashMap<String, Object>();
claims.put("UserId", user.getId());
claims.put("UserName", user.getUserName());
var token = JwtHelper.generateToken(user.getUserName(), user.getId(), UUID.randomUUID()); var token = JwtHelper.generateToken(user.getUserName(), user.getId(), UUID.randomUUID());
if (Strings.isBlank(token)) { if (Strings.isBlank(token)) {
return BaseResponse.internalSystemError(); return BaseResponse.internalSystemError();
} }
redisService.hSet(CommonConst.TOKEN,user.getId().toString(),token); redisService.hSet(CommonConst.TOKEN, user.getId().toString(), token);
var baseResponseLogin = BaseResponse.builder().code("00"); // Lấy thông tin role
baseResponseLogin.data( LoginResponse.RoleInfo roleInfo = null;
LoginResponse.builder() if (user.getRoleId() != null) {
.user(user) BoRole role = roleRepository.findById(user.getRoleId()).orElse(null);
.token(token) if (role != null) {
.build()); roleInfo = LoginResponse.RoleInfo.builder()
return baseResponseLogin.build(); .roleId(role.getId())
.roleName(role.getRoleName())
.description(role.getDescription())
.build();
}
}
// Lấy danh sách function/menu được phép truy cập
List<LoginResponse.FunctionInfo> functions = new ArrayList<>();
if (user.getRoleId() != null) {
List<UUID> functionIds = roleFuncRepository.findFunctionIdsByRoleId(user.getRoleId());
if (!functionIds.isEmpty()) {
List<BoFunction> functionList = functionRepository.findByIdIn(functionIds);
functions = functionList.stream()
.map(f -> LoginResponse.FunctionInfo.builder()
.functionId(f.getId())
.functionName(f.getFunctionName())
.functionUrl(f.getFunctionUrl())
.functionLevel(f.getFunctionLevel())
.functionOrder(f.getFunctionOrder())
.parentId(f.getParentId())
.build())
.collect(Collectors.toList());
}
}
// Build user info
LoginResponse.UserLoginInfo userInfo = LoginResponse.UserLoginInfo.builder()
.userId(user.getId())
.userName(user.getUserName())
.fullName(user.getFullName())
.email(user.getEmail())
.avatarUrl(user.getAvatarUrl())
.status(user.getStatus())
.build();
var loginResponse = LoginResponse.builder()
.user(userInfo)
.token(token)
.role(roleInfo)
.functions(functions)
.build();
LogHelper.info("Đăng nhập thành công: " + user.getUserName()
+ " với " + functions.size() + " quyền");
return BaseResponse.builder()
.code("00")
.message("Đăng nhập thành công")
.data(loginResponse)
.build();
} }
if (user.getStatus().equals("1")) { if (user.getStatus().equals("1")) {
user.setNumberOfFailedLogins(user.getNumberOfFailedLogins() + 1); user.setNumberOfFailedLogins(user.getNumberOfFailedLogins() + 1);
@ -58,29 +125,270 @@ public class UserService {
} }
} }
return BaseResponse.invalid( return BaseResponse.invalid("Tên đăng nhập hoặc mật khẩu không đúng");
"UserName or Password is valid. Please check.");
} }
@Transactional @Transactional
public BaseResponse<Boolean> insert(CreateUserRequest request) { public BaseResponse<Boolean> insert(CreateUserRequest request) {
var user = userRepository.findByUserName(request.getUserName()); // Kiểm tra username đã tồn tại
if (user != null) { var existingUser = userRepository.findByUserName(request.getUserName());
return BaseResponse.success("User created unsuccessful"); if (existingUser != null) {
return BaseResponse.invalid("Tên đăng nhập đã tồn tại");
} }
// Kiểm tra email đã tồn tại
if (request.getEmail() != null) {
var existingEmail = userRepository.findByEmail(request.getEmail());
if (existingEmail != null) {
return BaseResponse.invalid("Email đã được sử dụng");
}
}
// Validate roleId
UUID roleId;
try {
roleId = UUID.fromString(request.getRoleId());
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("Role ID không hợp lệ");
}
var newUser = new BoUser(); var newUser = new BoUser();
newUser.setId(UUID.randomUUID()); newUser.setId(UUID.randomUUID());
newUser.setUserName(request.getUserName()); newUser.setUserName(request.getUserName());
newUser.setFullName(request.getFullName()); newUser.setFullName(request.getFullName());
newUser.setRoleId(UUID.randomUUID()); newUser.setEmail(request.getEmail());
newUser.setRoleId(roleId);
newUser.setPassword(AuthHelper.hashPassword(request.getUserName(), request.getPassword())); newUser.setPassword(AuthHelper.hashPassword(request.getUserName(), request.getPassword()));
newUser.setStatus("1"); newUser.setStatus("1");
newUser.setPwdExpireDate(Instant.now().plusSeconds(90L * 24 * 60 * 60)); newUser.setPwdExpireDate(Instant.now().plusSeconds(90L * 24 * 60 * 60));
newUser.setCreatedDate(Instant.now()); newUser.setCreatedDate(Instant.now());
newUser.setCreatedUser("system"); newUser.setCreatedUser("admin"); // TODO: Lấy từ token
newUser.setNumberOfFailedLogins(0L); newUser.setNumberOfFailedLogins(0L);
newUser.setIsPasswordChanged(0L); newUser.setIsPasswordChanged(0L);
userRepository.save(newUser); userRepository.save(newUser);
return BaseResponse.success("User created successfully"); LogHelper.info("Tạo user thành công: " + request.getUserName() + " với role: " + roleId);
return BaseResponse.success("Tạo user thành công", true);
}
/**
* Đăng tài khoản mới (cho người dùng tự đăng )
*/
@Transactional
public BaseResponse<Boolean> register(RegisterRequest request) {
// Kiểm tra username đã tồn tại
var existingUser = userRepository.findByUserName(request.getUserName());
if (existingUser != null) {
return BaseResponse.invalid("Tên đăng nhập đã tồn tại");
}
// Kiểm tra email đã tồn tại
if (request.getEmail() != null) {
var existingEmail = userRepository.findByEmail(request.getEmail());
if (existingEmail != null) {
return BaseResponse.invalid("Email đã được sử dụng");
}
}
var newUser = new BoUser();
newUser.setId(UUID.randomUUID());
newUser.setUserName(request.getUserName());
newUser.setFullName(request.getFullName());
newUser.setEmail(request.getEmail());
newUser.setPassword(AuthHelper.hashPassword(request.getUserName(), request.getPassword()));
newUser.setStatus("1");
newUser.setPwdExpireDate(Instant.now().plusSeconds(90L * 24 * 60 * 60));
newUser.setCreatedDate(Instant.now());
newUser.setCreatedUser("self-register");
newUser.setNumberOfFailedLogins(0L);
newUser.setIsPasswordChanged(0L);
userRepository.save(newUser);
LogHelper.info("Đăng ký tài khoản thành công: " + request.getUserName());
return BaseResponse.success("Đăng ký tài khoản thành công", true);
}
/**
* Lấy thông tin tài khoản
*/
public BaseResponse<UserInfoResponse> getUserInfo(String userId) {
try {
UUID id = UUID.fromString(userId);
var user = userRepository.findById(id).orElse(null);
if (user == null) {
return BaseResponse.notFound("Không tìm thấy người dùng");
}
UserInfoResponse response = UserInfoResponse.builder()
.userId(user.getId())
.userName(user.getUserName())
.fullName(user.getFullName())
.email(user.getEmail())
.avatarUrl(user.getAvatarUrl())
.status(user.getStatus())
.roleId(user.getRoleId())
.lastLoginTime(user.getLastLoginTime())
.createdDate(user.getCreatedDate())
.build();
return BaseResponse.success("Lấy thông tin thành công", response);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("User ID không hợp lệ");
}
}
/**
* Cập nhật thông tin nhân
*/
@Transactional
public BaseResponse<Boolean> updateProfile(UpdateProfileRequest request) {
try {
UUID id = UUID.fromString(request.getUserId());
var user = userRepository.findById(id).orElse(null);
if (user == null) {
return BaseResponse.notFound("Không tìm thấy người dùng");
}
// Kiểm tra email trùng (nếu thay đổi)
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
var existingEmail = userRepository.findByEmail(request.getEmail());
if (existingEmail != null && !existingEmail.getId().equals(id)) {
return BaseResponse.invalid("Email đã được sử dụng bởi tài khoản khác");
}
}
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setUpdatedDate(Instant.now());
user.setUpdatedUser(user.getUserName());
userRepository.save(user);
LogHelper.info("Cập nhật thông tin thành công cho user: " + user.getUserName());
return BaseResponse.success("Cập nhật thông tin thành công", true);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("User ID không hợp lệ");
}
}
/**
* Đổi mật khẩu
*/
@Transactional
public BaseResponse<Boolean> changePassword(ChangePasswordRequest request) {
try {
UUID id = UUID.fromString(request.getUserId());
var user = userRepository.findById(id).orElse(null);
if (user == null) {
return BaseResponse.notFound("Không tìm thấy người dùng");
}
// Kiểm tra mật khẩu
String hashedOldPassword = AuthHelper.hashPassword(user.getUserName(), request.getOldPassword());
if (!user.getPassword().equals(hashedOldPassword)) {
return BaseResponse.invalid("Mật khẩu cũ không đúng");
}
// Cập nhật mật khẩu mới
String hashedNewPassword = AuthHelper.hashPassword(user.getUserName(), request.getNewPassword());
user.setPassword(hashedNewPassword);
user.setIsPasswordChanged(1L);
user.setPwdExpireDate(Instant.now().plusSeconds(90L * 24 * 60 * 60));
user.setUpdatedDate(Instant.now());
user.setUpdatedUser(user.getUserName());
userRepository.save(user);
LogHelper.info("Đổi mật khẩu thành công cho user: " + user.getUserName());
return BaseResponse.success("Đổi mật khẩu thành công", true);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid("User ID không hợp lệ");
}
}
/**
* Quên mật khẩu - Tạo mật khẩu mới gửi qua email (giả lập)
*/
@Transactional
public BaseResponse<String> forgotPassword(ForgotPasswordRequest request) {
var user = userRepository.findByEmail(request.getEmail());
if (user == null) {
// Không tiết lộ email tồn tại hay không (bảo mật)
return BaseResponse.success("Nếu email tồn tại, mật khẩu mới đã được gửi", null);
}
// Tạo mật khẩu ngẫu nhiên
String newPassword = generateRandomPassword(8);
// Hash lưu mật khẩu mới
String hashedPassword = AuthHelper.hashPassword(user.getUserName(), newPassword);
user.setPassword(hashedPassword);
user.setIsPasswordChanged(0L); // Bắt buộc đổi mật khẩu lần đầu đăng nhập
user.setPwdExpireDate(Instant.now().plusSeconds(7L * 24 * 60 * 60)); // 7 ngày
user.setUpdatedDate(Instant.now());
user.setUpdatedUser("system-forgot-password");
userRepository.save(user);
// TODO: Tích hợp email service để gửi mật khẩu mới
LogHelper.info("Tạo mật khẩu mới cho user: " + user.getUserName());
LogHelper.info("Mật khẩu tạm thời (chỉ hiển thị trong môi trường dev): " + newPassword);
// Trong production, không trả về mật khẩu qua API
return BaseResponse.success("Mật khẩu mới đã được gửi qua email", newPassword);
}
/**
* Tạo mật khẩu ngẫu nhiên
*/
private String generateRandomPassword(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%";
SecureRandom random = new SecureRandom();
StringBuilder password = new StringBuilder();
for (int i = 0; i < length; i++) {
password.append(chars.charAt(random.nextInt(chars.length())));
}
return password.toString();
}
/**
* Upload avatar cho người dùng
*/
@Transactional
public BaseResponse<String> uploadAvatar(String userId, org.springframework.web.multipart.MultipartFile file) {
try {
UUID id = UUID.fromString(userId);
var user = userRepository.findById(id).orElse(null);
if (user == null) {
return BaseResponse.notFound("Không tìm thấy người dùng");
}
// Xóa avatar nếu
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
fileStorageService.deleteAvatarFile(user.getAvatarUrl());
}
// Lưu avatar mới
String avatarUrl = fileStorageService.storeAvatarFile(file, userId);
user.setAvatarUrl(avatarUrl);
user.setUpdatedDate(Instant.now());
user.setUpdatedUser(user.getUserName());
userRepository.save(user);
LogHelper.info("Upload avatar thành công cho user: " + user.getUserName());
return BaseResponse.success("Upload avatar thành công", avatarUrl);
} catch (IllegalArgumentException e) {
return BaseResponse.invalid(e.getMessage());
} catch (Exception e) {
LogHelper.error("Lỗi khi upload avatar: " + e.getMessage());
return BaseResponse.internalSystemError();
}
} }
} }

View File

@ -5,9 +5,11 @@ vega.hrm.postgre.enabled=true
vega.jpa.repository.basePackage=com.vega.hrm.core.repositories vega.jpa.repository.basePackage=com.vega.hrm.core.repositories
vega.jpa.entity.basePackage=com.vega.hrm.core.entities vega.jpa.entity.basePackage=com.vega.hrm.core.entities
spring.config.import=file:config/shared.properties spring.config.import=file:config/shared.properties
logging.config=file:config/log4j2.properties logging.config=file:./config/log4j2.properties
springdoc.api-docs.path=/api-docs/auth
springdoc.swagger-ui.path=/swagger-ui/auth
springdoc.swagger-ui.operations-sorter=method springdoc.swagger-ui.operations-sorter=method
springdoc.swagger-ui.tags-sorter=alpha springdoc.swagger-ui.tags-sorter=alpha
springdoc.swagger-ui.path=/auth/swagger-ui/index.html
springdoc.api-docs.path=/auth/api-docs

View File

@ -0,0 +1,21 @@
package com.vega.hrm.core.configs;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class StaticResourceConfig implements WebMvcConfigurer {
@Value("${file.upload.avatar-dir:uploads/avatars}")
private String avatarDir;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Map URL /uploads/avatars/** to file system directory
registry.addResourceHandler("/uploads/avatars/**")
.addResourceLocations("file:" + avatarDir + "/");
}
}

View File

@ -86,4 +86,8 @@ public class BoUser {
@Column(name = "email") @Column(name = "email")
private String email; private String email;
@Size(max = 500)
@Column(name = "avatar_url", length = 500)
private String avatarUrl;
} }

View File

@ -41,7 +41,26 @@ public class AuthorizationFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) { protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) {
try { try {
var uri = request.getRequestURI(); String uri = request.getRequestURI(); // /auth/api/v1/test
String[] parts = uri.split("/");
if(!uri.contains("swagger-ui")) {
if(!uri.contains("api-docs")) {
if (parts.length > 1) {
String first = parts[1];
if ("auth".equals(first) || "report".equals(first)) {
String newUri = uri.replaceFirst("/" + first, "");
request.getRequestDispatcher(newUri).forward(request, response);
return;
}
}
}
}
if (EXCLUDE_URIS.contains(uri) || uri.contains("actuator") || uri.contains("swagger") || uri.contains("api-docs")) { if (EXCLUDE_URIS.contains(uri) || uri.contains("actuator") || uri.contains("swagger") || uri.contains("api-docs")) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;

View File

@ -2,11 +2,13 @@ package com.vega.hrm.core.filters;
import com.vega.hrm.core.helpers.LogHelper; import com.vega.hrm.core.helpers.LogHelper;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -20,11 +22,27 @@ import org.springframework.web.filter.OncePerRequestFilter;
@Component @Component
@Slf4j @Slf4j
@Order(1) @Order(0)
public class CorsFilter extends OncePerRequestFilter { public class CorsFilter extends OncePerRequestFilter {
/**
* dụ cấu hình:
* cors.allowed-origins=*
* cors.allowed-origins=http://localhost:3000,https://vega-frontend.onestop.vn
*/
@Value("${cors.allowed-origins:*}") @Value("${cors.allowed-origins:*}")
private String[] allowedOrigins; private String allowedOriginsConfig;
private List<String> allowedOrigins;
@PostConstruct
public void init() {
this.allowedOrigins = Arrays.stream(allowedOriginsConfig.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList();
}
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
@NonNull HttpServletRequest request, @NonNull HttpServletRequest request,
@ -33,20 +51,30 @@ public class CorsFilter extends OncePerRequestFilter {
) { ) {
try { try {
var startTime = Instant.now(); var startTime = Instant.now();
// Xử CORS
var origin = request.getHeader("Origin"); var origin = request.getHeader("Origin");
if (Strings.isNotBlank(origin) && Arrays.stream(allowedOrigins).toList() if (Strings.isNotBlank(origin)) {
.contains(origin)) { // Nếu cấu hình chứa "*" thì cho phép mọi origin,
response.setHeader("Access-Control-Allow-Origin", origin); // còn không thì chỉ cho phép những origin nằm trong danh sách.
if (allowedOrigins.contains("*") || allowedOrigins.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Vary", "Origin");
}
} }
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); response.setHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, POST, OPTIONS");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, b"); response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, b");
response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Credentials", "true");
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
// Preflight request
if (request.getMethod().equalsIgnoreCase("OPTIONS")) { if (request.getMethod().equalsIgnoreCase("OPTIONS")) {
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(HttpServletResponse.SC_OK);
return; return;
} }
// Logging + trace
var clientIp = request.getHeader("X-Forwarded-For"); var clientIp = request.getHeader("X-Forwarded-For");
if (Strings.isBlank(clientIp)) { if (Strings.isBlank(clientIp)) {
clientIp = request.getRemoteAddr(); clientIp = request.getRemoteAddr();
@ -58,7 +86,9 @@ public class CorsFilter extends OncePerRequestFilter {
var traceId = UUID.randomUUID().toString(); var traceId = UUID.randomUUID().toString();
ThreadContext.put("traceId", traceId); ThreadContext.put("traceId", traceId);
LogHelper.info("Request start"); LogHelper.info("Request start");
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
var duration = Instant.now().toEpochMilli() - startTime.toEpochMilli(); var duration = Instant.now().toEpochMilli() - startTime.toEpochMilli();
LogHelper.info("Request end - Duration: " + duration + "ms"); LogHelper.info("Request end - Duration: " + duration + "ms");
} catch (Exception e) { } catch (Exception e) {

View File

@ -0,0 +1,25 @@
package com.vega.hrm.core.repositories;
import com.vega.hrm.core.entities.BoRoleFunc;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface BoRoleFuncRepository extends JpaRepository<BoRoleFunc, UUID> {
@Query("SELECT rf FROM BoRoleFunc rf WHERE rf.role.id = :roleId")
List<BoRoleFunc> findByRoleId(@Param("roleId") UUID roleId);
@Modifying
@Query("DELETE FROM BoRoleFunc rf WHERE rf.role.id = :roleId")
void deleteByRoleId(@Param("roleId") UUID roleId);
@Query("SELECT rf.function.id FROM BoRoleFunc rf WHERE rf.role.id = :roleId")
List<UUID> findFunctionIdsByRoleId(@Param("roleId") UUID roleId);
}

View File

@ -10,6 +10,14 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface CoreFunctionRepository extends JpaRepository<BoFunction, UUID> { public interface CoreFunctionRepository extends JpaRepository<BoFunction, UUID> {
@Query("SELECT f FROM BoFunction f ORDER BY f.functionLevel, f.functionOrder")
List<BoFunction> findAllOrderByLevelAndOrder();
@Query("SELECT f FROM BoFunction f WHERE f.parentId = :parentId ORDER BY f.functionOrder")
List<BoFunction> findByParentId(@Param("parentId") UUID parentId);
@Query("SELECT f FROM BoFunction f WHERE f.id IN :ids ORDER BY f.functionLevel, f.functionOrder")
List<BoFunction> findByIdIn(@Param("ids") List<UUID> ids);
} }

View File

@ -1,11 +1,14 @@
package com.vega.hrm.core.repositories; package com.vega.hrm.core.repositories;
import com.vega.hrm.core.entities.BoRole; import com.vega.hrm.core.entities.BoRole;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface CoreRoleRepository extends JpaRepository<BoRole, UUID> { public interface CoreRoleRepository extends JpaRepository<BoRole, UUID> {
BoRole findBoRoleById(UUID id); BoRole findBoRoleById(UUID id);
BoRole findByRoleName(String roleName);
List<BoRole> findByStatus(String status);
} }

View File

@ -9,4 +9,5 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface CoreUserRepository extends JpaRepository<BoUser, UUID> { public interface CoreUserRepository extends JpaRepository<BoUser, UUID> {
BoUser findByUserName(String username); BoUser findByUserName(String username);
BoUser findByEmail(String email);
} }

View File

@ -0,0 +1,113 @@
package com.vega.hrm.core.service;
import com.vega.hrm.core.helpers.LogHelper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@Service
public class FileStorageService {
private final Path avatarStorageLocation;
public FileStorageService(@Value("${file.upload.avatar-dir:uploads/avatars}") String avatarDir) {
this.avatarStorageLocation = Paths.get(avatarDir).toAbsolutePath().normalize();
try {
Files.createDirectories(this.avatarStorageLocation);
LogHelper.info("Thư mục lưu avatar đã được tạo: " + this.avatarStorageLocation);
} catch (IOException ex) {
LogHelper.error("Không thể tạo thư mục lưu trữ avatar: " + ex.getMessage());
throw new RuntimeException("Không thể tạo thư mục lưu trữ avatar", ex);
}
}
/**
* Lưu file avatar trả về đường dẫn tương đối
*/
public String storeAvatarFile(MultipartFile file, String userId) {
if (file.isEmpty()) {
throw new IllegalArgumentException("File rỗng, không thể upload");
}
// Lấy extension gốc
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
String fileExtension = "";
if (originalFilename.contains(".")) {
fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
// Validate extension
if (!isValidImageExtension(fileExtension)) {
throw new IllegalArgumentException("Chỉ chấp nhận file ảnh: jpg, jpeg, png, gif");
}
// Tạo tên file unique: userId_timestamp_uuid.ext
String newFilename = userId + "_" + System.currentTimeMillis() + "_"
+ UUID.randomUUID().toString().substring(0, 8) + fileExtension;
try {
// Kiểm tra path traversal attack
if (newFilename.contains("..")) {
throw new IllegalArgumentException("Tên file không hợp lệ: " + newFilename);
}
Path targetLocation = this.avatarStorageLocation.resolve(newFilename);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
LogHelper.info("Đã lưu avatar: " + newFilename + " (size: " + file.getSize() + " bytes)");
// Trả về đường dẫn tương đối để lưu vào DB
return "/uploads/avatars/" + newFilename;
} catch (IOException ex) {
LogHelper.error("Lỗi khi lưu file avatar: " + ex.getMessage());
throw new RuntimeException("Lỗi khi lưu file avatar", ex);
}
}
/**
* Xóa file avatar
*/
public void deleteAvatarFile(String avatarUrl) {
if (avatarUrl == null || avatarUrl.isEmpty()) {
return;
}
try {
// Lấy tên file từ URL
String filename = avatarUrl.substring(avatarUrl.lastIndexOf("/") + 1);
Path filePath = this.avatarStorageLocation.resolve(filename).normalize();
Files.deleteIfExists(filePath);
LogHelper.info("Đã xóa avatar cũ: " + filename);
} catch (IOException ex) {
LogHelper.error("Lỗi khi xóa avatar cũ: " + ex.getMessage());
}
}
/**
* Kiểm tra extension hợp lệ
*/
private boolean isValidImageExtension(String extension) {
String lowerExt = extension.toLowerCase();
return lowerExt.equals(".jpg") || lowerExt.equals(".jpeg")
|| lowerExt.equals(".png") || lowerExt.equals(".gif");
}
/**
* Lấy đường dẫn đầy đủ của file
*/
public Path getAvatarPath(String filename) {
return this.avatarStorageLocation.resolve(filename).normalize();
}
}

View File

@ -1,4 +1,4 @@
google.client.clientId = 719251949807-0jqbsmlh0a116cm8vm47oknmc5vpi19q.apps.googleusercontent.com # File Upload Configuration
google.client.clientSecret = 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 file.upload.avatar-dir=uploads/avatars
google.client.redirect.uri=http://localhost:8089/api/google/user/callback spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=5MB

View File

@ -6,8 +6,8 @@ vega.jpa.repository.basePackage=com.vega.hrm.core.repositories
vega.jpa.entity.basePackage=com.vega.hrm.core.entities vega.jpa.entity.basePackage=com.vega.hrm.core.entities
spring.config.import=file:config/shared.properties spring.config.import=file:config/shared.properties
logging.config=file:config/log4j2.properties logging.config=file:config/log4j2.properties
springdoc.api-docs.path=/api-docs/report springdoc.swagger-ui.path=/report/swagger-ui/index.html
springdoc.swagger-ui.path=/swagger-ui/report springdoc.api-docs.path=/report/api-docs
springdoc.swagger-ui.operations-sorter=method springdoc.swagger-ui.operations-sorter=method
springdoc.swagger-ui.tags-sorter=alpha springdoc.swagger-ui.tags-sorter=alpha