diff --git a/common-events/src/main/java/events/user/UserEvent.java b/common-events/src/main/java/events/user/UserEvent.java
new file mode 100644
index 00000000..5a22e22e
--- /dev/null
+++ b/common-events/src/main/java/events/user/UserEvent.java
@@ -0,0 +1,22 @@
+package events.user;
+
+import events.user.data.UserPayload;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UserEvent {
+ Type type;
+ String id; // userId
+ UserPayload payload; // null nếu DELETED
+
+ public enum Type { CREATED, UPDATED, DELETED, RESTORED }
+}
\ No newline at end of file
diff --git a/common-events/src/main/java/events/user/UserRegisteredEvent.java b/common-events/src/main/java/events/user/UserRegisteredEvent.java
new file mode 100644
index 00000000..8d7cffc4
--- /dev/null
+++ b/common-events/src/main/java/events/user/UserRegisteredEvent.java
@@ -0,0 +1,21 @@
+package events.user;
+
+import events.user.data.UserPayload;
+import events.user.data.UserProfileCreationPayload;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UserRegisteredEvent {
+ String id;
+ UserPayload user;
+ UserProfileCreationPayload profile;
+}
diff --git a/common-events/src/main/java/events/user/data/UserPayload.java b/common-events/src/main/java/events/user/data/UserPayload.java
new file mode 100644
index 00000000..a32d8919
--- /dev/null
+++ b/common-events/src/main/java/events/user/data/UserPayload.java
@@ -0,0 +1,27 @@
+package events.user.data;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+import java.time.Instant;
+import java.util.Set;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UserPayload {
+ String userId; // id của identity -> dùng làm @Id trong profile
+ String username;
+ String email;
+ boolean active; // enabled của identity
+
+ Set roles; // ["ADMIN","USER",...]
+ Instant createdAt;
+ Instant updatedAt;
+}
\ No newline at end of file
diff --git a/common-events/src/main/java/events/user/data/UserProfileCreationPayload.java b/common-events/src/main/java/events/user/data/UserProfileCreationPayload.java
new file mode 100644
index 00000000..abe7b131
--- /dev/null
+++ b/common-events/src/main/java/events/user/data/UserProfileCreationPayload.java
@@ -0,0 +1,27 @@
+package events.user.data;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+import java.time.Instant;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UserProfileCreationPayload {
+ String firstName;
+ String lastName;
+ Instant dob;
+ String bio;
+ Boolean gender;
+ String displayName;
+ Integer education;
+ String[] links;
+ String city;
+}
diff --git a/identity-service/pom.xml b/identity-service/pom.xml
index 0994211a..9b0929c8 100644
--- a/identity-service/pom.xml
+++ b/identity-service/pom.xml
@@ -212,6 +212,12 @@
spring-security-test
test
+
+
+ com.codecampus
+ common-events
+ ${project.version}
+
diff --git a/identity-service/src/main/java/com/codecampus/identity/configuration/config/init/ApplicationInitializationService.java b/identity-service/src/main/java/com/codecampus/identity/configuration/config/init/ApplicationInitializationService.java
index 5a050b54..a19ed7df 100644
--- a/identity-service/src/main/java/com/codecampus/identity/configuration/config/init/ApplicationInitializationService.java
+++ b/identity-service/src/main/java/com/codecampus/identity/configuration/config/init/ApplicationInitializationService.java
@@ -1,13 +1,15 @@
package com.codecampus.identity.configuration.config.init;
-import com.codecampus.identity.dto.request.profile.UserProfileCreationRequest;
import com.codecampus.identity.entity.account.Role;
import com.codecampus.identity.entity.account.User;
+import com.codecampus.identity.mapper.kafka.UserPayloadMapper;
import com.codecampus.identity.repository.account.PermissionRepository;
import com.codecampus.identity.repository.account.RoleRepository;
import com.codecampus.identity.repository.account.UserRepository;
import com.codecampus.identity.repository.httpclient.profile.ProfileClient;
+import com.codecampus.identity.service.kafka.UserEventProducer;
import com.codecampus.identity.utils.ConvertUtils;
+import events.user.data.UserProfileCreationPayload;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
@@ -32,11 +34,19 @@ public class ApplicationInitializationService {
RoleRepository roleRepository;
PermissionRepository permissionRepository;
UserRepository userRepository;
+
PasswordEncoder passwordEncoder;
ProfileClient profileClient;
+ UserPayloadMapper userPayloadMapper;
+
+ UserEventProducer userEventProducer;
+
@Transactional
- void createAdminUser(String username, String password, String email) {
+ void createAdminUser(
+ String username,
+ String password,
+ String email) {
Role adminRole = checkRoleAndCreate(ADMIN_ROLE);
Set roles = new HashSet<>();
@@ -50,9 +60,10 @@ void createAdminUser(String username, String password, String email) {
.roles(roles)
.build());
- profileClient.internalCreateUserProfile(
- UserProfileCreationRequest.builder()
- .userId(user.getId())
+ userEventProducer.publishCreatedUserEvent(user);
+
+ UserProfileCreationPayload payload =
+ UserProfileCreationPayload.builder()
.firstName("Admin")
.lastName("Sys")
.dob(ConvertUtils.parseDdMmYyyyToInstant("28/03/2004"))
@@ -63,8 +74,9 @@ void createAdminUser(String username, String password, String email) {
.links(new String[] {"https://github.com/yunomix2834",
"https://github.com/CapstoneProjectCMC/backend"})
.city("Vietnam")
- .build()
- );
+ .build();
+
+ userEventProducer.publishRegisteredUserEvent(user, payload);
}
@Transactional
@@ -82,9 +94,10 @@ void createUser(String username, String password, String email) {
.roles(roles)
.build());
- profileClient.internalCreateUserProfile(
- UserProfileCreationRequest.builder()
- .userId(user.getId())
+ userEventProducer.publishCreatedUserEvent(user);
+
+ UserProfileCreationPayload payload =
+ UserProfileCreationPayload.builder()
.firstName("Code")
.lastName("Campus")
.dob(ConvertUtils.parseDdMmYyyyToInstant("28/03/2004"))
@@ -95,8 +108,9 @@ void createUser(String username, String password, String email) {
.links(new String[] {"https://github.com/yunomix2834",
"https://github.com/CapstoneProjectCMC/backend"})
.city("Vietnam")
- .build()
- );
+ .build();
+
+ userEventProducer.publishRegisteredUserEvent(user, payload);
}
Role checkRoleAndCreate(String roleName) {
diff --git a/identity-service/src/main/java/com/codecampus/identity/configuration/kafka/KafkaProducerConfig.java b/identity-service/src/main/java/com/codecampus/identity/configuration/kafka/KafkaProducerConfig.java
new file mode 100644
index 00000000..5bcb5ae4
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/configuration/kafka/KafkaProducerConfig.java
@@ -0,0 +1,45 @@
+package com.codecampus.identity.configuration.kafka;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.core.DefaultKafkaProducerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.core.ProducerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@EnableKafka
+@RequiredArgsConstructor
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public class KafkaProducerConfig {
+ Environment env;
+
+ @Bean
+ public ProducerFactory producerFactory() {
+ Map props = new HashMap<>();
+ props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
+ env.getProperty("spring.kafka.bootstrap-servers",
+ "localhost:9092"));
+ props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
+ StringSerializer.class);
+ props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
+ StringSerializer.class);
+ props.put(ProducerConfig.ACKS_CONFIG, "all");
+ return new DefaultKafkaProducerFactory<>(props);
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate(
+ ProducerFactory factory) {
+ return new KafkaTemplate<>(factory);
+ }
+}
diff --git a/identity-service/src/main/java/com/codecampus/identity/controller/authentication/EmailChangeController.java b/identity-service/src/main/java/com/codecampus/identity/controller/authentication/EmailChangeController.java
new file mode 100644
index 00000000..072de770
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/controller/authentication/EmailChangeController.java
@@ -0,0 +1,45 @@
+package com.codecampus.identity.controller.authentication;
+
+import com.codecampus.identity.dto.common.ApiResponse;
+import com.codecampus.identity.dto.request.authentication.ChangeEmailRequest;
+import com.codecampus.identity.dto.request.authentication.ChangeEmailVerifyRequest;
+import com.codecampus.identity.service.authentication.EmailChangeService;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+@Builder
+@Slf4j
+@RequestMapping("/user/email")
+public class EmailChangeController {
+
+ EmailChangeService emailChangeService;
+
+ @PostMapping("/change/request")
+ public ApiResponse request(
+ @RequestBody ChangeEmailRequest changeEmailRequest) {
+ emailChangeService.requestChangeEmail(changeEmailRequest);
+ return ApiResponse.builder()
+ .message("OTP đã gửi tới email mới")
+ .build();
+ }
+
+ @PostMapping("/change/verify")
+ public ApiResponse verify(
+ @RequestBody ChangeEmailVerifyRequest changeEmailVerifyRequest) {
+ emailChangeService.verifyOtp(changeEmailVerifyRequest);
+ emailChangeService.verifyOtp(changeEmailVerifyRequest);
+ return ApiResponse.builder()
+ .message("Đổi email thành công")
+ .build();
+ }
+}
diff --git a/identity-service/src/main/java/com/codecampus/identity/controller/authentication/UserController.java b/identity-service/src/main/java/com/codecampus/identity/controller/authentication/UserController.java
index c84c3338..baec5afc 100644
--- a/identity-service/src/main/java/com/codecampus/identity/controller/authentication/UserController.java
+++ b/identity-service/src/main/java/com/codecampus/identity/controller/authentication/UserController.java
@@ -33,10 +33,10 @@ public class UserController {
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/user")
- ApiResponse createUser(
+ ApiResponse createUser(
@RequestBody @Valid UserCreationRequest request) {
- return ApiResponse.builder()
- .result(userService.createUser(request))
+ userService.createUser(request);
+ return ApiResponse.builder()
.message("Create User successful")
.build();
}
@@ -92,20 +92,20 @@ ApiResponse deleteUser(
@PreAuthorize("hasRole('ADMIN')")
@PutMapping("/user/{userId}")
- ApiResponse updateUser(
+ ApiResponse updateUser(
@PathVariable("userId") String userId,
@RequestBody UserUpdateRequest request) {
- return ApiResponse.builder()
- .result(userService.updateUser(userId, request))
+ userService.updateUserById(userId, request);
+ return ApiResponse.builder()
.message("Update User successful")
.build();
}
@PutMapping("/user/my-info")
- ApiResponse updateMyInfo(
+ ApiResponse updateMyInfo(
UserUpdateRequest request) {
- return ApiResponse.builder()
- .result(userService.updateMyInfo(request))
+ userService.updateMyInfo(request);
+ return ApiResponse.builder()
.message("Update My Info successful")
.build();
}
diff --git a/identity-service/src/main/java/com/codecampus/identity/dto/request/authentication/ChangeEmailRequest.java b/identity-service/src/main/java/com/codecampus/identity/dto/request/authentication/ChangeEmailRequest.java
new file mode 100644
index 00000000..109d07cb
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/dto/request/authentication/ChangeEmailRequest.java
@@ -0,0 +1,12 @@
+package com.codecampus.identity.dto.request.authentication;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChangeEmailRequest {
+ String newEmail;
+}
diff --git a/identity-service/src/main/java/com/codecampus/identity/dto/request/authentication/ChangeEmailVerifyRequest.java b/identity-service/src/main/java/com/codecampus/identity/dto/request/authentication/ChangeEmailVerifyRequest.java
new file mode 100644
index 00000000..76ee485b
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/dto/request/authentication/ChangeEmailVerifyRequest.java
@@ -0,0 +1,13 @@
+package com.codecampus.identity.dto.request.authentication;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChangeEmailVerifyRequest {
+ String newEmail;
+ String otpCode;
+}
diff --git a/identity-service/src/main/java/com/codecampus/identity/entity/account/User.java b/identity-service/src/main/java/com/codecampus/identity/entity/account/User.java
index 125c0fff..27587600 100644
--- a/identity-service/src/main/java/com/codecampus/identity/entity/account/User.java
+++ b/identity-service/src/main/java/com/codecampus/identity/entity/account/User.java
@@ -63,7 +63,7 @@ public class User extends AuditMetadata {
@PreRemove
private void doSoftDelete() {
- this.setDeletedBy(AuthenticationHelper.getMyEmail());
+ this.setDeletedBy(AuthenticationHelper.getMyUsername());
this.setDeletedAt(Instant.now());
}
}
diff --git a/identity-service/src/main/java/com/codecampus/identity/mapper/authentication/UserMapper.java b/identity-service/src/main/java/com/codecampus/identity/mapper/authentication/UserMapper.java
index 211291c0..70a16634 100644
--- a/identity-service/src/main/java/com/codecampus/identity/mapper/authentication/UserMapper.java
+++ b/identity-service/src/main/java/com/codecampus/identity/mapper/authentication/UserMapper.java
@@ -13,16 +13,17 @@
@Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserMapper {
- User toUser(UserCreationRequest userCreationRequest);
+ User toUserFromUserCreationRequest(UserCreationRequest userCreationRequest);
@Mapping(target = "userId", ignore = true)
- UserProfileCreationRequest toUserProfileCreationRequest(
- UserCreationRequest req);
+ UserProfileCreationRequest toUserProfileCreationRequestFromUserCreationRequest(
+ UserCreationRequest userCreationRequest);
- UserResponse toUserResponse(User user);
+ UserResponse toUserResponseFromUser(
+ User user);
@Mapping(target = "roles", ignore = true)
- void updateUser(
+ void updateUserUpdateRequestToUser(
@MappingTarget User user,
UserUpdateRequest userUpdateRequest
);
diff --git a/identity-service/src/main/java/com/codecampus/identity/mapper/kafka/UserPayloadMapper.java b/identity-service/src/main/java/com/codecampus/identity/mapper/kafka/UserPayloadMapper.java
new file mode 100644
index 00000000..447c4c24
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/mapper/kafka/UserPayloadMapper.java
@@ -0,0 +1,34 @@
+package com.codecampus.identity.mapper.kafka;
+
+import com.codecampus.identity.dto.request.authentication.UserCreationRequest;
+import com.codecampus.identity.entity.account.Role;
+import com.codecampus.identity.entity.account.User;
+import events.user.data.UserPayload;
+import events.user.data.UserProfileCreationPayload;
+import org.mapstruct.Mapper;
+
+import java.time.Instant;
+import java.util.stream.Collectors;
+
+@Mapper(componentModel = "spring")
+public interface UserPayloadMapper {
+
+ UserProfileCreationPayload toUserProfileCreationPayloadFromUserCreationRequest(
+ UserCreationRequest userCreationRequest);
+
+ default UserPayload toUserPayloadFromUser(User user) {
+ return UserPayload.builder()
+ .userId(user.getId())
+ .username(user.getUsername())
+ .email(user.getEmail())
+ .active(user.isEnabled())
+ .roles(user.getRoles()
+ .stream()
+ .map(Role::getName)
+ .collect(Collectors.toSet()))
+ .createdAt(user.getCreatedAt())
+ .updatedAt(user.getUpdatedAt() == null ? Instant.now() :
+ user.getUpdatedAt())
+ .build();
+ }
+}
diff --git a/identity-service/src/main/java/com/codecampus/identity/service/account/UserService.java b/identity-service/src/main/java/com/codecampus/identity/service/account/UserService.java
index 9e975b3a..a980f84b 100644
--- a/identity-service/src/main/java/com/codecampus/identity/service/account/UserService.java
+++ b/identity-service/src/main/java/com/codecampus/identity/service/account/UserService.java
@@ -13,10 +13,13 @@
import com.codecampus.identity.helper.ProfileSyncHelper;
import com.codecampus.identity.mapper.authentication.UserMapper;
import com.codecampus.identity.mapper.client.UserProfileMapper;
+import com.codecampus.identity.mapper.kafka.UserPayloadMapper;
import com.codecampus.identity.repository.account.RoleRepository;
import com.codecampus.identity.repository.account.UserRepository;
import com.codecampus.identity.repository.httpclient.profile.ProfileClient;
import com.codecampus.identity.service.authentication.OtpService;
+import com.codecampus.identity.service.kafka.UserEventProducer;
+import events.user.data.UserProfileCreationPayload;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
@@ -62,11 +65,13 @@ public class UserService {
UserMapper userMapper;
UserProfileMapper userProfileMapper;
+ UserPayloadMapper userPayloadMapper;
PasswordEncoder passwordEncoder;
ProfileClient profileClient;
AuthenticationHelper authenticationHelper;
ProfileSyncHelper profileSyncHelper;
+ UserEventProducer userEventProducer;
/**
* Tạo mới người dùng, gán vai trò USER và khởi tạo profile.
@@ -79,20 +84,18 @@ public class UserService {
*
*
* @param request thông tin tạo người dùng mới
- * @return thông tin người dùng vừa tạo (UserResponse)
* @throws AppException nếu user đã tồn tại
*/
@PreAuthorize("hasRole('ADMIN')")
@Transactional
- public UserResponse createUser(UserCreationRequest request) {
+ public void createUser(UserCreationRequest request) {
authenticationHelper.checkExistsUsernameEmail(
request.getUsername(),
request.getEmail()
);
- User user = userMapper.toUser(request);
+ User user = userMapper.toUserFromUserCreationRequest(request);
user.setPassword(passwordEncoder.encode(user.getPassword()));
-
HashSet roles = new HashSet<>();
roleRepository.findById(USER_ROLE)
.ifPresent(roles::add);
@@ -101,12 +104,15 @@ public UserResponse createUser(UserCreationRequest request) {
try {
user = userRepository.save(user);
- profileSyncHelper.createProfile(user, request);
+ userEventProducer.publishCreatedUserEvent(user);
+ UserProfileCreationPayload profilePayload =
+ userPayloadMapper.toUserProfileCreationPayloadFromUserCreationRequest(
+ request);
+ userEventProducer.publishRegisteredUserEvent(
+ user, profilePayload);
} catch (DataIntegrityViolationException e) {
throw new AppException(ErrorCode.USER_ALREADY_EXISTS);
}
-
- return userMapper.toUserResponse(user);
}
/**
@@ -146,14 +152,13 @@ public UserResponse getMyInfo() {
*
* @param userId ID người dùng cần cập nhật
* @param request thông tin cập nhật
- * @return UserResponse chứa thông tin sau khi cập nhật
*/
@PreAuthorize("hasRole('ADMIN')")
- public UserResponse updateUser(
+ public void updateUserById(
String userId,
UserUpdateRequest request) {
User user = findUser(userId);
- return updateUserAndReturnUserResponse(request, user);
+ updateUser(request, user);
}
/**
@@ -162,24 +167,24 @@ public UserResponse updateUser(
* Chỉ cho phép khi username trả về khớp tên trong authentication.
*
* @param request thông tin cập nhật
- * @return UserResponse sau khi cập nhật
*/
- public UserResponse updateMyInfo(
+ public void updateMyInfo(
UserUpdateRequest request) {
User user = findUser(AuthenticationHelper.getMyUserId());
- return updateUserAndReturnUserResponse(request, user);
+ updateUser(request, user);
}
- private UserResponse updateUserAndReturnUserResponse(
+ private void updateUser(
UserUpdateRequest request,
User user) {
- userMapper.updateUser(user, request);
+ userMapper.updateUserUpdateRequestToUser(user, request);
user.setPassword(passwordEncoder.encode(user.getPassword()));
// List roles = roleRepository.findAllById(request.getRoles());
// user.setRoles(new HashSet<>(roles));
+ userRepository.save(user);
- return userMapper.toUserResponse(userRepository.save(user));
+ userEventProducer.publishUpdatedUserEvent(user);
}
/**
@@ -196,8 +201,20 @@ public void deleteUser(String userId) {
user.markDeleted(AuthenticationHelper.getMyEmail());
userRepository.save(user);
- profileSyncHelper.softDeleteProfile(userId,
- AuthenticationHelper.getMyUsername());
+ userEventProducer.publishDeletedUserEvent(user);
+ }
+
+ @PreAuthorize("hasRole('ADMIN')")
+ @Transactional
+ public void restoreUser(String userId) {
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND));
+ if (user.getDeletedAt() != null) {
+ user.setDeletedAt(null);
+ user.setDeletedBy(null);
+ userRepository.save(user);
+ userEventProducer.publishRestoredUserEvent(user);
+ }
}
/**
@@ -215,7 +232,7 @@ public PageResponse getUsers(
Pageable pageable = PageRequest.of(page - 1, size);
Page pageData = userRepository
.findAll(pageable)
- .map(userMapper::toUserResponse);
+ .map(userMapper::toUserResponseFromUser);
return toPageResponse(pageData, page);
}
@@ -228,7 +245,7 @@ public PageResponse getUsers(
* @throws AppException nếu không tìm thấy
*/
public UserResponse getUser(String userId) {
- return userMapper.toUserResponse(
+ return userMapper.toUserResponseFromUser(
userRepository.findById(userId)
.orElseThrow(() -> new AppException(
ErrorCode.USER_NOT_FOUND))
diff --git a/identity-service/src/main/java/com/codecampus/identity/service/authentication/AuthenticationService.java b/identity-service/src/main/java/com/codecampus/identity/service/authentication/AuthenticationService.java
index 04b1f44c..700f381c 100644
--- a/identity-service/src/main/java/com/codecampus/identity/service/authentication/AuthenticationService.java
+++ b/identity-service/src/main/java/com/codecampus/identity/service/authentication/AuthenticationService.java
@@ -16,13 +16,14 @@
import com.codecampus.identity.exception.AppException;
import com.codecampus.identity.exception.ErrorCode;
import com.codecampus.identity.helper.AuthenticationHelper;
-import com.codecampus.identity.helper.ProfileSyncHelper;
import com.codecampus.identity.mapper.authentication.UserMapper;
+import com.codecampus.identity.mapper.kafka.UserPayloadMapper;
import com.codecampus.identity.repository.account.InvalidatedTokenRepository;
import com.codecampus.identity.repository.account.RoleRepository;
import com.codecampus.identity.repository.account.UserRepository;
import com.codecampus.identity.repository.httpclient.google.OutboundGoogleIdentityClient;
import com.codecampus.identity.repository.httpclient.google.OutboundGoogleUserClient;
+import com.codecampus.identity.service.kafka.UserEventProducer;
import com.codecampus.identity.utils.EmailUtils;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
@@ -33,6 +34,7 @@
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
+import events.user.data.UserProfileCreationPayload;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
@@ -84,13 +86,14 @@ public class AuthenticationService {
OtpService otpService;
UserMapper userMapper;
+ UserPayloadMapper userPayloadMapper;
PasswordEncoder passwordEncoder;
OutboundGoogleIdentityClient outboundGoogleIdentityClient;
OutboundGoogleUserClient outboundGoogleUserClient;
AuthenticationHelper authenticationHelper;
- ProfileSyncHelper profileSyncHelper;
+ UserEventProducer userEventProducer;
@NonFinal
@Value("${app.jwt.signerKey}")
@@ -189,7 +192,12 @@ public AuthenticationResponse outboundGoogleLogin(
.displayName(googleUser.getName())
.build();
- profileSyncHelper.createProfile(newUser, googleRequest);
+ userEventProducer.publishCreatedUserEvent(newUser);
+ UserProfileCreationPayload profilePayload =
+ userPayloadMapper.toUserProfileCreationPayloadFromUserCreationRequest(
+ googleRequest);
+ userEventProducer.publishRegisteredUserEvent(newUser,
+ profilePayload);
return newUser;
}
);
@@ -265,18 +273,22 @@ public void register(UserCreationRequest request) {
);
// Tạo user nhưng chưa kích hoạt
- User user = userMapper.toUser(request);
+ User user = userMapper.toUserFromUserCreationRequest(request);
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setEnabled(false); // Chưa kích hoạt
-
- // Gán role mặc định
- HashSet roles = new HashSet<>();
- roleRepository.findById(USER_ROLE).ifPresent(roles::add);
- user.setRoles(roles);
+ user.setRoles(new HashSet<>());
+ roleRepository.findById(USER_ROLE)
+ .ifPresent(r -> user.getRoles().add(r));
try {
userRepository.save(user);
- profileSyncHelper.createProfile(user, request);
+ // Sự kiện định danh
+ userEventProducer.publishCreatedUserEvent(user);
+ // Sự kiện khởi tạo profile giàu dữ liệu
+ var profilePayload =
+ userPayloadMapper.toUserProfileCreationPayloadFromUserCreationRequest(
+ request);
+ userEventProducer.publishRegisteredUserEvent(user, profilePayload);
} catch (DataIntegrityViolationException e) {
throw new AppException(ErrorCode.USER_ALREADY_EXISTS);
}
diff --git a/identity-service/src/main/java/com/codecampus/identity/service/authentication/EmailChangeService.java b/identity-service/src/main/java/com/codecampus/identity/service/authentication/EmailChangeService.java
new file mode 100644
index 00000000..56c42ef5
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/service/authentication/EmailChangeService.java
@@ -0,0 +1,111 @@
+package com.codecampus.identity.service.authentication;
+
+import com.codecampus.identity.dto.request.authentication.ChangeEmailRequest;
+import com.codecampus.identity.dto.request.authentication.ChangeEmailVerifyRequest;
+import com.codecampus.identity.entity.account.OtpVerification;
+import com.codecampus.identity.entity.account.User;
+import com.codecampus.identity.exception.AppException;
+import com.codecampus.identity.exception.ErrorCode;
+import com.codecampus.identity.helper.AuthenticationHelper;
+import com.codecampus.identity.repository.account.OtpVerificationRepository;
+import com.codecampus.identity.repository.account.UserRepository;
+import com.codecampus.identity.service.kafka.UserEventProducer;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Random;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public class EmailChangeService {
+
+ OtpVerificationRepository otpVerificationRepository;
+ UserRepository userRepository;
+ UserEventProducer userEventProducer;
+
+ JavaMailSender javaMailSender;
+
+ @Value("${app.otp.expiry-minutes}")
+ @NonFinal
+ protected int otpExpiryMinutes;
+
+ public void requestChangeEmail(
+ ChangeEmailRequest changeEmailRequest) {
+ // Không cho trùng email
+ if (userRepository.existsByEmail(changeEmailRequest.getNewEmail())) {
+ throw new AppException(ErrorCode.EMAIL_ALREADY_EXISTS);
+ }
+
+ String otpCode = generateOtp();
+ Instant expiryTime = Instant.now()
+ .plus(otpExpiryMinutes, ChronoUnit.MINUTES);
+
+ OtpVerification otpVerification = otpVerificationRepository.findByEmail(
+ changeEmailRequest.getNewEmail())
+ .map(o -> {
+ o.setOtpCode(otpCode);
+ o.setExpiryTime(expiryTime);
+ o.setVerified(false);
+ return o;
+ })
+ .orElseGet(() -> OtpVerification.builder()
+ .email(changeEmailRequest.getNewEmail())
+ .otpCode(otpCode)
+ .expiryTime(expiryTime)
+ .verified(false)
+ .build());
+ otpVerificationRepository.save(otpVerification);
+
+ sendEmail(changeEmailRequest.getNewEmail(), otpCode);
+ }
+
+ public void verifyOtp(ChangeEmailVerifyRequest changeEmailVerifyRequest) {
+ OtpVerification otpVerification = otpVerificationRepository.findByEmail(
+ changeEmailVerifyRequest.getNewEmail())
+ .orElseThrow(() -> new AppException(ErrorCode.EMAIL_NOT_FOUND));
+
+ if (!otpVerification.getOtpCode()
+ .equals(changeEmailVerifyRequest.getOtpCode())) {
+ throw new AppException(ErrorCode.INVALID_OTP);
+ }
+
+ if (Instant.now().isAfter(otpVerification.getExpiryTime())) {
+ throw new AppException(ErrorCode.OTP_EXPIRED);
+ }
+
+ // Cập nhật email cho user hiện tại
+ User user = userRepository.findById(AuthenticationHelper.getMyUserId())
+ .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND));
+ user.setEmail(changeEmailVerifyRequest.getNewEmail());
+ userRepository.save(user);
+ userEventProducer.publishUpdatedUserEvent(user);
+
+ otpVerification.setVerified(true);
+ otpVerificationRepository.save(otpVerification);
+ }
+
+ private String generateOtp() {
+ return String.format("%06d", new Random().nextInt(999999));
+ }
+
+ private void sendEmail(String email, String otpCode) {
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setTo(email);
+ message.setSubject("Xác minh đổi email");
+ message.setText(
+ "Mã OTP để xác minh email mới: " + otpCode + "\nHiệu lực: "
+ + otpExpiryMinutes + " phút.");
+ javaMailSender.send(message);
+ }
+}
diff --git a/identity-service/src/main/java/com/codecampus/identity/service/authentication/OtpService.java b/identity-service/src/main/java/com/codecampus/identity/service/authentication/OtpService.java
index 222b56df..c0dd15fb 100644
--- a/identity-service/src/main/java/com/codecampus/identity/service/authentication/OtpService.java
+++ b/identity-service/src/main/java/com/codecampus/identity/service/authentication/OtpService.java
@@ -9,6 +9,7 @@
import com.codecampus.identity.exception.ErrorCode;
import com.codecampus.identity.repository.account.OtpVerificationRepository;
import com.codecampus.identity.repository.account.UserRepository;
+import com.codecampus.identity.service.kafka.UserEventProducer;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
@@ -41,6 +42,8 @@ public class OtpService {
JavaMailSender mailSender;
OtpVerificationRepository otpRepository;
UserRepository userRepository;
+
+ UserEventProducer userEventProducer;
@Value("${app.otp.expiry-minutes}")
@NonFinal
@@ -139,6 +142,7 @@ public void verifyOtp(OtpVerificationRequest request) {
user.setEnabled(true);
userRepository.save(user);
+ userEventProducer.publishUpdatedUserEvent(user);
otpRepository.save(otp);
}
diff --git a/identity-service/src/main/java/com/codecampus/identity/service/kafka/UserEventProducer.java b/identity-service/src/main/java/com/codecampus/identity/service/kafka/UserEventProducer.java
new file mode 100644
index 00000000..f6d8d637
--- /dev/null
+++ b/identity-service/src/main/java/com/codecampus/identity/service/kafka/UserEventProducer.java
@@ -0,0 +1,102 @@
+package com.codecampus.identity.service.kafka;
+
+import com.codecampus.identity.entity.account.User;
+import com.codecampus.identity.mapper.kafka.UserPayloadMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import events.user.UserEvent;
+import events.user.UserRegisteredEvent;
+import events.user.data.UserProfileCreationPayload;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public class UserEventProducer {
+
+ KafkaTemplate kafkaTemplate;
+ ObjectMapper objectMapper;
+ UserPayloadMapper userPayloadMapper;
+
+ @NonFinal
+ @Value("${app.event.user-registrations}")
+ String USER_REGISTRATIONS_TOPIC;
+ @Value("${app.event.user-events}")
+ @NonFinal
+ String USER_EVENTS_TOPIC;
+
+ public void publishCreatedUserEvent(
+ User user) {
+ publishEvent(UserEvent.Type.CREATED, user);
+ }
+
+ public void publishUpdatedUserEvent(User user) {
+ publishEvent(UserEvent.Type.UPDATED, user);
+ }
+
+ public void publishDeletedUserEvent(User user) {
+ publishEvent(UserEvent.Type.DELETED, user);
+ }
+
+ public void publishRestoredUserEvent(User user) {
+ publishEvent(UserEvent.Type.RESTORED, user);
+ }
+
+ void publishEvent(
+ UserEvent.Type type,
+ User user) {
+ UserEvent userEvent = UserEvent.builder()
+ .type(type)
+ .id(user.getId())
+ .payload(type == UserEvent.Type.DELETED ? null
+ : userPayloadMapper.toUserPayloadFromUser(
+ user))
+ .build();
+
+ sendEvent(USER_EVENTS_TOPIC,
+ user.getId(),
+ userEvent
+ );
+ }
+
+ public void publishRegisteredUserEvent(
+ User user,
+ UserProfileCreationPayload profilePayload) {
+ UserRegisteredEvent userRegisteredEvent = UserRegisteredEvent.builder()
+ .id(user.getId())
+ .user(userPayloadMapper.toUserPayloadFromUser(user))
+ .profile(profilePayload)
+ .build();
+
+ sendEvent(USER_REGISTRATIONS_TOPIC,
+ user.getId(),
+ userRegisteredEvent
+ );
+ }
+
+ private void sendEvent(
+ String topic,
+ String key,
+ Object event) {
+ try {
+ String jsonObject = objectMapper.writeValueAsString(
+ event);
+
+ kafkaTemplate.send(
+ topic,
+ key,
+ jsonObject);
+ } catch (JsonProcessingException exception) {
+ log.error("[Kafka] Serialize thất bại", exception);
+ throw new RuntimeException(exception);
+ }
+ }
+}
diff --git a/identity-service/src/main/resources/application.yml b/identity-service/src/main/resources/application.yml
index da5123ed..63087d98 100644
--- a/identity-service/src/main/resources/application.yml
+++ b/identity-service/src/main/resources/application.yml
@@ -44,6 +44,11 @@ spring:
auth: true
starttls:
enable: true
+ kafka:
+ bootstrap-servers: localhost:9094
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.apache.kafka.common.serialization.StringSerializer
app:
services:
@@ -58,4 +63,7 @@ app:
client-id: "839020123858-qnan968uvj0u9d5h6bq5cd5ulls9h7dk.apps.googleusercontent.com"
client-secret: "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"
redirect-uri: "http://localhost:4200/auth/identity/authenticate"
+ event:
+ user-events: "user-events"
+ user-registrations: "user-registrations"
diff --git a/profile-service/pom.xml b/profile-service/pom.xml
index 937ef10a..62cda186 100644
--- a/profile-service/pom.xml
+++ b/profile-service/pom.xml
@@ -178,6 +178,12 @@
spring-security-test
test
+
+
+ com.codecampus
+ common-events
+ ${project.version}
+
diff --git a/profile-service/src/main/java/com/codecampus/profile/config/kafka/KafkaConsumerConfig.java b/profile-service/src/main/java/com/codecampus/profile/config/kafka/KafkaConsumerConfig.java
new file mode 100644
index 00000000..34bb6dc7
--- /dev/null
+++ b/profile-service/src/main/java/com/codecampus/profile/config/kafka/KafkaConsumerConfig.java
@@ -0,0 +1,59 @@
+package com.codecampus.profile.config.kafka;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import org.springframework.kafka.listener.ContainerProperties;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@EnableKafka
+@RequiredArgsConstructor
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public class KafkaConsumerConfig {
+
+ Environment env;
+
+ @Bean
+ public ConsumerFactory consumerFactory() {
+
+ Map props = new HashMap<>();
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
+ env.getProperty("spring.kafka.bootstrap-servers",
+ "localhost:9092"));
+ props.put(ConsumerConfig.GROUP_ID_CONFIG,
+ env.getProperty("spring.kafka.consumer.group-id",
+ "search-service"));
+ props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,
+ env.getProperty("spring.kafka.consumer.auto-offset-reset",
+ "earliest"));
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
+ StringDeserializer.class);
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
+ StringDeserializer.class);
+
+ return new DefaultKafkaConsumerFactory<>(props);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties()
+ .setAckMode(ContainerProperties.AckMode.BATCH);
+ factory.setBatchListener(false);
+ return factory;
+ }
+}
diff --git a/profile-service/src/main/java/com/codecampus/profile/controller/InternalUserProfileController.java b/profile-service/src/main/java/com/codecampus/profile/controller/InternalUserProfileController.java
index 2d5567c6..848bd10d 100644
--- a/profile-service/src/main/java/com/codecampus/profile/controller/InternalUserProfileController.java
+++ b/profile-service/src/main/java/com/codecampus/profile/controller/InternalUserProfileController.java
@@ -16,6 +16,7 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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;
@@ -24,10 +25,11 @@
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@Builder
@Slf4j
+@RequestMapping("/internal")
public class InternalUserProfileController {
UserProfileService userProfileService;
- @PostMapping("/internal/user")
+ @PostMapping("/user")
ApiResponse internalCreateUserProfile(
@RequestBody UserProfileCreationRequest request) {
return ApiResponse.builder()
@@ -35,7 +37,7 @@ ApiResponse internalCreateUserProfile(
.build();
}
- @GetMapping("/internal/user/{userId}")
+ @GetMapping("/user/{userId}")
ApiResponse internalGetUserProfileByUserId(
@PathVariable("userId") String userId) {
return ApiResponse.builder()
@@ -43,7 +45,7 @@ ApiResponse internalGetUserProfileByUserId(
.build();
}
- @PatchMapping("/internal/user/{userId}")
+ @PatchMapping("/user/{userId}")
ApiResponse internalUpdateProfileByUserId(
@PathVariable String userId,
@RequestBody UserProfileUpdateRequest request) {
@@ -52,7 +54,7 @@ ApiResponse internalUpdateProfileByUserId(
.build();
}
- @DeleteMapping("/internal/user/{userId}")
+ @DeleteMapping("/user/{userId}")
ApiResponse internalSoftDeleteByUserId(
@PathVariable String userId,
@RequestParam(required = false) String deletedBy) {
@@ -62,7 +64,7 @@ ApiResponse internalSoftDeleteByUserId(
.build();
}
- @PatchMapping("/internal/user/{userId}/restore")
+ @PatchMapping("/user/{userId}/restore")
ApiResponse restoreByUserId(
@PathVariable String userId) {
userProfileService.restoreByUserId(userId);
diff --git a/profile-service/src/main/java/com/codecampus/profile/dto/response/UserProfileResponse.java b/profile-service/src/main/java/com/codecampus/profile/dto/response/UserProfileResponse.java
index 95ffc64a..86485f84 100644
--- a/profile-service/src/main/java/com/codecampus/profile/dto/response/UserProfileResponse.java
+++ b/profile-service/src/main/java/com/codecampus/profile/dto/response/UserProfileResponse.java
@@ -16,6 +16,11 @@
public class UserProfileResponse {
String id;
String userId;
+
+ String username;
+ String email;
+ boolean active;
+
String firstName;
String lastName;
String dob;
diff --git a/profile-service/src/main/java/com/codecampus/profile/entity/UserProfile.java b/profile-service/src/main/java/com/codecampus/profile/entity/UserProfile.java
index af1ea454..ccbd6d12 100644
--- a/profile-service/src/main/java/com/codecampus/profile/entity/UserProfile.java
+++ b/profile-service/src/main/java/com/codecampus/profile/entity/UserProfile.java
@@ -23,11 +23,9 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
-import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;
-import org.springframework.data.neo4j.core.support.UUIDStringGenerator;
import java.time.Instant;
import java.util.HashSet;
@@ -42,11 +40,13 @@
@Node("User")
public class UserProfile {
@Id
- @GeneratedValue(generatorClass = UUIDStringGenerator.class)
- String id;
-
String userId;
+ String username;
+ String email;
+ @Builder.Default
+ Boolean active = true;
+
String firstName;
String lastName;
Instant dob;
diff --git a/profile-service/src/main/java/com/codecampus/profile/mapper/UserProfileMapper.java b/profile-service/src/main/java/com/codecampus/profile/mapper/UserProfileMapper.java
index 228455f3..ef6fa166 100644
--- a/profile-service/src/main/java/com/codecampus/profile/mapper/UserProfileMapper.java
+++ b/profile-service/src/main/java/com/codecampus/profile/mapper/UserProfileMapper.java
@@ -22,14 +22,16 @@ public interface UserProfileMapper {
target = "dob",
qualifiedByName = "DdMmYyyyToInstant"
)
- UserProfile toUserProfile(UserProfileCreationRequest request);
+ UserProfile toUserProfileFromUserProfileCreationRequest(
+ UserProfileCreationRequest request);
@Mapping(
source = "dob",
target = "dob",
qualifiedByName = "instantToDdMmYyyyUTC"
)
- UserProfileResponse toUserProfileResponse(UserProfile userProfile);
+ UserProfileResponse toUserProfileResponseFromUserProfile(
+ UserProfile userProfile);
@BeanMapping(nullValuePropertyMappingStrategy = IGNORE)
@Mapping(
@@ -37,7 +39,7 @@ public interface UserProfileMapper {
target = "dob",
qualifiedByName = "DdMmYyyyToInstant"
)
- void updateUserProfile(
+ void updateUserProfileUpdateRequestToUserProfile(
@MappingTarget UserProfile userProfile,
UserProfileUpdateRequest request
);
diff --git a/profile-service/src/main/java/com/codecampus/profile/repository/UserProfileRepository.java b/profile-service/src/main/java/com/codecampus/profile/repository/UserProfileRepository.java
index 0dd62bd1..63b881c7 100644
--- a/profile-service/src/main/java/com/codecampus/profile/repository/UserProfileRepository.java
+++ b/profile-service/src/main/java/com/codecampus/profile/repository/UserProfileRepository.java
@@ -30,225 +30,312 @@ public interface UserProfileRepository
Optional findByUserId(String userId);
@Query("""
- MATCH (u:User {userId:$userId})
- WHERE u.deletedAt IS NULL
- RETURN u
- LIMIT 1
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ RETURN u
+ LIMIT 1
""")
- Optional findActiveByUserId(
- String userId);
+ Optional findActiveByUserId(String userId);
boolean existsByUserId(String userId);
- // Exercise
+ /* ========================= EXERCISE ========================= */
+
@Query(value = """
- MATCH (u:User {userId:$userId})-[completed:COMPLETED_EXERCISE]->(e:Exercise)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[completed:COMPLETED_EXERCISE]->(e:Exercise)
RETURN completed, e
ORDER BY completed.completedAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[completed:COMPLETED_EXERCISE]->(:Exercise)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[completed:COMPLETED_EXERCISE]->(:Exercise)
RETURN count(completed)
""")
Page findCompletedExercises(
- String userId, Pageable pageable);
-
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (u:User {userId:$userId})-[saved:SAVED_EXERCISE]->(e:Exercise)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[saved:SAVED_EXERCISE]->(e:Exercise)
RETURN saved, e
ORDER BY saved.saveAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[saved:SAVED_EXERCISE]->(:Exercise)
- RETURN count(saved)
- """
- )
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[saved:SAVED_EXERCISE]->(:Exercise)
+ RETURN count(saved)
+ """)
Page findSavedExercises(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (u:User {userId:$userId})-[created:CREATED_EXERCISE]->(e:Exercise)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[created:CREATED_EXERCISE]->(e:Exercise)
RETURN created, e
ORDER BY created.id DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[created:CREATED_EXERCISE]->(:Exercise)
- RETURN count(created)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[created:CREATED_EXERCISE]->(:Exercise)
+ RETURN count(created)
""")
Page findCreatedExercises(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
+
+ /* ========================= CONTEST ========================= */
@Query(value = """
- MATCH (u:User {userId:$userId})-[cs:CONTEST_STATUS]->(c:Contest)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[cs:CONTEST_STATUS]->(c:Contest)
RETURN cs, c
ORDER BY cs.updatedAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[cs:CONTEST_STATUS]->(:Contest)
- RETURN count(cs)
- """
- )
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[cs:CONTEST_STATUS]->(:Contest)
+ RETURN count(cs)
+ """)
Page findContestStatuses(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
+
+ /* ========================= POST ========================= */
- // Post
@Query(value = """
- MATCH (u:User {userId:$userId})-[sp:SAVED_POST]->(p:Post)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[sp:SAVED_POST]->(p:Post)
RETURN sp, p
ORDER BY sp.saveAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[sp:SAVED_POST]->(:Post)
- RETURN count(sp)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[sp:SAVED_POST]->(:Post)
+ RETURN count(sp)
""")
Page findSavedPosts(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (u:User {userId:$userId})-[r:REACTION]->(p:Post)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[r:REACTION]->(p:Post)
RETURN r, p
ORDER BY r.at DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[r:REACTION]->(:Post)
- RETURN count(r)
- """
- )
- Page findReactions(String userId, Pageable pageable);
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[r:REACTION]->(:Post)
+ RETURN count(r)
+ """)
+ Page findReactions(
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (u:User {userId:$userId})-[rp:REPORTED_POST]->(p:Post)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[rp:REPORTED_POST]->(p:Post)
RETURN rp, p
ORDER BY rp.reportedAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[rp:REPORTED_POST]->(:Post)
- RETURN count(rp)
- """
- )
- Page findReportedPosts(String userId, Pageable pageable);
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[rp:REPORTED_POST]->(:Post)
+ RETURN count(rp)
+ """)
+ Page findReportedPosts(
+ String userId,
+ Pageable pageable);
+
+ /* ========================= ACTIVITY TIME ========================= */
- // Activity Time
@Query(value = """
- MATCH (u:User {userId:$userId})-[:HAS_ACTIVITY]->(a:ActivityWeek)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[:HAS_ACTIVITY]->(a:ActivityWeek)
RETURN a
ORDER BY a.weekStart DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[:HAS_ACTIVITY]->(a:ActivityWeek)
- RETURN count(a)
- """
- )
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[:HAS_ACTIVITY]->(a:ActivityWeek)
+ RETURN count(a)
+ """)
Page findActivityWeek(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
+
+ /* ========================= FOLLOW / BLOCK ========================= */
- // Follow / Block
@Query(value = """
- MATCH (me:User {userId:$userId})-[f:FOLLOWS]->(target:User)
+ MATCH (me:User {userId:$userId})
+ WHERE me.deletedAt IS NULL
+ MATCH (me)-[f:FOLLOWS]->(target:User)
+ WHERE target.deletedAt IS NULL
RETURN f, target
ORDER BY f.since DESC
""",
countQuery = """
- MATCH (me:User {userId:$userId})-[f:FOLLOWS]->(:User)
- RETURN count(f)
- """
- )
- Page findFollowings(String userId, Pageable pageable);
+ MATCH (me:User {userId:$userId})
+ WHERE me.deletedAt IS NULL
+ MATCH (me)-[f:FOLLOWS]->(target:User)
+ WHERE target.deletedAt IS NULL
+ RETURN count(f)
+ """)
+ Page findFollowings(
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (src:User)-[f:FOLLOWS]->(me:User {userId:$userId})
+ MATCH (me:User {userId:$userId})
+ WHERE me.deletedAt IS NULL
+ MATCH (src:User)-[f:FOLLOWS]->(me)
+ WHERE src.deletedAt IS NULL
RETURN f, src
ORDER BY f.since DESC
""",
countQuery = """
- MATCH (:User)-[f:FOLLOWS]->(me:User {userId:$userId})
- RETURN count(f)
- """
- )
- Page findFollowers(String userId, Pageable pageable);
+ MATCH (me:User {userId:$userId})
+ WHERE me.deletedAt IS NULL
+ MATCH (src:User)-[f:FOLLOWS]->(me)
+ WHERE src.deletedAt IS NULL
+ RETURN count(f)
+ """)
+ Page findFollowers(
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (me:User {userId:$userId})-[b:BLOCKS]->(target:User)
+ MATCH (me:User {userId:$userId})
+ WHERE me.deletedAt IS NULL
+ MATCH (me)-[b:BLOCKS]->(target:User)
+ WHERE target.deletedAt IS NULL
RETURN b, target
ORDER BY b.since DESC
""",
countQuery = """
- MATCH (me:User {userId:$userId})-[b:BLOCKS]->(:User)
- RETURN count(b)
- """
- )
- Page findBlocked(String userId, Pageable pageable);
+ MATCH (me:User {userId:$userId})
+ WHERE me.deletedAt IS NULL
+ MATCH (me)-[b:BLOCKS]->(target:User)
+ WHERE target.deletedAt IS NULL
+ RETURN count(b)
+ """)
+ Page findBlocked(
+ String userId,
+ Pageable pageable);
+
+ /* ========================= ORG ========================= */
- // Org
@Query(value = """
- MATCH (u:User {userId:$userId})-[m:MEMBER_ORG]->(o:Organization)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[m:MEMBER_ORG]->(o:Organization)
RETURN m, o
ORDER BY m.joinAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[m:MEMBER_ORG]->(:Organization)
- RETURN count(m)
- """
- )
- Page findMemberOrgs(String userId, Pageable pageable);
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[m:MEMBER_ORG]->(:Organization)
+ RETURN count(m)
+ """)
+ Page findMemberOrgs(
+ String userId,
+ Pageable pageable);
@Query(value = """
- MATCH (u:User {userId:$userId})-[m:MEMBER_ORG]->(o:Organization)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[m:MEMBER_ORG]->(o:Organization)
WHERE m.memberRole = $role
RETURN m, o
ORDER BY m.joinAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[m:MEMBER_ORG]->(:Organization)
- WHERE m.memberRole = $role
- RETURN count(m)
- """
- )
- Page findMemberOrgsByRole(String userId, String role,
- Pageable p);
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[m:MEMBER_ORG]->(:Organization)
+ WHERE m.memberRole = $role
+ RETURN count(m)
+ """)
+ Page findMemberOrgsByRole(
+ String userId,
+ String role,
+ Pageable p);
@Query(value = """
- MATCH (u:User {userId:$userId})-[c:CREATED_ORG]->(o:Organization)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[c:CREATED_ORG]->(o:Organization)
RETURN c, o
ORDER BY c.createdAt DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[c:CREATED_ORG]->(:Organization)
- RETURN count(c)
- """
- )
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[c:CREATED_ORG]->(:Organization)
+ RETURN count(c)
+ """)
Page findCreatedOrgs(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
+
+ /* ========================= PACKAGE ========================= */
- // Package
@Query(value = """
- MATCH (u:User {userId:$userId})-[s:SUBSCRIBED_TO]->(p:Package)
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[s:SUBSCRIBED_TO]->(p:Package)
RETURN s, p
ORDER BY s.start DESC
""",
countQuery = """
- MATCH (u:User {userId:$userId})-[s:SUBSCRIBED_TO]->(:Package)
- RETURN count(s)
- """
- )
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[s:SUBSCRIBED_TO]->(:Package)
+ RETURN count(s)
+ """)
Page findSubscriptions(
- String userId, Pageable pageable);
+ String userId,
+ Pageable pageable);
+
+ /* ========================= RESOURCE ========================= */
- // Resource
- @Query(
- value = """
- MATCH (u:User {userId:$userId})-[sr:SAVED_RESOURCE]->(f:FileResource)
+ @Query(value = """
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[sr:SAVED_RESOURCE]->(f:FileResource)
+ WHERE f.type = $type
+ RETURN sr, f
+ ORDER BY sr.saveAt DESC
+ """,
+ countQuery = """
+ MATCH (u:User {userId:$userId})
+ WHERE u.deletedAt IS NULL
+ MATCH (u)-[sr:SAVED_RESOURCE]->(f:FileResource)
WHERE f.type = $type
- RETURN sr, f
- ORDER BY sr.saveAt DESC
- """,
- countQuery = """
- MATCH (u:User {userId:$userId})-[sr:SAVED_RESOURCE]->(f:FileResource)
- WHERE f.type = $type
- RETURN count(sr)
- """
- )
+ RETURN count(sr)
+ """)
Page findSavedResourcesByType(
- String userId, String type, Pageable pageable);
+ String userId,
+ String type,
+ Pageable pageable);
}
diff --git a/profile-service/src/main/java/com/codecampus/profile/service/FamilyService.java b/profile-service/src/main/java/com/codecampus/profile/service/FamilyService.java
index 040ec0f0..c274be0d 100644
--- a/profile-service/src/main/java/com/codecampus/profile/service/FamilyService.java
+++ b/profile-service/src/main/java/com/codecampus/profile/service/FamilyService.java
@@ -60,7 +60,8 @@ public void addChild(String parentId, String childId) {
*/
public void removeChild(String parentId, String childId) {
UserProfile parent = userProfileService.getUserProfile(parentId);
- parent.getChildren().removeIf(child -> childId.equals(child.getId()));
+ parent.getChildren()
+ .removeIf(child -> childId.equals(child.getUserId()));
userProfileRepository.save(parent);
}
diff --git a/profile-service/src/main/java/com/codecampus/profile/service/UserProfileService.java b/profile-service/src/main/java/com/codecampus/profile/service/UserProfileService.java
index 84bbed35..70b319d9 100644
--- a/profile-service/src/main/java/com/codecampus/profile/service/UserProfileService.java
+++ b/profile-service/src/main/java/com/codecampus/profile/service/UserProfileService.java
@@ -65,11 +65,14 @@ public UserProfileResponse createUserProfile(
UserProfileCreationRequest request) {
authenticationHelper.checkExistsUserid(request.getUserId());
- UserProfile userProfile = userProfileMapper.toUserProfile(request);
+ UserProfile userProfile =
+ userProfileMapper.toUserProfileFromUserProfileCreationRequest(
+ request);
userProfile.setCreatedAt(Instant.now());
userProfile = userProfileRepository.save(userProfile);
- return userProfileMapper.toUserProfileResponse(userProfile);
+ return userProfileMapper.toUserProfileResponseFromUserProfile(
+ userProfile);
}
/**
@@ -82,7 +85,7 @@ public UserProfileResponse createUserProfile(
public UserProfileResponse getUserProfileByUserId(String userId) {
return userProfileRepository
.findByUserId(userId)
- .map(userProfileMapper::toUserProfileResponse)
+ .map(userProfileMapper::toUserProfileResponseFromUserProfile)
.orElseThrow(
() -> new AppException(ErrorCode.USER_NOT_FOUND)
);
@@ -98,7 +101,7 @@ public UserProfileResponse getUserProfileByUserId(String userId) {
public UserProfileResponse getUserProfileById(String id) {
return userProfileRepository
.findById(id)
- .map(userProfileMapper::toUserProfileResponse)
+ .map(userProfileMapper::toUserProfileResponseFromUserProfile)
.orElseThrow(
() -> new AppException(ErrorCode.USER_NOT_FOUND)
);
@@ -117,7 +120,7 @@ public PageResponse getAllUserProfiles(int page,
Pageable pageable = PageRequest.of(page - 1, size);
var pageData = userProfileRepository
.findAll(pageable)
- .map(userProfileMapper::toUserProfileResponse);
+ .map(userProfileMapper::toUserProfileResponseFromUserProfile);
return toPageResponse(pageData, page);
}
@@ -177,8 +180,9 @@ public void updateMyUserProfile(
UserProfile profile = getUserProfile();
- userProfileMapper.updateUserProfile(profile, request);
- userProfileMapper.toUserProfileResponse(
+ userProfileMapper.updateUserProfileUpdateRequestToUserProfile(profile,
+ request);
+ userProfileMapper.toUserProfileResponseFromUserProfile(
userProfileRepository.save(profile)
);
}
@@ -189,7 +193,8 @@ public void updateUserProfileById(
UserProfile profile = userProfileRepository
.findActiveByUserId(userId)
.orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND));
- userProfileMapper.updateUserProfile(profile, request);
+ userProfileMapper.updateUserProfileUpdateRequestToUserProfile(profile,
+ request);
userProfileRepository.save(profile);
}
diff --git a/profile-service/src/main/java/com/codecampus/profile/service/kafka/UserEventListener.java b/profile-service/src/main/java/com/codecampus/profile/service/kafka/UserEventListener.java
new file mode 100644
index 00000000..f29f7e68
--- /dev/null
+++ b/profile-service/src/main/java/com/codecampus/profile/service/kafka/UserEventListener.java
@@ -0,0 +1,77 @@
+package com.codecampus.profile.service.kafka;
+
+import com.codecampus.profile.entity.UserProfile;
+import com.codecampus.profile.repository.UserProfileRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import events.user.UserEvent;
+import events.user.data.UserPayload;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public class UserEventListener {
+
+ UserProfileRepository userProfileRepository;
+ ObjectMapper objectMapper;
+
+ @KafkaListener(topics = "${app.event.user-events}",
+ groupId = "profile-service"
+ )
+ public void onMessageUser(String raw) {
+ try {
+ UserEvent userEvent = objectMapper.readValue(
+ raw,
+ UserEvent.class);
+
+ switch (userEvent.getType()) {
+ case CREATED, UPDATED, RESTORED ->
+ upsert(userEvent.getPayload());
+ case DELETED -> softDelete(userEvent.getId());
+ }
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void upsert(UserPayload userPayload) {
+ Optional userProfile =
+ userProfileRepository.findByUserId(
+ userPayload.getUserId());
+
+ UserProfile profile = userProfile.orElseGet(() -> UserProfile.builder()
+ .userId(userPayload.getUserId())
+ .createdAt(Instant.now())
+ .build());
+
+ profile.setUsername(userPayload.getUsername());
+ profile.setEmail(userPayload.getEmail());
+ profile.setActive(userPayload.isActive());
+ if (profile.getDeletedAt() != null && userPayload.isActive()) {
+ // nếu muốn auto-restore khi nhận RESTORED/UPDATED active=true
+ profile.setDeletedAt(null);
+ profile.setDeletedBy(null);
+ }
+ userProfileRepository.save(profile);
+ }
+
+ private void softDelete(String userId) {
+ userProfileRepository.findByUserId(userId).ifPresent(p -> {
+ if (p.getDeletedAt() == null) {
+ p.setDeletedAt(Instant.now());
+ p.setDeletedBy("identity-service");
+ userProfileRepository.save(p);
+ }
+ });
+ }
+}
diff --git a/profile-service/src/main/java/com/codecampus/profile/service/kafka/UserRegistrationListener.java b/profile-service/src/main/java/com/codecampus/profile/service/kafka/UserRegistrationListener.java
new file mode 100644
index 00000000..2a4bc89c
--- /dev/null
+++ b/profile-service/src/main/java/com/codecampus/profile/service/kafka/UserRegistrationListener.java
@@ -0,0 +1,78 @@
+package com.codecampus.profile.service.kafka;
+
+import com.codecampus.profile.entity.UserProfile;
+import com.codecampus.profile.repository.UserProfileRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import events.user.UserRegisteredEvent;
+import events.user.data.UserPayload;
+import events.user.data.UserProfileCreationPayload;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public class UserRegistrationListener {
+
+ UserProfileRepository userProfileRepository;
+ ObjectMapper objectMapper;
+
+ @KafkaListener(
+ topics = "${app.event.user-registrations}",
+ groupId = "profile-service"
+ )
+ public void onRegistrationUser(String raw) {
+ try {
+ UserRegisteredEvent event =
+ objectMapper.readValue(raw, UserRegisteredEvent.class);
+ upsertFromRegistration(event);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void upsertFromRegistration(UserRegisteredEvent event) {
+ UserPayload u = event.getUser();
+ UserProfileCreationPayload p = event.getProfile();
+
+ UserProfile profile = userProfileRepository.findByUserId(u.getUserId())
+ .orElseGet(() -> UserProfile.builder()
+ .userId(u.getUserId())
+ .createdAt(u.getCreatedAt() != null ? u.getCreatedAt() :
+ Instant.now())
+ .build());
+
+ // fields từ UserPayload
+ profile.setUsername(u.getUsername());
+ profile.setEmail(u.getEmail());
+ profile.setActive(u.isActive());
+
+ // fields profile chi tiết
+ profile.setFirstName(p.getFirstName());
+ profile.setLastName(p.getLastName());
+ profile.setDob(p.getDob());
+ profile.setBio(p.getBio());
+ profile.setGender(p.getGender());
+ profile.setDisplayName(p.getDisplayName());
+ profile.setEducation(p.getEducation());
+ profile.setLinks(p.getLinks());
+ profile.setCity(p.getCity());
+
+ // nếu đang bị soft-delete mà nhận được registration (enabled=true) -> auto-restore
+ if (profile.getDeletedAt() != null &&
+ Boolean.TRUE.equals(profile.getActive())) {
+ profile.setDeletedAt(null);
+ profile.setDeletedBy(null);
+ }
+
+ userProfileRepository.save(profile);
+ }
+}
diff --git a/profile-service/src/main/resources/application-docker.yml b/profile-service/src/main/resources/application-docker.yml
index 22999e6a..fbd676bc 100644
--- a/profile-service/src/main/resources/application-docker.yml
+++ b/profile-service/src/main/resources/application-docker.yml
@@ -9,4 +9,10 @@ spring:
authentication:
username: ${NEO4J_USERNAME}
password: ${NEO4J_PASSWORD}
+ kafka:
+ bootstrap-servers: kafka:9092
+
+app:
+ services:
+ file: http://file-service:8082/file
diff --git a/profile-service/src/main/resources/application.yml b/profile-service/src/main/resources/application.yml
index def2e05e..f06c74c7 100644
--- a/profile-service/src/main/resources/application.yml
+++ b/profile-service/src/main/resources/application.yml
@@ -21,8 +21,18 @@ spring:
username: neo4j
password: dinhanst2832004
schema-action: create
+ kafka:
+ bootstrap-servers: localhost:9094
+ consumer:
+ group-id: profile-service
+ auto-offset-reset: earliest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ properties:
+ spring.json.trusted.packages: "*"
app:
services:
file: http://localhost:8082/file
-
+ event:
+ user-events: "user-events"
diff --git a/search-service/src/main/java/com/codecampus/search/service/ExerciseIndexer.java b/search-service/src/main/java/com/codecampus/search/service/kafka/ExerciseEventListener.java
similarity index 50%
rename from search-service/src/main/java/com/codecampus/search/service/ExerciseIndexer.java
rename to search-service/src/main/java/com/codecampus/search/service/kafka/ExerciseEventListener.java
index e05364ff..92198292 100644
--- a/search-service/src/main/java/com/codecampus/search/service/ExerciseIndexer.java
+++ b/search-service/src/main/java/com/codecampus/search/service/kafka/ExerciseEventListener.java
@@ -1,7 +1,8 @@
-package com.codecampus.search.service;
+package com.codecampus.search.service.kafka;
import com.codecampus.search.mapper.ExerciseMapper;
import com.codecampus.search.repository.ExerciseDocumentRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import events.exercise.ExerciseEvent;
import lombok.AccessLevel;
@@ -11,13 +12,11 @@
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;
-import java.io.IOException;
-
@Service
@RequiredArgsConstructor
@Slf4j
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
-public class ExerciseIndexer {
+public class ExerciseEventListener {
ExerciseDocumentRepository exerciseDocumentRepository;
@@ -27,15 +26,19 @@ public class ExerciseIndexer {
@KafkaListener(
topics = "exercise-events",
groupId = "search-service")
- public void onMessageExercise(String raw) throws IOException {
- ExerciseEvent exerciseEvent =
- objectMapper.readValue(raw, ExerciseEvent.class);
- switch (exerciseEvent.getType()) {
- case CREATED, UPDATED -> exerciseDocumentRepository.save(
- exerciseMapper.toExerciseDocumentFromExercisePayload(
- exerciseEvent.getPayload()));
- case DELETED -> exerciseDocumentRepository.deleteById(
- exerciseEvent.getId());
+ public void onMessageExercise(String raw) {
+ try {
+ ExerciseEvent exerciseEvent =
+ objectMapper.readValue(raw, ExerciseEvent.class);
+ switch (exerciseEvent.getType()) {
+ case CREATED, UPDATED -> exerciseDocumentRepository.save(
+ exerciseMapper.toExerciseDocumentFromExercisePayload(
+ exerciseEvent.getPayload()));
+ case DELETED -> exerciseDocumentRepository.deleteById(
+ exerciseEvent.getId());
+ }
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
}
}
}
diff --git a/submission-service/src/main/java/com/codecampus/submission/service/kafka/ExerciseEventProducer.java b/submission-service/src/main/java/com/codecampus/submission/service/kafka/ExerciseEventProducer.java
index e781c7b6..f61ca8d6 100644
--- a/submission-service/src/main/java/com/codecampus/submission/service/kafka/ExerciseEventProducer.java
+++ b/submission-service/src/main/java/com/codecampus/submission/service/kafka/ExerciseEventProducer.java
@@ -22,7 +22,7 @@ public class ExerciseEventProducer {
@Value("${app.event.exercise-events}")
@NonFinal
- static String EXERCISE_EVENTS_TOPIC = "exercise-events";
+ static String EXERCISE_EVENTS_TOPIC;
KafkaTemplate kafkaTemplate;
ObjectMapper objectMapper;