diff --git a/client/src/pages/registration/api/action.ts b/client/src/pages/registration/api/action.ts index e57cf72a..c35dbea6 100644 --- a/client/src/pages/registration/api/action.ts +++ b/client/src/pages/registration/api/action.ts @@ -5,8 +5,7 @@ import { SERVER_URL } from "shared/config"; import { API_REGISTRATION } from "shared/config/backend"; interface RegistrationRequestBody { - name: string; - surname: string; + fullName: string; login: string; password: string; } @@ -18,7 +17,7 @@ export interface RegistrationAction { export const action: ActionFunction = async ({ request, }): Promise => { - const { name, surname, login, password, errors } = parseForm( + const { fullName, login, password, errors } = parseForm( await request.formData(), ); @@ -29,8 +28,7 @@ export const action: ActionFunction = async ({ } const registrationRequestBody: RegistrationRequestBody = { - name: name, - surname: surname, + fullName: fullName, login: login, password: password, }; @@ -48,6 +46,6 @@ export const action: ActionFunction = async ({ } return { - errors: [`Failed to authenticate: '${status}' code`], + errors: [`User already exists`], }; }; diff --git a/client/src/pages/registration/model/parseForm.ts b/client/src/pages/registration/model/parseForm.ts index 2da918e0..5f33535d 100644 --- a/client/src/pages/registration/model/parseForm.ts +++ b/client/src/pages/registration/model/parseForm.ts @@ -3,27 +3,22 @@ import { MINIMAL_PASSWORD_LENGTH } from "shared/config/frontend"; export function parseForm(data: FormData) { const errors = []; - const name = data.get("name"); - if (typeof name !== "string" || name === "") { + const fullName = data.get("fullName"); + if (typeof fullName !== "string" || fullName.trim() === "") { errors.push("Type name at form"); } - const surname = data.get("surname"); - if (typeof surname !== "string" || surname === "") { - errors.push("Type surname at form"); - } - const login = data.get("login"); - if (typeof login !== "string" || login === "") { + if (typeof login !== "string" || login.trim() === "") { errors.push("Type login at form"); } - if (typeof login === "string" && login.length < 4) { - errors.push("Login must greater than 3 symbols"); + if (typeof login === "string" && login.trim() === "") { + errors.push("Type login at form"); } const password = data.get("password"); - if (typeof password !== "string" || password === "") { + if (typeof password !== "string" || password.trim() === "") { errors.push("Type password at form"); } @@ -31,12 +26,11 @@ export function parseForm(data: FormData) { typeof password === "string" && password.length < MINIMAL_PASSWORD_LENGTH ) { - errors.push(`Password must greater ${MINIMAL_PASSWORD_LENGTH} symbols`); + errors.push(`Password length less than ${MINIMAL_PASSWORD_LENGTH}`); } - return { name, surname, login, password, errors } as { - name: string; - surname: string; + return { fullName, login, password, errors } as { + fullName: string; login: string; password: string; errors: string[]; diff --git a/client/src/pages/registration/ui/RegistrationPage.tsx b/client/src/pages/registration/ui/RegistrationPage.tsx index d4aecc9d..a2153d95 100644 --- a/client/src/pages/registration/ui/RegistrationPage.tsx +++ b/client/src/pages/registration/ui/RegistrationPage.tsx @@ -19,8 +19,7 @@ export const RegistrationPage = () => { )}
- - + diff --git a/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt b/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt index f0f2e024..56cfe874 100644 --- a/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt +++ b/server/app/src/main/kotlin/mu/muse/application/muse/PersistenceConfiguration.kt @@ -5,22 +5,23 @@ import mu.muse.domain.instrument.Country import mu.muse.domain.instrument.Instrument import mu.muse.domain.instrument.InstrumentId import mu.muse.domain.instrument.InstrumentName -import mu.muse.domain.instrument.ManufacturerDate import mu.muse.domain.instrument.Manufacturer +import mu.muse.domain.instrument.ManufacturerDate import mu.muse.domain.instrument.Material import mu.muse.domain.instrument.ReleaseDate +import mu.muse.domain.user.FullName import mu.muse.domain.user.Password import mu.muse.domain.user.Role import mu.muse.domain.user.User +import mu.muse.domain.user.UserId import mu.muse.domain.user.Username -import mu.muse.domain.user.UsernameId import mu.muse.persistence.instrument.InMemoryInstrumentIdGenerator import mu.muse.persistence.instrument.InMemoryInstrumentRepository import mu.muse.persistence.user.InMemoryUserRepository import mu.muse.persistence.user.InMemoryUsernameIdGenerator import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.security.crypto.factory.PasswordEncoderFactories +import org.springframework.security.crypto.password.PasswordEncoder import java.time.Instant @Configuration @@ -30,23 +31,21 @@ class PersistenceConfiguration { fun usernameIdGenerator() = InMemoryUsernameIdGenerator() @Bean - fun users(idGenerator: IdGenerator): Set { - val passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() - + fun users(idGenerator: IdGenerator, passwordEncoder: PasswordEncoder): Set { val user = User.create( - idGenerator = idGenerator, + id = idGenerator.generate(), username = Username.from("user"), password = Password.from(passwordEncoder.encode("123")), role = Role.user(), - fullName = "User Userov", + fullName = FullName.from("User Userov"), ) val editor = User.create( - idGenerator = idGenerator, + id = idGenerator.generate(), username = Username.from("editor"), password = Password.from(passwordEncoder.encode("321")), role = Role.editor(), - fullName = "Editor Editorov", + fullName = FullName.from("Editor Editorov"), ) return setOf(user, editor) @@ -60,7 +59,6 @@ class PersistenceConfiguration { @Bean fun instruments(idGenerator: IdGenerator): Set { - val releasedGuitar = Instrument.create( id = idGenerator.generate(), name = InstrumentName.from("Fender Stratocaster"), diff --git a/server/app/src/main/kotlin/mu/muse/application/muse/RestConfiguration.kt b/server/app/src/main/kotlin/mu/muse/application/muse/RestConfiguration.kt index f9d3386f..3bd514a1 100644 --- a/server/app/src/main/kotlin/mu/muse/application/muse/RestConfiguration.kt +++ b/server/app/src/main/kotlin/mu/muse/application/muse/RestConfiguration.kt @@ -12,6 +12,7 @@ import mu.muse.rest.instruments.GetInstrumentMaterialsEndpoint import mu.muse.rest.instruments.GetInstrumentTypesEndpoint import mu.muse.rest.login.BasicLoginEndpoint import mu.muse.rest.profile.GetProfileEndpoint +import mu.muse.rest.registration.RegistrationEndpoint import mu.muse.usecase.BasicLogin import mu.muse.usecase.CreateInstrument import mu.muse.usecase.DeleteInstrumentById @@ -24,6 +25,7 @@ import mu.muse.usecase.GetInstrumentMaterials import mu.muse.usecase.GetInstrumentTypes import mu.muse.usecase.GetInstrumentsByCriteria import mu.muse.usecase.GetProfile +import mu.muse.usecase.RegisterUser import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -73,4 +75,7 @@ class RestConfiguration { @Bean fun editInstrumentEndpoint(editInstrument: EditInstrument) = EditInstrumentEndpoint(editInstrument) + + @Bean + fun registrationEndpoint(registerUser: RegisterUser) = RegistrationEndpoint(registerUser) } diff --git a/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt b/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt index 1d9fb9ac..9f3329ad 100644 --- a/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt +++ b/server/app/src/main/kotlin/mu/muse/application/muse/SecurityConfiguration.kt @@ -10,9 +10,10 @@ import mu.muse.rest.API_INSTRUMENTS import mu.muse.rest.API_INSTRUMENT_BY_ID import mu.muse.rest.API_INSTRUMENT_MATERIALS import mu.muse.rest.API_INSTRUMENT_TYPES +import mu.muse.rest.API_REGISTRATION import mu.muse.rest.AUTH_BASIC_LOGIN import mu.muse.usecase.access.user.UserExtractor -import mu.muse.usecase.scenario.BasicLoginUseCase +import mu.muse.usecase.scenario.login.BasicLoginUseCase import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -26,6 +27,7 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.core.session.SessionRegistry import org.springframework.security.core.session.SessionRegistryImpl import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter @@ -34,10 +36,10 @@ import org.springframework.web.cors.CorsConfigurationSource import org.springframework.web.cors.UrlBasedCorsConfigurationSource import java.security.Key - @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) +@Suppress("TooManyFunctions") class SecurityConfiguration { @Bean @@ -88,6 +90,7 @@ class SecurityConfiguration { .requestMatchers(HttpMethod.GET, API_INSTRUMENT_MATERIALS).permitAll() .requestMatchers(HttpMethod.GET, API_COUNTRIES).permitAll() .requestMatchers(HttpMethod.GET, API_GET_MANUFACTURER_NAMES).permitAll() + .requestMatchers(HttpMethod.POST, API_REGISTRATION).permitAll() .anyRequest().authenticated() } @@ -135,4 +138,7 @@ class SecurityConfiguration { @Bean fun securityContextHolderAwareRequestFilter() = SecurityContextHolderAwareRequestFilter() + + @Bean + fun passwordEncoder() = BCryptPasswordEncoder() } diff --git a/server/app/src/main/kotlin/mu/muse/application/muse/UseCaseConfiguration.kt b/server/app/src/main/kotlin/mu/muse/application/muse/UseCaseConfiguration.kt index 016e584b..b66217ff 100644 --- a/server/app/src/main/kotlin/mu/muse/application/muse/UseCaseConfiguration.kt +++ b/server/app/src/main/kotlin/mu/muse/application/muse/UseCaseConfiguration.kt @@ -2,10 +2,12 @@ package mu.muse.application.muse import mu.muse.domain.IdGenerator import mu.muse.domain.instrument.InstrumentId +import mu.muse.domain.user.UserId import mu.muse.usecase.access.instrument.InstrumentExtractor import mu.muse.usecase.access.instrument.InstrumentPersister import mu.muse.usecase.access.instrument.InstrumentRemover import mu.muse.usecase.access.user.UserExtractor +import mu.muse.usecase.access.user.UserPersister import mu.muse.usecase.scenario.country.GetCountriesUseCase import mu.muse.usecase.scenario.instrument.CreateInstrumentUseCase import mu.muse.usecase.scenario.instrument.DeleteInstrumentByIdUseCase @@ -17,8 +19,10 @@ import mu.muse.usecase.scenario.instrument.GetInstrumentTypesUseCase import mu.muse.usecase.scenario.instrument.GetInstrumentsByCriteriaPaginatedUseCase import mu.muse.usecase.scenario.instrument.GetInstrumentsByCriteriaUseCase import mu.muse.usecase.scenario.profile.GetProfileUseCase +import mu.muse.usecase.scenario.registration.RegisterUserUseCase import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.crypto.password.PasswordEncoder @Suppress("TooManyFunctions") @Configuration @@ -62,4 +66,13 @@ class UseCaseConfiguration { @Bean fun editInstrument(instrumentExtractor: InstrumentExtractor, instrumentPersister: InstrumentPersister) = EditInstrumentUseCase(instrumentExtractor, instrumentPersister) + + @Bean + fun registerUser( + idGenerator: IdGenerator, + userExtractor: UserExtractor, + userPersister: UserPersister, + passwordEncoder: PasswordEncoder, + ) = + RegisterUserUseCase(idGenerator, userExtractor, userPersister, passwordEncoder) } diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt b/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt new file mode 100644 index 00000000..5540609a --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/domain/user/FullName.kt @@ -0,0 +1,16 @@ +package mu.muse.domain.user + +import mu.muse.common.annotations.ValueObject + +@ValueObject +data class FullName(private val value: String) { + + fun toStringValue() = value + + companion object { + fun from(fullName: String): FullName { + require(fullName.isNotEmpty()) + return FullName(fullName) + } + } +} diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/User.kt b/server/app/src/main/kotlin/mu/muse/domain/user/User.kt index aecbf27e..4f3172ef 100644 --- a/server/app/src/main/kotlin/mu/muse/domain/user/User.kt +++ b/server/app/src/main/kotlin/mu/muse/domain/user/User.kt @@ -2,29 +2,26 @@ package mu.muse.domain.user import mu.muse.common.types.AggregateRoot import mu.muse.common.types.Version -import mu.muse.domain.IdGenerator - -typealias FullName = String class User internal constructor( - id: UsernameId, + id: UserId, val username: Username, val password: Password, val role: Role, val fullName: FullName, version: Version, -) : AggregateRoot(id, version) { +) : AggregateRoot(id, version) { companion object { fun create( - idGenerator: IdGenerator, + id: UserId, username: Username, password: Password, role: Role, fullName: FullName, ): User { return User( - id = idGenerator.generate(), + id = id, username = username, password = password, role = role, diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt b/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt new file mode 100644 index 00000000..15e31eb5 --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/domain/user/UserId.kt @@ -0,0 +1,10 @@ +package mu.muse.domain.user + +class UserId internal constructor(private val value: Long) { + companion object { + fun from(value: Long): UserId { + require(value >= 1L && value <= Long.MAX_VALUE) + return UserId(value) + } + } +} diff --git a/server/app/src/main/kotlin/mu/muse/domain/user/UsernameId.kt b/server/app/src/main/kotlin/mu/muse/domain/user/UsernameId.kt deleted file mode 100644 index 1d2c8f5e..00000000 --- a/server/app/src/main/kotlin/mu/muse/domain/user/UsernameId.kt +++ /dev/null @@ -1,10 +0,0 @@ -package mu.muse.domain.user - -class UsernameId internal constructor(private val value: Long) { - companion object { - fun from(value: Long): UsernameId { - require(value >= 1L && value <= Long.MAX_VALUE) - return UsernameId(value) - } - } -} diff --git a/server/app/src/main/kotlin/mu/muse/persistence/user/InMemoryUsernameIdGenerator.kt b/server/app/src/main/kotlin/mu/muse/persistence/user/InMemoryUsernameIdGenerator.kt index 12545d3b..095eca71 100644 --- a/server/app/src/main/kotlin/mu/muse/persistence/user/InMemoryUsernameIdGenerator.kt +++ b/server/app/src/main/kotlin/mu/muse/persistence/user/InMemoryUsernameIdGenerator.kt @@ -1,14 +1,14 @@ package mu.muse.persistence.user import mu.muse.domain.IdGenerator -import mu.muse.domain.user.UsernameId +import mu.muse.domain.user.UserId import java.util.concurrent.atomic.AtomicLong -class InMemoryUsernameIdGenerator : IdGenerator { +class InMemoryUsernameIdGenerator : IdGenerator { private val counter = AtomicLong(0L) - override fun generate(): UsernameId { - return UsernameId.from(counter.incrementAndGet()) + override fun generate(): UserId { + return UserId.from(counter.incrementAndGet()) } } diff --git a/server/app/src/main/kotlin/mu/muse/rest/EndpointURL.kt b/server/app/src/main/kotlin/mu/muse/rest/EndpointURL.kt index eaa061f9..155df5c1 100644 --- a/server/app/src/main/kotlin/mu/muse/rest/EndpointURL.kt +++ b/server/app/src/main/kotlin/mu/muse/rest/EndpointURL.kt @@ -13,3 +13,4 @@ const val API_COUNTRIES = "$API/countries" const val API_GET_MANUFACTURER_NAMES = "$API/manufacturers" const val API_CREATE_INSTRUMENT = "$API/instrument/create" const val API_EDIT_INSTRUMENT = "$API/instrument/edit" +const val API_REGISTRATION = "$API/registration" diff --git a/server/app/src/main/kotlin/mu/muse/rest/registration/RegistrationEndpoint.kt b/server/app/src/main/kotlin/mu/muse/rest/registration/RegistrationEndpoint.kt new file mode 100644 index 00000000..192b79d5 --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/rest/registration/RegistrationEndpoint.kt @@ -0,0 +1,34 @@ +package mu.muse.rest.registration + +import mu.muse.domain.user.FullName +import mu.muse.domain.user.Password +import mu.muse.domain.user.Username +import mu.muse.rest.API_REGISTRATION +import mu.muse.usecase.RegisterUser +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class RegistrationEndpoint( + private val registerUser: RegisterUser, +) { + + @PostMapping(API_REGISTRATION) + fun register(@RequestBody request: RegistrationRequest) { + val username = Username.from(request.login) + val password = Password.from(request.password) + val fullName = FullName.from(request.fullName) + registerUser.execute( + fullName = fullName, + username = username, + password = password, + ) + } + + data class RegistrationRequest( + val fullName: String, + val login: String, + val password: String, + ) +} diff --git a/server/app/src/main/kotlin/mu/muse/usecase/BasicLogin.kt b/server/app/src/main/kotlin/mu/muse/usecase/BasicLogin.kt index dab16fe1..348fad02 100644 --- a/server/app/src/main/kotlin/mu/muse/usecase/BasicLogin.kt +++ b/server/app/src/main/kotlin/mu/muse/usecase/BasicLogin.kt @@ -3,7 +3,7 @@ package mu.muse.usecase import mu.muse.common.types.BusinessError import mu.muse.domain.user.Password import mu.muse.domain.user.Username -import mu.muse.usecase.scenario.JwtRaw +import mu.muse.usecase.scenario.login.JwtRaw fun interface BasicLogin { fun execute(username: Username, password: Password): JwtRaw diff --git a/server/app/src/main/kotlin/mu/muse/usecase/RegisterUser.kt b/server/app/src/main/kotlin/mu/muse/usecase/RegisterUser.kt new file mode 100644 index 00000000..4a5958ed --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/usecase/RegisterUser.kt @@ -0,0 +1,15 @@ +package mu.muse.usecase + +import mu.muse.common.types.BusinessError +import mu.muse.domain.user.FullName +import mu.muse.domain.user.Password +import mu.muse.domain.user.Username + +fun interface RegisterUser { + fun execute(fullName: FullName, username: Username, password: Password) +} + +sealed class RegisterUserError : BusinessError { + data class AlreadyRegistered(val username: Username) : + RuntimeException("Username `${username.toStringValue()}` already registered") +} diff --git a/server/app/src/main/kotlin/mu/muse/usecase/dto/ProfileDetails.kt b/server/app/src/main/kotlin/mu/muse/usecase/dto/ProfileDetails.kt index af3e4cef..a446502f 100644 --- a/server/app/src/main/kotlin/mu/muse/usecase/dto/ProfileDetails.kt +++ b/server/app/src/main/kotlin/mu/muse/usecase/dto/ProfileDetails.kt @@ -12,7 +12,7 @@ data class ProfileDetails( return ProfileDetails( username = user.username.toStringValue(), role = user.role.toStringValue(), - fullName = user.fullName, + fullName = user.fullName.toStringValue(), ) } } diff --git a/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaPaginatedUseCase.kt b/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaPaginatedUseCase.kt index 2c3ba5cb..e614fb2e 100644 --- a/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaPaginatedUseCase.kt +++ b/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaPaginatedUseCase.kt @@ -5,6 +5,7 @@ import mu.muse.common.rest.PageRequest import mu.muse.usecase.GetInstrumentsByCriteriaPaginated import mu.muse.usecase.access.instrument.InstrumentExtractor import mu.muse.usecase.dto.InstrumentDetails +import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -13,7 +14,7 @@ class GetInstrumentsByCriteriaPaginatedUseCase( ) : GetInstrumentsByCriteriaPaginated { companion object { - val logger = LoggerFactory.getLogger(GetInstrumentByIdUseCase::class.java) + val logger: Logger = LoggerFactory.getLogger(GetInstrumentsByCriteriaPaginatedUseCase::class.java) } override fun execute( diff --git a/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaUseCase.kt b/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaUseCase.kt index 7f763530..9e86e3fe 100644 --- a/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaUseCase.kt +++ b/server/app/src/main/kotlin/mu/muse/usecase/scenario/instrument/GetInstrumentsByCriteriaUseCase.kt @@ -3,12 +3,17 @@ package mu.muse.usecase.scenario.instrument import mu.muse.usecase.GetInstrumentsByCriteria import mu.muse.usecase.access.instrument.InstrumentExtractor import mu.muse.usecase.dto.InstrumentDetails -import mu.muse.usecase.scenario.instrument.GetInstrumentsByCriteriaPaginatedUseCase.Companion.logger +import org.slf4j.Logger +import org.slf4j.LoggerFactory class GetInstrumentsByCriteriaUseCase( private val instrumentExtractor: InstrumentExtractor, ) : GetInstrumentsByCriteria { + companion object { + val logger: Logger = LoggerFactory.getLogger(GetInstrumentsByCriteriaUseCase::class.java) + } + override fun execute(criteria: InstrumentExtractor.Criteria): Collection { val instruments = instrumentExtractor.findByCriteria( InstrumentExtractor.Criteria( diff --git a/server/app/src/main/kotlin/mu/muse/usecase/scenario/BasicLoginUseCase.kt b/server/app/src/main/kotlin/mu/muse/usecase/scenario/login/BasicLoginUseCase.kt similarity index 87% rename from server/app/src/main/kotlin/mu/muse/usecase/scenario/BasicLoginUseCase.kt rename to server/app/src/main/kotlin/mu/muse/usecase/scenario/login/BasicLoginUseCase.kt index 41561626..df7aab04 100644 --- a/server/app/src/main/kotlin/mu/muse/usecase/scenario/BasicLoginUseCase.kt +++ b/server/app/src/main/kotlin/mu/muse/usecase/scenario/login/BasicLoginUseCase.kt @@ -1,4 +1,4 @@ -package mu.muse.usecase.scenario +package mu.muse.usecase.scenario.login import io.jsonwebtoken.Jwts import mu.muse.domain.user.Password @@ -15,7 +15,7 @@ typealias JwtRaw = String class BasicLoginUseCase( private val authenticationManager: AuthenticationManager, - private val userRepository: UserExtractor, + private val userExtractor: UserExtractor, private val secretKey: Key, private val expirationMillis: Long, ) : BasicLogin { @@ -27,7 +27,7 @@ class BasicLoginUseCase( ), ) - val user = userRepository.findByUsername(username) ?: throw BasicLoginError.UserNotFound(username) + val user = userExtractor.findByUsername(username) ?: throw BasicLoginError.UserNotFound(username) val claims = Jwts.claims().setSubject(user.username.toStringValue()) return Jwts.builder() diff --git a/server/app/src/main/kotlin/mu/muse/usecase/scenario/registration/RegisterUserUseCase.kt b/server/app/src/main/kotlin/mu/muse/usecase/scenario/registration/RegisterUserUseCase.kt new file mode 100644 index 00000000..ae7439e8 --- /dev/null +++ b/server/app/src/main/kotlin/mu/muse/usecase/scenario/registration/RegisterUserUseCase.kt @@ -0,0 +1,50 @@ +package mu.muse.usecase.scenario.registration + +import mu.muse.domain.IdGenerator +import mu.muse.domain.user.FullName +import mu.muse.domain.user.Password +import mu.muse.domain.user.Role +import mu.muse.domain.user.User +import mu.muse.domain.user.UserId +import mu.muse.domain.user.Username +import mu.muse.usecase.RegisterUser +import mu.muse.usecase.RegisterUserError +import mu.muse.usecase.access.user.UserExtractor +import mu.muse.usecase.access.user.UserPersister +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.security.crypto.password.PasswordEncoder + +class RegisterUserUseCase( + private val idGenerator: IdGenerator, + private val userExtractor: UserExtractor, + private val userPersister: UserPersister, + private val passwordEncoder: PasswordEncoder, +) : RegisterUser { + + companion object { + val logger: Logger = LoggerFactory.getLogger(RegisterUserUseCase::class.java) + } + + override fun execute(fullName: FullName, username: Username, password: Password) { + // TODO(unit-of-work): use unit of work there + // do not keep it idempotent, we want to show error if we submit same data once again + userExtractor.findByUsername(username)?.let { + throw RegisterUserError.AlreadyRegistered(username) + } + + logger.info("User `{}` not registered yet", username.toStringValue()) + + val user = User.create( + id = idGenerator.generate(), + username = username, + password = passwordEncoder.encode(password), + role = Role.user(), + fullName = fullName, + ) + + userPersister.save(user) + } +} + +fun PasswordEncoder.encode(password: Password) = Password.from(this.encode(password.toPlainStringValue())) diff --git a/server/app/src/test/kotlin/mu/muse/HelloEndpointTest.kt b/server/app/src/test/kotlin/mu/muse/HelloEndpointTest.kt index 395fdbb9..465c1b2b 100644 --- a/server/app/src/test/kotlin/mu/muse/HelloEndpointTest.kt +++ b/server/app/src/test/kotlin/mu/muse/HelloEndpointTest.kt @@ -2,11 +2,12 @@ package mu.muse import mu.muse.application.muse.SecurityConfiguration import mu.muse.domain.IdGenerator +import mu.muse.domain.user.FullName import mu.muse.domain.user.Password import mu.muse.domain.user.Role import mu.muse.domain.user.User import mu.muse.domain.user.Username -import mu.muse.domain.user.UsernameId +import mu.muse.domain.user.UserId import mu.muse.persistence.user.InMemoryUserRepository import mu.muse.persistence.user.InMemoryUsernameIdGenerator import mu.muse.rest.HelloEndpoint @@ -15,7 +16,9 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.factory.PasswordEncoderFactories +import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.test.context.support.WithAnonymousUser import org.springframework.security.test.context.support.WithUserDetails import org.springframework.test.context.ContextConfiguration @@ -75,23 +78,21 @@ internal class HelloEndpointTest { fun usernameIdGenerator() = InMemoryUsernameIdGenerator() @Bean - fun users(usernameIdGenerator: IdGenerator): Set { - val passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() - + fun users(idGenerator: IdGenerator, passwordEncoder: PasswordEncoder): Set { val testUser = User.create( - idGenerator = usernameIdGenerator, + id = idGenerator.generate(), username = Username.from("user"), password = Password.from(passwordEncoder.encode("123")), role = Role.user(), - fullName = "Anonymous", + fullName = FullName.from("Anonymous"), ) val testEditor = User.create( - idGenerator = usernameIdGenerator, + id = idGenerator.generate(), username = Username.from("editor"), password = Password.from(passwordEncoder.encode("321")), role = Role.editor(), - fullName = "Editor", + fullName = FullName.from("Editor"), ) return setOf(testUser, testEditor)