diff --git a/codiki-application/src/main/java/org/codiki/application/user/UserUseCases.java b/codiki-application/src/main/java/org/codiki/application/user/UserUseCases.java index e0cebc8..af4884b 100644 --- a/codiki-application/src/main/java/org/codiki/application/user/UserUseCases.java +++ b/codiki-application/src/main/java/org/codiki/application/user/UserUseCases.java @@ -5,6 +5,9 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static org.codiki.domain.user.model.UserRole.STANDARD; +import static org.codiki.domain.user.model.builder.UserBuilder.anUser; +import static org.springframework.util.ObjectUtils.isEmpty; import org.codiki.application.security.AuthenticationFacade; import org.codiki.application.security.JwtService; import org.codiki.application.security.annotation.AllowedToAdmins; @@ -12,6 +15,8 @@ import org.codiki.application.security.model.CustomUserDetails; import org.codiki.domain.exception.LoginFailureException; import org.codiki.domain.exception.RefreshTokenDoesNotExistException; import org.codiki.domain.exception.UserDoesNotExistException; +import org.codiki.domain.user.exception.UserAlreadyExistsException; +import org.codiki.domain.user.exception.UserCreationException; import org.codiki.domain.user.model.RefreshToken; import org.codiki.domain.user.model.User; import org.codiki.domain.user.model.UserAuthenticationData; @@ -107,4 +112,26 @@ public class UserUseCases { userPort.save(refreshToken); return refreshToken; } + + public User createUser(String pseudo, String email, String password) { + if (isEmpty(pseudo) || isEmpty(email) || isEmpty(password)) { + throw new UserCreationException(); + } + + if (userPort.existsByEmail(email)) { + throw new UserAlreadyExistsException(); + } + + User newUser = anUser() + .withId(UUID.randomUUID()) + .withPseudo(pseudo) + .withEmail(email) + .withPassword(passwordEncoder.encode(password)) + .withRole(STANDARD) + .build(); + + userPort.save(newUser); + + return newUser; + } } diff --git a/codiki-domain/src/main/java/org/codiki/domain/user/exception/UserAlreadyExistsException.java b/codiki-domain/src/main/java/org/codiki/domain/user/exception/UserAlreadyExistsException.java new file mode 100644 index 0000000..8562f14 --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/user/exception/UserAlreadyExistsException.java @@ -0,0 +1,9 @@ +package org.codiki.domain.user.exception; + +import org.codiki.domain.exception.FunctionnalException; + +public class UserAlreadyExistsException extends FunctionnalException { + public UserAlreadyExistsException() { + super("An user already exists with this email address."); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/user/exception/UserCreationException.java b/codiki-domain/src/main/java/org/codiki/domain/user/exception/UserCreationException.java new file mode 100644 index 0000000..5a0521b --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/user/exception/UserCreationException.java @@ -0,0 +1,9 @@ +package org.codiki.domain.user.exception; + +import org.codiki.domain.exception.FunctionnalException; + +public class UserCreationException extends FunctionnalException { + public UserCreationException() { + super("Pseudo, email address and password can not be empty."); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/user/model/User.java b/codiki-domain/src/main/java/org/codiki/domain/user/model/User.java index ae42506..e2bc3a2 100644 --- a/codiki-domain/src/main/java/org/codiki/domain/user/model/User.java +++ b/codiki-domain/src/main/java/org/codiki/domain/user/model/User.java @@ -5,6 +5,9 @@ import java.util.UUID; public record User( UUID id, + String pseudo, + String email, String password, + UUID photoId, List roles ) {} diff --git a/codiki-domain/src/main/java/org/codiki/domain/user/model/builder/UserBuilder.java b/codiki-domain/src/main/java/org/codiki/domain/user/model/builder/UserBuilder.java new file mode 100644 index 0000000..ac3305a --- /dev/null +++ b/codiki-domain/src/main/java/org/codiki/domain/user/model/builder/UserBuilder.java @@ -0,0 +1,64 @@ +package org.codiki.domain.user.model.builder; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.codiki.domain.user.model.User; +import org.codiki.domain.user.model.UserRole; + +public class UserBuilder { + private UUID id; + private String pseudo; + private String email; + private String password; + private UUID photoId; + private Set roles = new HashSet<>(); + + private UserBuilder() {} + + public static UserBuilder anUser() { + return new UserBuilder(); + } + + public UserBuilder withId(UUID id) { + this.id = id; + return this; + } + + public UserBuilder withPseudo(String pseudo) { + this.pseudo = pseudo; + return this; + } + + public UserBuilder withEmail(String email) { + this.email = email; + return this; + } + + public UserBuilder withPassword(String password) { + this.password = password; + return this; + } + + public UserBuilder withPhotoId(UUID photoId) { + this.photoId = photoId; + return this; + } + + public UserBuilder withRole(UserRole role) { + this.roles.add(role); + return this; + } + + public UserBuilder withRoles(List roles) { + this.roles = new HashSet<>(roles); + return this; + } + + public User build() { + return new User(id, pseudo,email, password, photoId, new LinkedList<>(roles)); + } +} diff --git a/codiki-domain/src/main/java/org/codiki/domain/user/port/UserPort.java b/codiki-domain/src/main/java/org/codiki/domain/user/port/UserPort.java index b6caa38..3068536 100644 --- a/codiki-domain/src/main/java/org/codiki/domain/user/port/UserPort.java +++ b/codiki-domain/src/main/java/org/codiki/domain/user/port/UserPort.java @@ -21,4 +21,6 @@ public interface UserPort { Optional findRefreshTokenById(UUID refreshTokenId); void save(RefreshToken refreshToken); + + boolean existsByEmail(String email); } diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java index b39f56b..5bcc3f8 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/configuration/GlobalControllerExceptionHandler.java @@ -16,6 +16,8 @@ import org.codiki.domain.picture.exception.PictureUploadException; import org.codiki.domain.publication.exception.PublicationEditionException; import org.codiki.domain.publication.exception.PublicationNotFoundException; import org.codiki.domain.publication.exception.PublicationUpdateForbiddenException; +import org.codiki.domain.user.exception.UserAlreadyExistsException; +import org.codiki.domain.user.exception.UserCreationException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -30,7 +32,9 @@ public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHan CategoryNotFoundException.class, LoginFailureException.class, PublicationEditionException.class, - PictureUploadException.class + PictureUploadException.class, + UserAlreadyExistsException.class, + UserCreationException.class }) public ProblemDetail handleBadRequestExceptions(Exception exception) { return buildProblemDetail(BAD_REQUEST, exception); diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/user/UserController.java b/codiki-exposition/src/main/java/org/codiki/exposition/user/UserController.java index 58a36a2..0a7d4ac 100644 --- a/codiki-exposition/src/main/java/org/codiki/exposition/user/UserController.java +++ b/codiki-exposition/src/main/java/org/codiki/exposition/user/UserController.java @@ -2,6 +2,7 @@ package org.codiki.exposition.user; import java.util.List; +import static org.springframework.http.HttpStatus.CREATED; import org.codiki.application.security.annotation.AllowedToAdmins; import org.codiki.application.security.annotation.AllowedToAnonymous; import org.codiki.application.user.UserUseCases; @@ -10,10 +11,13 @@ import org.codiki.domain.user.model.UserAuthenticationData; import org.codiki.exposition.user.model.LoginRequest; import org.codiki.exposition.user.model.LoginResponse; import org.codiki.exposition.user.model.RefreshTokenRequest; +import org.codiki.exposition.user.model.SignInRequestDto; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @@ -43,4 +47,10 @@ public class UserController { UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.refreshTokenValue()); return new LoginResponse(userAuthenticationData); } + + @PostMapping + @ResponseStatus(CREATED) + public void signIn(@RequestBody SignInRequestDto request) { + userUseCases.createUser(request.pseudo(), request.email(), request.password()); + } } diff --git a/codiki-exposition/src/main/java/org/codiki/exposition/user/model/SignInRequestDto.java b/codiki-exposition/src/main/java/org/codiki/exposition/user/model/SignInRequestDto.java new file mode 100644 index 0000000..8ba2d24 --- /dev/null +++ b/codiki-exposition/src/main/java/org/codiki/exposition/user/model/SignInRequestDto.java @@ -0,0 +1,8 @@ +package org.codiki.exposition.user.model; + +public record SignInRequestDto( + String pseudo, + String email, + String password +) { +} diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/adapter/UserJpaAdapter.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/adapter/UserJpaAdapter.java index 7b89494..bcc0eec 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/adapter/UserJpaAdapter.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/adapter/UserJpaAdapter.java @@ -51,6 +51,11 @@ public class UserJpaAdapter implements UserPort { return userJpaRepository.existsById(userId); } + @Override + public boolean existsByEmail(String email) { + return userJpaRepository.existsByEmail(email); + } + @Override public Optional findRefreshTokenByUserId(UUID userId) { return refreshTokenJpaRepository.findByUserId(userId) diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/model/UserEntity.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/model/UserEntity.java index 52c7aff..24141ff 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/model/UserEntity.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/model/UserEntity.java @@ -27,10 +27,14 @@ import lombok.Setter; public class UserEntity { @Id private UUID id; - + @Column(nullable = false) + private String pseudo; + @Column(nullable = false) + private String email; @Column(nullable = false) private String password; - + @Column + private UUID photoId; @ElementCollection(targetClass = UserRole.class) @CollectionTable( name = "user_role", @@ -41,14 +45,20 @@ public class UserEntity { public UserEntity(User user) { id = user.id(); + pseudo = user.pseudo(); + email = user.email(); password = user.password(); + photoId = user.photoId(); roles = user.roles(); } public User toUser() { return new User( id, + pseudo, + email, password, + photoId, roles ); } diff --git a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/repository/UserJpaRepository.java b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/repository/UserJpaRepository.java index 727bd51..29fdc7e 100644 --- a/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/repository/UserJpaRepository.java +++ b/codiki-infrastructure/src/main/java/org/codiki/infrastructure/user/repository/UserJpaRepository.java @@ -17,4 +17,6 @@ public interface UserJpaRepository extends JpaRepository { @Query("SELECT u FROM UserEntity u JOIN FETCH u.roles") List findAll(); + + boolean existsByEmail(String email); } diff --git a/codiki-infrastructure/src/main/resources/sql/-001-default-data.sql b/codiki-infrastructure/src/main/resources/sql/-001-default-data.sql new file mode 100644 index 0000000..651d6eb --- /dev/null +++ b/codiki-infrastructure/src/main/resources/sql/-001-default-data.sql @@ -0,0 +1,12 @@ +insert into "user" values +('5ad462b8-8f9e-4a26-bb86-c74fef5d11b6', 'Standard user', 'standard.user@codiki.org', '$2a$10$FVhrYRXw.Zw2V5jGUkvX/.1U.IdWlwd8J.Y/5pb5etAzyoBhJ3FHG', null), +('15a13dc7-029d-4eab-a63d-c1e96f90241d', 'Admin user', 'admin.user@codiki.org', '$2a$10$FVhrYRXw.Zw2V5jGUkvX/.1U.IdWlwd8J.Y/5pb5etAzyoBhJ3FHG', null); + +insert into user_role values +('5ad462b8-8f9e-4a26-bb86-c74fef5d11b6', 0), +('15a13dc7-029d-4eab-a63d-c1e96f90241d', 0), +('15a13dc7-029d-4eab-a63d-c1e96f90241d', 1); + +insert into category values +('172fa901-3f4b-4540-92f3-1c15820e8ec9', 'Main category', null), +('3f4b4540-a901-92f3-1c15-8ec9172f820e', 'Sub category', '172fa901-3f4b-4540-92f3-1c15820e8ec9'); diff --git a/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql b/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql index a5e009c..5347d20 100644 --- a/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql +++ b/codiki-infrastructure/src/main/resources/sql/001-initial-script-tables-creation.sql @@ -1,7 +1,9 @@ CREATE TABLE IF NOT EXISTS "user" ( id UUID NOT NULL, - name VARCHAR NOT NULL, + pseudo VARCHAR NOT NULL, + email VARCHAR NOT NULL, password VARCHAR NOT NULL, + photo_id UUID, CONSTRAINT user_pk PRIMARY KEY (id) ); @@ -39,6 +41,9 @@ CREATE TABLE IF NOT EXISTS picture ( ); CREATE INDEX picture_publisher_id_idx ON picture (publisher_id); +ALTER TABLE "user" ADD CONSTRAINT user_photo_id_fk FOREIGN KEY (photo_id) REFERENCES picture (id); +CREATE INDEX user_photo_id_idx ON "user" (photo_id); + CREATE TABLE IF NOT EXISTS publication ( id UUID NOT NULL, key VARCHAR(10) NOT NULL,