diff --git a/framework/src/main/java/io/theurl/framework/configure/MediatorConfiguration.java b/framework/src/main/java/io/theurl/framework/configure/MediatorConfiguration.java index 40199da..a573727 100644 --- a/framework/src/main/java/io/theurl/framework/configure/MediatorConfiguration.java +++ b/framework/src/main/java/io/theurl/framework/configure/MediatorConfiguration.java @@ -24,9 +24,7 @@ public Mediator mediator(ObjectProvider handlers, ObjectProvider handlers.stream()) .use(() -> middlewares.stream()) .use(() -> validators.stream()) - .use(event -> CompletableFuture.runAsync(() -> { - publisher.publishEvent(event); - }, taskExecutor().getThreadPoolExecutor())); + .use(event -> CompletableFuture.runAsync(() -> publisher.publishEvent(event), taskExecutor().getThreadPoolExecutor())); } @Bean(name = "taskExecutor") diff --git a/identity/src/main/java/io/theurl/identity/application/contract/AuthApplicationService.java b/identity/src/main/java/io/theurl/identity/application/contract/AuthApplicationService.java index 2ad8f9e..562a723 100644 --- a/identity/src/main/java/io/theurl/identity/application/contract/AuthApplicationService.java +++ b/identity/src/main/java/io/theurl/identity/application/contract/AuthApplicationService.java @@ -1,7 +1,7 @@ package io.theurl.identity.application.contract; -import io.theurl.identity.interfaces.dto.TokenGrantRequestDto; -import io.theurl.identity.interfaces.dto.TokenGrantResponseDto; +import io.theurl.identity.application.dto.TokenGrantRequestDto; +import io.theurl.identity.application.dto.TokenGrantResponseDto; import java.util.concurrent.CompletableFuture; diff --git a/identity/src/main/java/io/theurl/identity/application/contract/UserApplicationService.java b/identity/src/main/java/io/theurl/identity/application/contract/UserApplicationService.java index 43115d4..3e42e80 100644 --- a/identity/src/main/java/io/theurl/identity/application/contract/UserApplicationService.java +++ b/identity/src/main/java/io/theurl/identity/application/contract/UserApplicationService.java @@ -1,6 +1,34 @@ package io.theurl.identity.application.contract; import io.theurl.framework.application.ApplicationService; +import io.theurl.identity.application.dto.UserCreateRequestDto; +import io.theurl.identity.application.dto.UserProfileResponseDto; + +import java.util.concurrent.CompletableFuture; public interface UserApplicationService extends ApplicationService { + + /** + * Create a new user asynchronously. + * + * @param user The user creation request data. + * @return A CompletableFuture representing the asynchronous operation. + */ + CompletableFuture createAsync(UserCreateRequestDto user); + + /** + * Get the profile of the currently authenticated user asynchronously. + * + * @return A CompletableFuture containing the user's profile data. + */ + CompletableFuture getProfileAsync(); + + /** + * Change the password of the currently authenticated user asynchronously. + * + * @param oldPassword The current password of the user. + * @param newPassword The new password to be set for the user. + * @return A CompletableFuture representing the asynchronous operation. + */ + CompletableFuture changePasswordAsync(String oldPassword, String newPassword); } diff --git a/identity/src/main/java/io/theurl/identity/interfaces/dto/TokenGrantRequestDto.java b/identity/src/main/java/io/theurl/identity/application/dto/TokenGrantRequestDto.java similarity index 91% rename from identity/src/main/java/io/theurl/identity/interfaces/dto/TokenGrantRequestDto.java rename to identity/src/main/java/io/theurl/identity/application/dto/TokenGrantRequestDto.java index 7d33c44..a33e4b5 100644 --- a/identity/src/main/java/io/theurl/identity/interfaces/dto/TokenGrantRequestDto.java +++ b/identity/src/main/java/io/theurl/identity/application/dto/TokenGrantRequestDto.java @@ -1,4 +1,4 @@ -package io.theurl.identity.interfaces.dto; +package io.theurl.identity.application.dto; /** * DTO for token grant request diff --git a/identity/src/main/java/io/theurl/identity/interfaces/dto/TokenGrantResponseDto.java b/identity/src/main/java/io/theurl/identity/application/dto/TokenGrantResponseDto.java similarity index 94% rename from identity/src/main/java/io/theurl/identity/interfaces/dto/TokenGrantResponseDto.java rename to identity/src/main/java/io/theurl/identity/application/dto/TokenGrantResponseDto.java index 8a1c4df..80f3641 100644 --- a/identity/src/main/java/io/theurl/identity/interfaces/dto/TokenGrantResponseDto.java +++ b/identity/src/main/java/io/theurl/identity/application/dto/TokenGrantResponseDto.java @@ -1,4 +1,4 @@ -package io.theurl.identity.interfaces.dto; +package io.theurl.identity.application.dto; /** * DTO for token grant response diff --git a/identity/src/main/java/io/theurl/identity/application/dto/UserCreateRequestDto.java b/identity/src/main/java/io/theurl/identity/application/dto/UserCreateRequestDto.java new file mode 100644 index 0000000..0cb081d --- /dev/null +++ b/identity/src/main/java/io/theurl/identity/application/dto/UserCreateRequestDto.java @@ -0,0 +1,12 @@ +package io.theurl.identity.application.dto; + +import lombok.Data; + +@Data +public class UserCreateRequestDto { + private String username; + private String password; + private String nickname; + private String email; + private String phone; +} diff --git a/identity/src/main/java/io/theurl/identity/application/dto/UserPasswordChangeRequestDto.java b/identity/src/main/java/io/theurl/identity/application/dto/UserPasswordChangeRequestDto.java new file mode 100644 index 0000000..5be562f --- /dev/null +++ b/identity/src/main/java/io/theurl/identity/application/dto/UserPasswordChangeRequestDto.java @@ -0,0 +1,12 @@ +package io.theurl.identity.application.dto; + +import lombok.Data; + +/** + * The data transfer object for user password change request. + */ +@Data +public class UserPasswordChangeRequestDto { + private String oldPassword; + private String newPassword; +} diff --git a/identity/src/main/java/io/theurl/identity/application/dto/UserProfileResponseDto.java b/identity/src/main/java/io/theurl/identity/application/dto/UserProfileResponseDto.java new file mode 100644 index 0000000..2b4a0c7 --- /dev/null +++ b/identity/src/main/java/io/theurl/identity/application/dto/UserProfileResponseDto.java @@ -0,0 +1,4 @@ +package io.theurl.identity.application.dto; + +public class UserProfileResponseDto { +} diff --git a/identity/src/main/java/io/theurl/identity/application/handler/UserCreateCommandHandler.java b/identity/src/main/java/io/theurl/identity/application/handler/UserCreateCommandHandler.java index 2d1f0a6..307b409 100644 --- a/identity/src/main/java/io/theurl/identity/application/handler/UserCreateCommandHandler.java +++ b/identity/src/main/java/io/theurl/identity/application/handler/UserCreateCommandHandler.java @@ -32,15 +32,8 @@ public CompletableFuture handleAsync(UserCreateCommand message) { throw new IllegalArgumentException("User with the same username, email or phone already exists."); } - var user = User.create(message.getUsername()); + var user = User.create(message.getUsername(), message.getNickname(), message.getEmail(), message.getPhone()); user.setPassword(message.getPassword(), "init"); - user.setNickname(message.getNickname()); - if (message.getEmail() != null) { - user.setEmail(message.getEmail()); - } - if (message.getPhone() != null) { - user.setPhone(message.getPhone()); - } repository.save(user); return CompletableFuture.completedFuture(null); } diff --git a/identity/src/main/java/io/theurl/identity/application/implement/AuthApplicationServiceImpl.java b/identity/src/main/java/io/theurl/identity/application/implement/AuthApplicationServiceImpl.java index a07be80..d76d893 100644 --- a/identity/src/main/java/io/theurl/identity/application/implement/AuthApplicationServiceImpl.java +++ b/identity/src/main/java/io/theurl/identity/application/implement/AuthApplicationServiceImpl.java @@ -13,8 +13,8 @@ import io.theurl.identity.application.event.UserAuthSuccessEvent; import io.theurl.identity.external.ExternalAuthProvider; import io.theurl.identity.external.ExternalAuthResult; -import io.theurl.identity.interfaces.dto.TokenGrantRequestDto; -import io.theurl.identity.interfaces.dto.TokenGrantResponseDto; +import io.theurl.identity.application.dto.TokenGrantRequestDto; +import io.theurl.identity.application.dto.TokenGrantResponseDto; import io.theurl.identity.persistence.model.UserAuthInfo; import io.theurl.identity.persistence.query.OnetimePasswordDetailQuery; import io.theurl.identity.persistence.query.UserAuthInfoQuery; diff --git a/identity/src/main/java/io/theurl/identity/application/implement/UserApplicationServiceImpl.java b/identity/src/main/java/io/theurl/identity/application/implement/UserApplicationServiceImpl.java index a8d2e16..574851d 100644 --- a/identity/src/main/java/io/theurl/identity/application/implement/UserApplicationServiceImpl.java +++ b/identity/src/main/java/io/theurl/identity/application/implement/UserApplicationServiceImpl.java @@ -1,16 +1,65 @@ package io.theurl.identity.application.implement; import io.theurl.framework.application.BaseApplicationService; +import io.theurl.framework.security.CredentialIncorrectException; +import io.theurl.framework.utility.Cryptography; +import io.theurl.identity.application.command.UserCreateCommand; +import io.theurl.identity.application.command.UserPasswordChangeCommand; import io.theurl.identity.application.contract.UserApplicationService; +import io.theurl.identity.application.dto.UserCreateRequestDto; +import io.theurl.identity.application.dto.UserProfileResponseDto; +import io.theurl.identity.persistence.query.UserAuthInfoQuery; +import io.theurl.identity.persistence.query.UserDetailQuery; +import org.modelmapper.ModelMapper; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.springframework.web.context.annotation.RequestScope; +import java.util.concurrent.CompletableFuture; + @Service @RequestScope public class UserApplicationServiceImpl extends BaseApplicationService implements UserApplicationService { - public UserApplicationServiceImpl(ApplicationContext applicationContext) { + private final ModelMapper mapper; + + public UserApplicationServiceImpl(ApplicationContext applicationContext, ModelMapper mapper) { super(applicationContext); + this.mapper = mapper; + } + + @Override + public CompletableFuture createAsync(UserCreateRequestDto user) { + var command = mapper.map(user, UserCreateCommand.class); + return mediator.sendAsync(command); + } + + @Override + public CompletableFuture getProfileAsync() { + var query = new UserDetailQuery(1L); + return mediator.executeAsync(query) + .thenApply(userDetail -> mapper.map(userDetail, UserProfileResponseDto.class)); + } + + @Override + public CompletableFuture changePasswordAsync(String oldPassword, String newPassword) { + mediator.executeAsync(new UserAuthInfoQuery("id", "")) + .thenAccept(userDetail -> { + if (userDetail == null) { + throw new IllegalStateException("User not found."); + } + try { + var oldPasswordHash = Cryptography.AES.encrypt(oldPassword, userDetail.getPasswordSalt()); + if (!oldPasswordHash.equals(userDetail.getPasswordHash())) { + throw new CredentialIncorrectException("Old password is incorrect."); + } + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt old password", e); + } + }) + .join(); + + var command = new UserPasswordChangeCommand(1L, newPassword, "change"); + return mediator.sendAsync(command); } } diff --git a/identity/src/main/java/io/theurl/identity/domain/aggregate/User.java b/identity/src/main/java/io/theurl/identity/domain/aggregate/User.java index 4484ff3..02b95f8 100644 --- a/identity/src/main/java/io/theurl/identity/domain/aggregate/User.java +++ b/identity/src/main/java/io/theurl/identity/domain/aggregate/User.java @@ -36,11 +36,27 @@ public User(Long id) { registerEvent(UserEmailChangedEvent.class, event -> this.email = event.getNewEmail()); } - public static User create(String username) { + /** + * Creates a new user with the given parameters and raises a UserCreatedEvent. + * + * @param username the username of the new user + * @param nickname the nickname of the new user + * @param email the email of the new user + * @param phone the phone number of the new user + * @return the newly created user + */ + public static User create(String username, String nickname, String email, String phone) { var id = SnowflakeId.getInstance().nextId(); var user = new User(id); user.setUsername(username); - user.raiseEvent(new UserCreatedEvent(username)); + user.setNickname(nickname); + if (email != null && !email.isEmpty()) { + user.setEmail(email); + } + if (phone != null && !phone.isEmpty()) { + user.setPhone(phone); + } + user.raiseEvent(new UserCreatedEvent(username, nickname, email, phone)); return user; } diff --git a/identity/src/main/java/io/theurl/identity/domain/event/UserCreatedEvent.java b/identity/src/main/java/io/theurl/identity/domain/event/UserCreatedEvent.java index f41e22f..4b45ea4 100644 --- a/identity/src/main/java/io/theurl/identity/domain/event/UserCreatedEvent.java +++ b/identity/src/main/java/io/theurl/identity/domain/event/UserCreatedEvent.java @@ -1,13 +1,14 @@ package io.theurl.identity.domain.event; import io.theurl.framework.domain.DomainEvent; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter -public class UserCreatedEvent extends DomainEvent { +@AllArgsConstructor +public final class UserCreatedEvent extends DomainEvent { private final String username; - - public UserCreatedEvent(String username) { - this.username = username; - } + private final String nickname; + private final String email; + private final String phone; } diff --git a/identity/src/main/java/io/theurl/identity/domain/event/UserUnlockedEvent.java b/identity/src/main/java/io/theurl/identity/domain/event/UserUnlockedEvent.java index 85dbe55..d48f668 100644 --- a/identity/src/main/java/io/theurl/identity/domain/event/UserUnlockedEvent.java +++ b/identity/src/main/java/io/theurl/identity/domain/event/UserUnlockedEvent.java @@ -9,12 +9,7 @@ @Getter @AllArgsConstructor -public class UserUnlockedEvent extends DomainEvent { +public final class UserUnlockedEvent extends DomainEvent { private final Long id; private final LocalDateTime unlockTime; - - @Override - public > void attach(IAggregateRoot aggregateRoot) { - - } } diff --git a/identity/src/main/java/io/theurl/identity/interfaces/controller/AccountController.java b/identity/src/main/java/io/theurl/identity/interfaces/controller/AccountController.java index 4dad31b..7758c3d 100644 --- a/identity/src/main/java/io/theurl/identity/interfaces/controller/AccountController.java +++ b/identity/src/main/java/io/theurl/identity/interfaces/controller/AccountController.java @@ -1,10 +1,40 @@ package io.theurl.identity.interfaces.controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import io.theurl.identity.application.contract.UserApplicationService; +import io.theurl.identity.application.dto.UserCreateRequestDto; +import io.theurl.identity.application.dto.UserPasswordChangeRequestDto; +import io.theurl.identity.application.dto.UserProfileResponseDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("account") public class AccountController { + private final UserApplicationService service; + + public AccountController(UserApplicationService service) { + this.service = service; + } + + @PostMapping("/register") + public ResponseEntity create(@RequestBody UserCreateRequestDto user) { + service.createAsync(user) + .join(); + return ResponseEntity.ok().build(); + } + + @GetMapping("/profile") + public ResponseEntity getProfile() { + var profileFuture = service.getProfileAsync(); + var profile = profileFuture.join(); + return ResponseEntity.ok(profile); + } + + @PostMapping("/password/change") + public ResponseEntity changePassword(@RequestBody UserPasswordChangeRequestDto user) { + service.changePasswordAsync(user.getOldPassword(), user.getNewPassword()) + .join(); + return ResponseEntity.ok().build(); + } } diff --git a/identity/src/main/java/io/theurl/identity/interfaces/controller/AuthController.java b/identity/src/main/java/io/theurl/identity/interfaces/controller/AuthController.java index d073b6e..37661f9 100644 --- a/identity/src/main/java/io/theurl/identity/interfaces/controller/AuthController.java +++ b/identity/src/main/java/io/theurl/identity/interfaces/controller/AuthController.java @@ -1,8 +1,8 @@ package io.theurl.identity.interfaces.controller; import io.theurl.identity.application.contract.AuthApplicationService; -import io.theurl.identity.interfaces.dto.TokenGrantRequestDto; -import io.theurl.identity.interfaces.dto.TokenGrantResponseDto; +import io.theurl.identity.application.dto.TokenGrantRequestDto; +import io.theurl.identity.application.dto.TokenGrantResponseDto; import org.springframework.web.bind.annotation.*; @RestController diff --git a/identity/src/main/java/io/theurl/identity/interfaces/controller/UserController.java b/identity/src/main/java/io/theurl/identity/interfaces/controller/UserController.java index 3a7cea2..2b63c52 100644 --- a/identity/src/main/java/io/theurl/identity/interfaces/controller/UserController.java +++ b/identity/src/main/java/io/theurl/identity/interfaces/controller/UserController.java @@ -1,9 +1,21 @@ package io.theurl.identity.interfaces.controller; +import io.theurl.identity.application.contract.UserApplicationService; +import io.theurl.identity.application.dto.UserCreateRequestDto; +import org.springframework.http.ResponseEntity; +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 @RequestMapping("user") public class UserController { + private final UserApplicationService service; + + public UserController(UserApplicationService service) { + this.service = service; + } + + } diff --git a/identity/src/main/java/io/theurl/identity/persistence/model/UserDetail.java b/identity/src/main/java/io/theurl/identity/persistence/model/UserDetail.java new file mode 100644 index 0000000..1b9f890 --- /dev/null +++ b/identity/src/main/java/io/theurl/identity/persistence/model/UserDetail.java @@ -0,0 +1,4 @@ +package io.theurl.identity.persistence.model; + +public class UserDetail { +} diff --git a/identity/src/main/java/io/theurl/identity/persistence/profile/UserMapProfile.java b/identity/src/main/java/io/theurl/identity/persistence/profile/UserMapProfile.java index e893b03..d71cb78 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/profile/UserMapProfile.java +++ b/identity/src/main/java/io/theurl/identity/persistence/profile/UserMapProfile.java @@ -13,7 +13,43 @@ public class UserMapProfile { @PostConstruct public void configure() { - Provider userProvider = request -> new io.theurl.identity.domain.aggregate.User((Long) request.getSource()); + Provider userProvider = request -> new io.theurl.identity.domain.aggregate.User( + ((io.theurl.identity.persistence.entity.User) request.getSource()).getId() + ); + Provider userRoleProvider = request -> new io.theurl.identity.domain.aggregate.UserRole( + ((io.theurl.identity.persistence.entity.UserRole) request.getSource()).getId(), + ((io.theurl.identity.persistence.entity.UserRole) request.getSource()).getName() + ); + Provider userAuthorityProvider = request -> new io.theurl.identity.domain.aggregate.UserAuthority( + ((io.theurl.identity.persistence.entity.UserAuthority) request.getSource()).getId(), + ((io.theurl.identity.persistence.entity.UserAuthority) request.getSource()).getProvider(), + ((io.theurl.identity.persistence.entity.UserAuthority) request.getSource()).getOpenId() + ); + + mapper.createTypeMap(io.theurl.identity.persistence.entity.UserRole.class, io.theurl.identity.domain.aggregate.UserRole.class) + .setProvider(userRoleProvider); + + mapper.createTypeMap(io.theurl.identity.domain.aggregate.UserRole.class, io.theurl.identity.persistence.entity.UserRole.class) + .addMappings(expression -> { + expression.map(io.theurl.identity.domain.aggregate.UserRole::getId, io.theurl.identity.persistence.entity.UserRole::setId); + expression.map(io.theurl.identity.domain.aggregate.UserRole::getName, io.theurl.identity.persistence.entity.UserRole::setName); + expression.skip(io.theurl.identity.persistence.entity.UserRole::setUserId); + }); + + mapper.createTypeMap(io.theurl.identity.persistence.entity.UserAuthority.class, io.theurl.identity.domain.aggregate.UserAuthority.class) + .setProvider(userAuthorityProvider) + .addMappings(expression -> { + expression.map(io.theurl.identity.persistence.entity.UserAuthority::getName, io.theurl.identity.domain.aggregate.UserAuthority::setName); + }); + + mapper.createTypeMap(io.theurl.identity.domain.aggregate.UserAuthority.class, io.theurl.identity.persistence.entity.UserAuthority.class) + .addMappings(expression -> { + expression.map(io.theurl.identity.domain.aggregate.UserAuthority::getId, io.theurl.identity.persistence.entity.UserAuthority::setId); + expression.map(io.theurl.identity.domain.aggregate.UserAuthority::getProvider, io.theurl.identity.persistence.entity.UserAuthority::setProvider); + expression.map(io.theurl.identity.domain.aggregate.UserAuthority::getOpenId, io.theurl.identity.persistence.entity.UserAuthority::setOpenId); + expression.map(io.theurl.identity.domain.aggregate.UserAuthority::getName, io.theurl.identity.persistence.entity.UserAuthority::setName); + expression.skip(io.theurl.identity.persistence.entity.UserAuthority::setUserId); + }); mapper.createTypeMap(io.theurl.identity.persistence.entity.User.class, io.theurl.identity.domain.aggregate.User.class) .setProvider(userProvider) diff --git a/identity/src/main/java/io/theurl/identity/persistence/query/UserDetailQuery.java b/identity/src/main/java/io/theurl/identity/persistence/query/UserDetailQuery.java new file mode 100644 index 0000000..ab818c4 --- /dev/null +++ b/identity/src/main/java/io/theurl/identity/persistence/query/UserDetailQuery.java @@ -0,0 +1,7 @@ +package io.theurl.identity.persistence.query; + +import com.neroyun.mediator.Query; +import io.theurl.identity.persistence.model.UserDetail; + +public record UserDetailQuery(Long id) implements Query { +} diff --git a/identity/src/main/java/io/theurl/identity/persistence/repository/UserRepositoryImpl.java b/identity/src/main/java/io/theurl/identity/persistence/repository/UserRepositoryImpl.java index 920112e..83dfca6 100644 --- a/identity/src/main/java/io/theurl/identity/persistence/repository/UserRepositoryImpl.java +++ b/identity/src/main/java/io/theurl/identity/persistence/repository/UserRepositoryImpl.java @@ -4,6 +4,8 @@ import io.theurl.framework.utility.RandomUtility; import io.theurl.identity.domain.aggregate.User; import io.theurl.identity.domain.repository.UserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import lombok.AllArgsConstructor; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Repository; @@ -14,6 +16,9 @@ public class UserRepositoryImpl implements UserRepository { private final JpaUserRepository repository; private final ModelMapper mapper; + @PersistenceContext + private EntityManager context; + @Override public void save(User user) { var entity = repository.findById(user.getId()) @@ -65,7 +70,10 @@ public User findByPhone(String phone) { @Override public User findByAnyOf(String username, String email, String phone) { return repository.findByAnyOf(username, email, phone) - .map(entity -> mapper.map(entity, User.class)) + .map(entity ->{ + System.out.println(entity); + return mapper.map(entity, User.class); + }) .orElse(null); } }