From 4a01412026046cca34c6a4cad2bf069b2a076a51 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Thu, 21 May 2026 10:05:20 +0700 Subject: [PATCH 1/3] =?UTF-8?q?refactor/qg-xxx:=20=D0=BE=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=B5=D1=80=D0=B0=D0=BF=D0=B5=D0=B2=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BA=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=BC=D1=83=20=D0=B3=D0=B0=D0=B9=D0=B4=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Который теперь разрешает вызывать операции из операций во имя повышения уровня абстракции/сокрытия деталей реализации и при условии, что граф зависимостей каждой операции является деревом (то есть каждый ресурс используется только в одной операции) --- .../app/publc/register/RegisterTherapistOp.kt | 20 +--------- .../pro/qyoga/core/users/auth/UsersFactory.kt | 19 --------- .../users/therapists/CreateTherapistUserOp.kt | 40 +++++++++++++++++++ .../therapists/CreateTherapistUserWorkflow.kt | 28 ------------- .../clients/card/CreateClientPageTest.kt | 4 +- .../clients/card/EditClientCardPageTest.kt | 4 +- .../CreateJournalEntryPageControllerTest.kt | 4 +- .../list/ClientsListPageControllerTest.kt | 6 +-- .../FillScheduleNotificationsRepoTest.kt | 6 +-- .../fixture/backgrounds/UsersBackgrounds.kt | 20 +++------- .../presets/TherapistsFixturePreset.kt | 4 +- 11 files changed, 62 insertions(+), 93 deletions(-) delete mode 100644 app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt create mode 100644 app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt delete mode 100644 app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserWorkflow.kt diff --git a/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt b/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt index 7708c7ac9..ec9a0582b 100644 --- a/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt +++ b/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt @@ -5,13 +5,10 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import pro.azhidkov.platform.errors.DomainError import pro.azhidkov.platform.kotlin.mapFailure -import pro.qyoga.core.users.auth.UsersFactory -import pro.qyoga.core.users.auth.UsersRepo import pro.qyoga.core.users.auth.errors.DuplicatedEmailException +import pro.qyoga.core.users.therapists.CreateTherapistUserOp import pro.qyoga.core.users.therapists.RegisterTherapistRequest import pro.qyoga.core.users.therapists.Therapist -import pro.qyoga.core.users.therapists.TherapistsRepo -import pro.qyoga.core.users.therapists.createTherapistUser import pro.qyoga.i9ns.email.Email import pro.qyoga.i9ns.email.EmailSender import pro.qyoga.tech.captcha.CaptchaService @@ -53,9 +50,7 @@ class RegistrationException( @Component class RegisterTherapistOp( - private val usersRepo: UsersRepo, - private val therapistsRepo: TherapistsRepo, - private val usersFactory: UsersFactory, + private val createTherapistUser: CreateTherapistUserOp, private val emailSender: EmailSender, private val captchaService: CaptchaService, @Value("\${spring.mail.username}") private val fromEmail: String, @@ -64,16 +59,6 @@ class RegisterTherapistOp( private val log = LoggerFactory.getLogger(javaClass) - private val createTherapistUser = { registerTherapistRequest: RegisterTherapistRequest, password: CharSequence -> - createTherapistUser( - usersRepo, - therapistsRepo, - usersFactory, - registerTherapistRequest, - password - ) - } - override fun invoke(registerTherapistRequest: RegisterTherapistRequest): Therapist { if (captchaService.isInvalid(registerTherapistRequest.captchaAnswer)) { log.info("Register therapist with invalid captcha request terminated") @@ -142,4 +127,3 @@ private fun generateRandomPassword() = append(passwordChars[Random.nextInt(passwordChars.size)]) } } - diff --git a/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt b/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt deleted file mode 100644 index 7dd33a098..000000000 --- a/app/src/main/kotlin/pro/qyoga/core/users/auth/UsersFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pro.qyoga.core.users.auth - -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.stereotype.Component -import pro.qyoga.core.users.auth.model.Role -import pro.qyoga.core.users.auth.model.User - - -@Component -class UsersFactory( - private val passwordEncoder: PasswordEncoder -) { - - fun createUser(email: String, plainPassword: CharSequence, roles: Set): User { - val passwordHash = passwordEncoder.encode(plainPassword)!! - return User(email, passwordHash, roles.toTypedArray(), enabled = true) - } - -} diff --git a/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt b/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt new file mode 100644 index 000000000..344933ae0 --- /dev/null +++ b/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt @@ -0,0 +1,40 @@ +package pro.qyoga.core.users.therapists + +import org.slf4j.LoggerFactory +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component +import pro.qyoga.core.users.auth.UsersRepo +import pro.qyoga.core.users.auth.model.Role +import pro.qyoga.core.users.auth.model.User + +@Component +class CreateTherapistUserOp( + private val usersRepo: UsersRepo, + private val therapistsRepo: TherapistsRepo, + private val passwordEncoder: PasswordEncoder +) { + private val log = LoggerFactory.getLogger("createTherapistUser") + + operator fun invoke( + registerTherapistRequest: RegisterTherapistRequest, + password: CharSequence + ): Therapist { + log.info("Creating new therapist user for {}", registerTherapistRequest.email) + + var user = createUser(registerTherapistRequest.email, password, setOf(Role.ROLE_THERAPIST)) + user = usersRepo.save(user) + + var therapist = Therapist(registerTherapistRequest.firstName, registerTherapistRequest.lastName, user.id) + therapist = therapistsRepo.save(therapist) + + log.info("Therapist user created, id = {}", user.id) + + return therapist + } + + private fun createUser(email: String, plainPassword: CharSequence, roles: Set): User { + val passwordHash = passwordEncoder.encode(plainPassword)!! + return User(email, passwordHash, roles.toTypedArray(), enabled = true) + } + +} diff --git a/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserWorkflow.kt b/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserWorkflow.kt deleted file mode 100644 index 1d1ae2fcd..000000000 --- a/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserWorkflow.kt +++ /dev/null @@ -1,28 +0,0 @@ -package pro.qyoga.core.users.therapists - -import org.slf4j.LoggerFactory -import pro.qyoga.core.users.auth.UsersFactory -import pro.qyoga.core.users.auth.UsersRepo -import pro.qyoga.core.users.auth.model.Role - -private val log = LoggerFactory.getLogger("createTherapistUser") - -fun createTherapistUser( - usersRepo: UsersRepo, - therapistsRepo: TherapistsRepo, - usersFactory: UsersFactory, - registerTherapistRequest: RegisterTherapistRequest, - password: CharSequence -): Therapist { - log.info("Creating new therapist user for {}", registerTherapistRequest.email) - - var user = usersFactory.createUser(registerTherapistRequest.email, password, setOf(Role.ROLE_THERAPIST)) - user = usersRepo.save(user) - - var therapist = Therapist(registerTherapistRequest.firstName, registerTherapistRequest.lastName, user.id) - therapist = therapistsRepo.save(therapist) - - log.info("Therapist user created, id = {}", user.id) - - return therapist -} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/CreateClientPageTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/CreateClientPageTest.kt index ba57decd3..5818c0d4a 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/CreateClientPageTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/CreateClientPageTest.kt @@ -92,7 +92,7 @@ class CreateClientPageTest : QYogaAppIntegrationBaseTest() { fun `System should allow to create client with existing phone number for another therapist`() { // Given val thePhone = randomPhoneNumber().toUIFormat() - val anotherTherapistId = backgrounds.users.registerNewTherapist().id + val anotherTherapistId = backgrounds.users.createNewTherapist().id backgrounds.clients.createClient(phone = thePhone, therapistId = anotherTherapistId) val duplicatedClient = ClientsObjectMother.createClientCardDto(phone = thePhone) val therapist = TherapistClient.loginAsTheTherapist() @@ -105,4 +105,4 @@ class CreateClientPageTest : QYogaAppIntegrationBaseTest() { clients.forAny { it shouldMatch duplicatedClient } } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/EditClientCardPageTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/EditClientCardPageTest.kt index e6f09f3a3..dfe0b56e3 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/EditClientCardPageTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/card/EditClientCardPageTest.kt @@ -139,7 +139,7 @@ class EditClientCardPageTest : QYogaAppIntegrationBaseTest() { fun `должна сохранять телефон, указанный для другого клиента другого терапевта`() { // Сетап val thePhone = randomPhoneNumber().toUIFormat() - val anotherTherapistId = backgrounds.users.registerNewTherapist().id + val anotherTherapistId = backgrounds.users.createNewTherapist().id backgrounds.clients.createClient(phone = thePhone, therapistId = anotherTherapistId) val targetClient = backgrounds.clients.createClient() val updatePhoneDto = targetClient.toDto().copy(phoneNumber = thePhone) @@ -153,4 +153,4 @@ class EditClientCardPageTest : QYogaAppIntegrationBaseTest() { clients.forAny { it shouldMatch updatePhoneDto } } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/journal/CreateJournalEntryPageControllerTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/journal/CreateJournalEntryPageControllerTest.kt index 36a399572..738ae6f10 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/journal/CreateJournalEntryPageControllerTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/journal/CreateJournalEntryPageControllerTest.kt @@ -67,7 +67,7 @@ class CreateJournalEntryPageControllerTest : QYogaAppIntegrationBaseTest() { fun createJournalEntryWithTaskExistingForAnotherTherapist() { // Given val taskName = "Йогатерапия гастрита" - val anotherTherapist = backgrounds.users.registerNewTherapist() + val anotherTherapist = backgrounds.users.createNewTherapist() val anotherTherapeuticTask = backgrounds.therapeuticTasks.createTherapeuticTask(anotherTherapist.id, taskName) val client = backgrounds.clients.aClient() @@ -90,4 +90,4 @@ class CreateJournalEntryPageControllerTest : QYogaAppIntegrationBaseTest() { } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/list/ClientsListPageControllerTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/list/ClientsListPageControllerTest.kt index 70e4c20ac..c7fe998e9 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/list/ClientsListPageControllerTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/app/therapist/clients/list/ClientsListPageControllerTest.kt @@ -50,7 +50,7 @@ class ClientsListPageControllerTest : QYogaAppIntegrationBaseTest() { val ownClientsCount = 5 val alienClientsCount = 5 - val anotherTherapist = backgrounds.users.registerNewTherapist() + val anotherTherapist = backgrounds.users.createNewTherapist() backgrounds.clients.createClients(ownClientsCount, THE_THERAPIST_ID) backgrounds.clients.createClients(alienClientsCount, anotherTherapist.id) @@ -73,7 +73,7 @@ class ClientsListPageControllerTest : QYogaAppIntegrationBaseTest() { val ownClientsCount = 5 val alienClientsCount = 5 - val anotherTherapist = backgrounds.users.registerNewTherapist() + val anotherTherapist = backgrounds.users.createNewTherapist() backgrounds.clients.createClients(ownClientsCount, THE_THERAPIST_ID) backgrounds.clients.createClients(alienClientsCount, anotherTherapist.id) @@ -142,4 +142,4 @@ class ClientsListPageControllerTest : QYogaAppIntegrationBaseTest() { clients.content[0].id shouldBe createdLaterClient.client.id } -} \ No newline at end of file +} diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/core/appointments/notifications/FillScheduleNotificationsRepoTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/core/appointments/notifications/FillScheduleNotificationsRepoTest.kt index 8195ab968..822595e02 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/core/appointments/notifications/FillScheduleNotificationsRepoTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/core/appointments/notifications/FillScheduleNotificationsRepoTest.kt @@ -47,7 +47,7 @@ class FillScheduleNotificationsRepoTest : QYogaAppBaseTest() { LocalTime.of(10, 0) ) - val moscowTherapist = usersBackgrounds.registerNewTherapist().ref() + val moscowTherapist = usersBackgrounds.createNewTherapist().ref() userTimeZonesTestApi.setTimeZone(moscowTherapist, ZoneId.of("Europe/Moscow")) notificationsTestApi.createFillScheduleSettings( moscowTherapist, @@ -253,7 +253,7 @@ class FillScheduleNotificationsRepoTest : QYogaAppBaseTest() { ) // Токио UTC+9 - val tokyoTherapist = usersBackgrounds.registerNewTherapist().ref() + val tokyoTherapist = usersBackgrounds.createNewTherapist().ref() userTimeZonesTestApi.setTimeZone(tokyoTherapist, ZoneId.of("Asia/Tokyo")) notificationsTestApi.createFillScheduleSettings( tokyoTherapist, @@ -262,7 +262,7 @@ class FillScheduleNotificationsRepoTest : QYogaAppBaseTest() { ) // Нью-Йорк UTC-4/-5 - val nyTherapist = usersBackgrounds.registerNewTherapist().ref() + val nyTherapist = usersBackgrounds.createNewTherapist().ref() userTimeZonesTestApi.setTimeZone(nyTherapist, ZoneId.of("America/New_York")) notificationsTestApi.createFillScheduleSettings( nyTherapist, diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/UsersBackgrounds.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/UsersBackgrounds.kt index ad93bc266..1b4957937 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/UsersBackgrounds.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/backgrounds/UsersBackgrounds.kt @@ -1,30 +1,22 @@ package pro.qyoga.tests.fixture.backgrounds import org.springframework.stereotype.Component -import pro.qyoga.core.users.auth.UsersFactory -import pro.qyoga.core.users.auth.UsersRepo import pro.qyoga.core.users.auth.impl.TaUserDetailsService +import pro.qyoga.core.users.therapists.CreateTherapistUserOp import pro.qyoga.core.users.therapists.Therapist -import pro.qyoga.core.users.therapists.TherapistsRepo -import pro.qyoga.core.users.therapists.createTherapistUser import pro.qyoga.tests.fixture.data.randomPassword import pro.qyoga.tests.fixture.object_mothers.therapists.TherapistsObjectMother.registerTherapistRequest @Component class UsersBackgrounds( - private val usersRepo: UsersRepo, - private val therapistsRepo: TherapistsRepo, - private val usersFactory: UsersFactory, + private val createTherapistUser: CreateTherapistUserOp, private val userDetailsService: TaUserDetailsService ) { - fun registerNewTherapist(): Therapist { + fun createNewTherapist(): Therapist { return createTherapistUser( - usersRepo, - therapistsRepo, - usersFactory, - registerTherapistRequest(), - randomPassword() + registerTherapistRequest = registerTherapistRequest(), + password = randomPassword() ) } @@ -32,4 +24,4 @@ class UsersBackgrounds( userDetailsService.disableUser(userLogin) } -} \ No newline at end of file +} diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/presets/TherapistsFixturePreset.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/presets/TherapistsFixturePreset.kt index 9c8e766a5..47124bea1 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/presets/TherapistsFixturePreset.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/presets/TherapistsFixturePreset.kt @@ -19,7 +19,7 @@ class TherapistsFixturePreset( ) { fun createTherapistWithExercise(): Pair { - val therapist = usersBackgrounds.registerNewTherapist() + val therapist = usersBackgrounds.createNewTherapist() val exercise = exerciseBackgrounds.createExercise(ownerRef = therapist.ref()) return therapist to exercise } @@ -34,4 +34,4 @@ class TherapistsFixturePreset( ) } -} \ No newline at end of file +} From a10416df581bec414066aabb472660f422db07f8 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Thu, 21 May 2026 10:08:28 +0700 Subject: [PATCH 2/3] =?UTF-8?q?refactor/qg-xxx:=20=D0=BF=D0=B0=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=20=D0=B2=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D1=82=D0=B5=D1=80=D0=B0=D0=BF=D0=B5=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D1=91=D0=BD=20=D0=BD=D0=B0=20SecretChars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pro/qyoga/app/publc/register/RegisterTherapistOp.kt | 6 ++++-- .../qyoga/core/users/therapists/CreateTherapistUserOp.kt | 7 ++++--- .../kotlin/pro/qyoga/tests/fixture/data/Text.kt | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt b/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt index ec9a0582b..f93f561a6 100644 --- a/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt +++ b/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import pro.azhidkov.platform.errors.DomainError import pro.azhidkov.platform.kotlin.mapFailure +import pro.azhidkov.platform.secrets.SecretChars import pro.qyoga.core.users.auth.errors.DuplicatedEmailException import pro.qyoga.core.users.therapists.CreateTherapistUserOp import pro.qyoga.core.users.therapists.RegisterTherapistRequest @@ -88,7 +89,7 @@ class RegisterTherapistOp( return therapist } - private fun welcomeEmail(to: String, password: String) = + private fun welcomeEmail(to: String, password: SecretChars) = Email( fromEmail, to, @@ -98,7 +99,7 @@ class RegisterTherapistOp( Вы зарегистрировались в Trainer Advisor. Логин от вашего аккаунта: $to. - Пароль от вашего аккаунта: $password. + Пароль от вашего аккаунта: ${password.show()}. Теперь вы можете войти в систему на странице https://trainer-advisor.pro/login используя email и пароль из этого письма. @@ -127,3 +128,4 @@ private fun generateRandomPassword() = append(passwordChars[Random.nextInt(passwordChars.size)]) } } + .let { SecretChars(it.toCharArray()) } diff --git a/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt b/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt index 344933ae0..23593d6be 100644 --- a/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt +++ b/app/src/main/kotlin/pro/qyoga/core/users/therapists/CreateTherapistUserOp.kt @@ -3,6 +3,7 @@ package pro.qyoga.core.users.therapists import org.slf4j.LoggerFactory import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Component +import pro.azhidkov.platform.secrets.SecretChars import pro.qyoga.core.users.auth.UsersRepo import pro.qyoga.core.users.auth.model.Role import pro.qyoga.core.users.auth.model.User @@ -17,7 +18,7 @@ class CreateTherapistUserOp( operator fun invoke( registerTherapistRequest: RegisterTherapistRequest, - password: CharSequence + password: SecretChars ): Therapist { log.info("Creating new therapist user for {}", registerTherapistRequest.email) @@ -32,8 +33,8 @@ class CreateTherapistUserOp( return therapist } - private fun createUser(email: String, plainPassword: CharSequence, roles: Set): User { - val passwordHash = passwordEncoder.encode(plainPassword)!! + private fun createUser(email: String, plainPassword: SecretChars, roles: Set): User { + val passwordHash = passwordEncoder.encode(plainPassword.show())!! return User(email, passwordHash, roles.toTypedArray(), enabled = true) } diff --git a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/data/Text.kt b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/data/Text.kt index 9a5846030..a54e358ac 100644 --- a/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/data/Text.kt +++ b/app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/data/Text.kt @@ -1,6 +1,7 @@ package pro.qyoga.tests.fixture.data import net.datafaker.Faker +import pro.azhidkov.platform.secrets.SecretChars import java.util.* val lowerCaseCyrillicLetters = ('а'..'я').toList() @@ -42,7 +43,7 @@ fun randomEmail(): String = append(randomWord(lowerCaseLatinLetters, 2, 3)) } -fun randomPassword() = randomLatinWord(minLength = 8) +fun randomPassword() = SecretChars(randomLatinWord(minLength = 8).toCharArray()) fun Faker.randomBase64String( bytes: Int = 32, From aa28751bb704047ea4c97d428293968acd1872d7 Mon Sep 17 00:00:00 2001 From: Aleksey Zhidkov Date: Thu, 21 May 2026 11:11:49 +0700 Subject: [PATCH 3/3] =?UTF-8?q?refactor/qg-xxx:=20=D0=BE=D1=82=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=B0=20=D0=B5=D0=BC=D0=B5=D0=B9=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81?= =?UTF-8?q?=D1=8B=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Для того чтобы повысить уровень абстракции операции и сделать эти каналы явной частью приложения --- app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt | 8 +-- .../app/publc/register/RegisterTherapistOp.kt | 50 +++++-------------- .../pro/qyoga/i9ns/email/EmailI9nsConf.kt | 9 ++++ .../email/NewUserRegisteredMessageChannel.kt | 27 ++++++++++ .../qyoga/i9ns/email/WelcomeMessageChannel.kt | 41 +++++++++++++++ .../pro/qyoga/{i9ns => infra}/email/Email.kt | 4 +- .../{i9ns => infra}/email/EmailSender.kt | 4 +- .../{i9ns => infra}/email/EmailsConfig.kt | 4 +- .../pro/qyoga/tests/cases/arch/ArchTest.kt | 2 +- queries/queries.sql | 2 +- 10 files changed, 102 insertions(+), 49 deletions(-) create mode 100644 app/src/main/kotlin/pro/qyoga/i9ns/email/EmailI9nsConf.kt create mode 100644 app/src/main/kotlin/pro/qyoga/i9ns/email/NewUserRegisteredMessageChannel.kt create mode 100644 app/src/main/kotlin/pro/qyoga/i9ns/email/WelcomeMessageChannel.kt rename app/src/main/kotlin/pro/qyoga/{i9ns => infra}/email/Email.kt (76%) rename app/src/main/kotlin/pro/qyoga/{i9ns => infra}/email/EmailSender.kt (95%) rename app/src/main/kotlin/pro/qyoga/{i9ns => infra}/email/EmailsConfig.kt (75%) diff --git a/app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt b/app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt index 173189f95..77fcc052f 100644 --- a/app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt +++ b/app/src/main/kotlin/pro/qyoga/app/QYogaApp.kt @@ -15,11 +15,12 @@ import pro.qyoga.core.therapy.TherapyConfig import pro.qyoga.core.users.UsersConfig import pro.qyoga.i9ns.calendars.google.GoogleCalendarConf import pro.qyoga.i9ns.calendars.ical.ICalCalendarsConfig -import pro.qyoga.i9ns.email.EmailsConfig +import pro.qyoga.i9ns.email.EmailI9nsConf import pro.qyoga.i9ns.pushes.web.WebPushesConf import pro.qyoga.infra.auth.AuthConfig import pro.qyoga.infra.cache.CacheConf import pro.qyoga.infra.db.SdjConfig +import pro.qyoga.infra.email.EmailsConfig import pro.qyoga.infra.minio.MinioConfig import pro.qyoga.infra.timezones.TimeZonesConfig import pro.qyoga.infra.web.ThymeleafConfig @@ -40,7 +41,7 @@ import pro.qyoga.tech.captcha.CaptchaConf CalendarGatewaysConf::class, // I9ns - EmailsConfig::class, + EmailI9nsConf::class, ICalCalendarsConfig::class, GoogleCalendarConf::class, WebPushesConf::class, @@ -57,7 +58,8 @@ import pro.qyoga.tech.captcha.CaptchaConf MinioConfig::class, FilesStorageConfig::class, TimeZonesConfig::class, - CacheConf::class + CacheConf::class, + EmailsConfig::class, ) @SpringBootApplication class QYogaApp diff --git a/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt b/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt index f93f561a6..4394a6336 100644 --- a/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt +++ b/app/src/main/kotlin/pro/qyoga/app/publc/register/RegisterTherapistOp.kt @@ -1,7 +1,6 @@ package pro.qyoga.app.publc.register import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import pro.azhidkov.platform.errors.DomainError import pro.azhidkov.platform.kotlin.mapFailure @@ -10,8 +9,8 @@ import pro.qyoga.core.users.auth.errors.DuplicatedEmailException import pro.qyoga.core.users.therapists.CreateTherapistUserOp import pro.qyoga.core.users.therapists.RegisterTherapistRequest import pro.qyoga.core.users.therapists.Therapist -import pro.qyoga.i9ns.email.Email -import pro.qyoga.i9ns.email.EmailSender +import pro.qyoga.i9ns.email.NewUserRegisteredMessageChannel +import pro.qyoga.i9ns.email.WelcomeMessageChannel import pro.qyoga.tech.captcha.CaptchaService import java.awt.image.BufferedImage import java.util.* @@ -52,15 +51,14 @@ class RegistrationException( @Component class RegisterTherapistOp( private val createTherapistUser: CreateTherapistUserOp, - private val emailSender: EmailSender, private val captchaService: CaptchaService, - @Value("\${spring.mail.username}") private val fromEmail: String, - @Value("\${trainer-advisor.admin.email}") private val adminEmail: String -) : (RegisterTherapistRequest) -> Therapist { + private val welcomeMessageChannel: WelcomeMessageChannel, + private val newUserRegisteredMessageChannel: NewUserRegisteredMessageChannel, +) { private val log = LoggerFactory.getLogger(javaClass) - override fun invoke(registerTherapistRequest: RegisterTherapistRequest): Therapist { + operator fun invoke(registerTherapistRequest: RegisterTherapistRequest): Therapist { if (captchaService.isInvalid(registerTherapistRequest.captchaAnswer)) { log.info("Register therapist with invalid captcha request terminated") throw RegistrationException.invalidCaptcha(newCaptcha = captchaService.generateCaptcha()) @@ -79,43 +77,19 @@ class RegisterTherapistOp( } .getOrThrow() - emailSender.sendEmail( - welcomeEmail(to = registerTherapistRequest.email, password = password) + welcomeMessageChannel.sendWelcomeMessage( + therapistEmail = registerTherapistRequest.email, + password = password + ) + newUserRegisteredMessageChannel.sendNewUserRegisteredMessage( + therapistEmail = registerTherapistRequest.email ) - emailSender.sendEmail(newRegistrationEmail(to = adminEmail, therapistEmail = registerTherapistRequest.email)) log.info("Therapist for {} registered", registerTherapistRequest.email) return therapist } - private fun welcomeEmail(to: String, password: SecretChars) = - Email( - fromEmail, - to, - "Добро пожаловать в Trainer Advisor", - """ - Здравствуйте! - - Вы зарегистрировались в Trainer Advisor. - Логин от вашего аккаунта: $to. - Пароль от вашего аккаунта: ${password.show()}. - - Теперь вы можете войти в систему на странице https://trainer-advisor.pro/login используя email и пароль из этого письма. - - С уважением, команда Trainer Advisor. - """.trimIndent() - ) - - private fun newRegistrationEmail(to: String, therapistEmail: String) = Email( - fromEmail, - to, - "Новый терапевт добавлен в систему", - """ - Email: $therapistEmail - """.trimIndent() - ) - } private val passwordChars = ('a'..'z').toList() + ('A'..'Z').toList() + ('0'..'9').toList() diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailI9nsConf.kt b/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailI9nsConf.kt new file mode 100644 index 000000000..9595310ac --- /dev/null +++ b/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailI9nsConf.kt @@ -0,0 +1,9 @@ +package pro.qyoga.i9ns.email + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + + +@Configuration +@ComponentScan +class EmailI9nsConf diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/email/NewUserRegisteredMessageChannel.kt b/app/src/main/kotlin/pro/qyoga/i9ns/email/NewUserRegisteredMessageChannel.kt new file mode 100644 index 000000000..821a79d7f --- /dev/null +++ b/app/src/main/kotlin/pro/qyoga/i9ns/email/NewUserRegisteredMessageChannel.kt @@ -0,0 +1,27 @@ +package pro.qyoga.i9ns.email + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import pro.qyoga.infra.email.Email +import pro.qyoga.infra.email.EmailSender + + +@Component +class NewUserRegisteredMessageChannel( + private val emailSender: EmailSender, + @Value("\${spring.mail.username}") private val fromEmail: String, + @Value("\${trainer-advisor.admin.email}") private val adminEmail: String +) { + + fun sendNewUserRegisteredMessage(therapistEmail: String) { + emailSender.sendEmail(newRegistrationEmail(adminEmail, therapistEmail)) + } + + private fun newRegistrationEmail(to: String, therapistEmail: String) = Email( + from = fromEmail, + to = to, + subject = "Новый терапевт добавлен в систему", + text = "Email: $therapistEmail" + ) + +} diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/email/WelcomeMessageChannel.kt b/app/src/main/kotlin/pro/qyoga/i9ns/email/WelcomeMessageChannel.kt new file mode 100644 index 000000000..fc228b836 --- /dev/null +++ b/app/src/main/kotlin/pro/qyoga/i9ns/email/WelcomeMessageChannel.kt @@ -0,0 +1,41 @@ +package pro.qyoga.i9ns.email + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import pro.azhidkov.platform.secrets.SecretChars +import pro.qyoga.infra.email.Email +import pro.qyoga.infra.email.EmailSender + + +@Component +class WelcomeMessageChannel( + private val emailSender: EmailSender, + @Value("\${spring.mail.username}") private val fromEmail: String, +) { + + fun sendWelcomeMessage( + therapistEmail: String, + password: SecretChars + ) { + emailSender.sendEmail(welcomeEmail(therapistEmail, password.show())) + } + + private fun welcomeEmail(to: String, password: String) = + Email( + fromEmail, + to, + "Добро пожаловать в Trainer Advisor", + """ + Здравствуйте! + + Вы зарегистрировались в Trainer Advisor. + Логин от вашего аккаунта: $to. + Пароль от вашего аккаунта: $password. + + Теперь вы можете войти в систему на странице https://trainer-advisor.pro/login используя email и пароль из этого письма. + + С уважением, команда Trainer Advisor. + """.trimIndent() + ) + +} diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/email/Email.kt b/app/src/main/kotlin/pro/qyoga/infra/email/Email.kt similarity index 76% rename from app/src/main/kotlin/pro/qyoga/i9ns/email/Email.kt rename to app/src/main/kotlin/pro/qyoga/infra/email/Email.kt index 93ef6070c..922c02ef8 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/email/Email.kt +++ b/app/src/main/kotlin/pro/qyoga/infra/email/Email.kt @@ -1,8 +1,8 @@ -package pro.qyoga.i9ns.email +package pro.qyoga.infra.email data class Email( val from: String, val to: String, val subject: String, val text: String -) \ No newline at end of file +) diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailSender.kt b/app/src/main/kotlin/pro/qyoga/infra/email/EmailSender.kt similarity index 95% rename from app/src/main/kotlin/pro/qyoga/i9ns/email/EmailSender.kt rename to app/src/main/kotlin/pro/qyoga/infra/email/EmailSender.kt index f54cdc70d..e01305925 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailSender.kt +++ b/app/src/main/kotlin/pro/qyoga/infra/email/EmailSender.kt @@ -1,4 +1,4 @@ -package pro.qyoga.i9ns.email +package pro.qyoga.infra.email import org.slf4j.LoggerFactory import org.springframework.mail.javamail.JavaMailSender @@ -25,4 +25,4 @@ class EmailSender( mailSender.send(mail) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailsConfig.kt b/app/src/main/kotlin/pro/qyoga/infra/email/EmailsConfig.kt similarity index 75% rename from app/src/main/kotlin/pro/qyoga/i9ns/email/EmailsConfig.kt rename to app/src/main/kotlin/pro/qyoga/infra/email/EmailsConfig.kt index 091151211..58bd0ff85 100644 --- a/app/src/main/kotlin/pro/qyoga/i9ns/email/EmailsConfig.kt +++ b/app/src/main/kotlin/pro/qyoga/infra/email/EmailsConfig.kt @@ -1,8 +1,8 @@ -package pro.qyoga.i9ns.email +package pro.qyoga.infra.email import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration @ComponentScan @Configuration -class EmailsConfig \ No newline at end of file +class EmailsConfig diff --git a/app/src/test/kotlin/pro/qyoga/tests/cases/arch/ArchTest.kt b/app/src/test/kotlin/pro/qyoga/tests/cases/arch/ArchTest.kt index 21490a235..3e471e099 100644 --- a/app/src/test/kotlin/pro/qyoga/tests/cases/arch/ArchTest.kt +++ b/app/src/test/kotlin/pro/qyoga/tests/cases/arch/ArchTest.kt @@ -32,7 +32,7 @@ class ArchTest { .whereLayer("App").mayOnlyBeAccessedByLayers(testsAbstractionLayer) .whereLayer("I9ns").mayOnlyBeAccessedByLayers("App", testsAbstractionLayer) .whereLayer("Core").mayOnlyBeAccessedByLayers("App", "I9ns", testsAbstractionLayer) - .whereLayer("Infra").mayOnlyBeAccessedByLayers("Core", "App", testsAbstractionLayer) + .whereLayer("Infra").mayOnlyBeAccessedByLayers("Core", "App", "I9ns", testsAbstractionLayer) .whereLayer("Platform").mayOnlyBeAccessedByLayers("App", "I9ns", "Core", "Infra", testsAbstractionLayer) .check(qyogaClasses) } diff --git a/queries/queries.sql b/queries/queries.sql index 6a0354465..a72ab865d 100644 --- a/queries/queries.sql +++ b/queries/queries.sql @@ -29,4 +29,4 @@ FROM generate_series(now() - '1 month'::interval, now(), '1 day'::interval) date LEFT JOIN clients c ON c.id = je.client_ref LEFT JOIN therapists t ON t.id = c.therapist_ref GROUP by dates.dates -ORDER BY dates.dates DESC; \ No newline at end of file +ORDER BY dates.dates DESC;