From e5d0c05001eab45025f41709f29665943021dc32 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sun, 20 Jul 2025 18:46:04 +0330 Subject: [PATCH 1/9] Add OTP to the email and mobile number update services --- .../kotlin/co/nilin/opex/common/OpexError.kt | 23 +++++--- profile/pom.xml | 1 + .../app/controller/ProfileController.kt | 32 ++++++++-- .../profile/app/service/ProfileManagement.kt | 58 ++++++++++++++----- .../src/main/resources/application.yml | 7 ++- .../profile/core/data/otp/NewOTPRequest.kt | 7 +++ .../opex/profile/core/data/otp/OTPCode.kt | 6 ++ .../opex/profile/core/data/otp/OTPReceiver.kt | 6 ++ .../profile/core/data/otp/OTPResultType.kt | 5 ++ .../opex/profile/core/data/otp/OTPType.kt | 7 +++ .../core/data/otp/OTPVerifyResponse.kt | 6 ++ .../profile/core/data/otp/TempOtpResponse.kt | 3 + .../profile/core/data/otp/VerifyOTPRequest.kt | 6 ++ .../nilin/opex/profile/core/spi/OtpProxy.kt | 11 ++++ .../opex/profile/core/spi/ProfilePersister.kt | 2 + .../profile-otp-proxy/.gitignore | 33 +++++++++++ .../profile-ports/profile-otp-proxy/pom.xml | 43 ++++++++++++++ .../profile/ports/otp/impl/OtpProxyImpl.kt | 45 ++++++++++++++ .../src/main/resources/application.properties | 1 + .../ports/postgres/dao/ProfileRepository.kt | 3 + .../postgres/imp/ProfileManagementImp.kt | 52 ++++++++++++++--- 21 files changed, 319 insertions(+), 38 deletions(-) create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/NewOTPRequest.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPCode.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPReceiver.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPResultType.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPType.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPVerifyResponse.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/TempOtpResponse.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/VerifyOTPRequest.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/OtpProxy.kt create mode 100644 profile/profile-ports/profile-otp-proxy/.gitignore create mode 100644 profile/profile-ports/profile-otp-proxy/pom.xml create mode 100644 profile/profile-ports/profile-otp-proxy/src/main/kotlin/co/nilin/opex/profile/ports/otp/impl/OtpProxyImpl.kt create mode 100644 profile/profile-ports/profile-otp-proxy/src/main/resources/application.properties diff --git a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt index 33b7e7711..f46de600d 100644 --- a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt +++ b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt @@ -137,14 +137,21 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus //code 12000 profile - UserIdAlreadyExists(130001, "User with this id or email is already registered", HttpStatus.BAD_REQUEST), - InvalidLinkedAccount(130002, "Irrelevant account", HttpStatus.BAD_REQUEST), - AccountNotFound(130003, " Account not found", HttpStatus.BAD_REQUEST), - DuplicateAccount(130004, " Duplicate account", HttpStatus.BAD_REQUEST), - InvalidIban(130005, " Invalid iban number", HttpStatus.BAD_REQUEST), - InvalidCard(130006, " Invalid card number", HttpStatus.BAD_REQUEST), - VerificationFailed(130007, "Verification Failed", HttpStatus.BAD_REQUEST), - ProfileApprovalRequestAlreadyExists(130008, "Request Already Exists", HttpStatus.BAD_REQUEST), + UserIdAlreadyExists(13001, "User with this id or email is already registered", HttpStatus.BAD_REQUEST), + InvalidLinkedAccount(13002, "Irrelevant account", HttpStatus.BAD_REQUEST), + AccountNotFound(13003, " Account not found", HttpStatus.BAD_REQUEST), + DuplicateAccount(13004, " Duplicate account", HttpStatus.BAD_REQUEST), + InvalidIban(13005, " Invalid iban number", HttpStatus.BAD_REQUEST), + InvalidCard(13006, " Invalid card number", HttpStatus.BAD_REQUEST), + VerificationFailed(13007, "Verification Failed", HttpStatus.BAD_REQUEST), + ProfileApprovalRequestAlreadyExists(13008, "Request Already Exists", HttpStatus.BAD_REQUEST), + ProfileNotfound(13009, "Profile not found", HttpStatus.NOT_FOUND), + EmailAlreadyExists(13010, "Email already exists", HttpStatus.BAD_REQUEST), + MobileAlreadyExists(13011, "Mobile already exists", HttpStatus.BAD_REQUEST), + InvalidEmail(13012, "Invalid email", HttpStatus.BAD_REQUEST), + InvalidMobile(13013, "Invalid mobile", HttpStatus.BAD_REQUEST), + MobileAlreadySet(13014, "Mobile already set", HttpStatus.BAD_REQUEST), + EmailAlreadySet(13015, "Email already set", HttpStatus.BAD_REQUEST), ; override fun code() = this.code diff --git a/profile/pom.xml b/profile/pom.xml index a9bbbabc8..776b45e13 100644 --- a/profile/pom.xml +++ b/profile/pom.xml @@ -26,6 +26,7 @@ profile-ports/profile-submitter-kafka profile-ports/profile-kyc-proxy profile-ports/profile-shahkar-proxy + profile-ports/profile-otp-proxy diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt index c2c7d907e..e1cc316c0 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt @@ -1,6 +1,7 @@ package co.nilin.opex.profile.app.controller import co.nilin.opex.profile.app.service.ProfileManagement +import co.nilin.opex.profile.core.data.otp.TempOtpResponse import co.nilin.opex.profile.core.data.profile.CompleteProfileRequest import co.nilin.opex.profile.core.data.profile.CompleteProfileResponse import co.nilin.opex.profile.core.data.profile.Profile @@ -11,7 +12,7 @@ import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/v2/profile") +@RequestMapping class ProfileController(val profileManagement: ProfileManagement) { @@ -37,20 +38,39 @@ class ProfileController(val profileManagement: ProfileManagement) { return profileManagement.completeProfile(securityContext.authentication.name, completeProfileRequest) } - //TODO update mobile and email need improvement - @PutMapping("/mobile/{mobile}") + //TODO use dto for input + @PutMapping("/request/mobile/{mobile}") + suspend fun requestUpdateMobile( + @PathVariable mobile: String, + @CurrentSecurityContext securityContext: SecurityContext + ): TempOtpResponse { + return profileManagement.requestUpdateMobile(securityContext.authentication.name, mobile) + } + + @PutMapping("/mobile/{mobile}/{otpCode}") suspend fun updateMobile( @PathVariable mobile: String, + @PathVariable otpCode: String, @CurrentSecurityContext securityContext: SecurityContext ) { - profileManagement.updateMobile(securityContext.authentication.name, mobile) + profileManagement.updateMobile(securityContext.authentication.name, mobile, otpCode) + } + + //TODO use dto for input + @PutMapping("/request/email/{email}") + suspend fun requestUpdateEmail( + @PathVariable email: String, + @CurrentSecurityContext securityContext: SecurityContext + ): TempOtpResponse { + return profileManagement.requestUpdateEmail(securityContext.authentication.name, email) } - @PutMapping("/email/{email}") + @PutMapping("/email/{email}/{otpCode}") suspend fun updateEmail( @PathVariable email: String, + @PathVariable otpCode: String, @CurrentSecurityContext securityContext: SecurityContext ) { - profileManagement.updateEmail(securityContext.authentication.name, email) + profileManagement.updateEmail(securityContext.authentication.name, email, otpCode) } } \ No newline at end of file diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt index c48dd718c..d8e50a05c 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt @@ -5,6 +5,7 @@ import co.nilin.opex.common.OpexError import co.nilin.opex.profile.core.data.event.KycLevelUpdatedEvent import co.nilin.opex.profile.core.data.event.UserCreatedEvent import co.nilin.opex.profile.core.data.kyc.KycLevel +import co.nilin.opex.profile.core.data.otp.* import co.nilin.opex.profile.core.data.profile.* import co.nilin.opex.profile.core.spi.* import kotlinx.coroutines.flow.map @@ -22,7 +23,8 @@ class ProfileManagement( private val limitationPersister: LimitationPersister, private val profileApprovalRequestPersister: ProfileApprovalRequestPersister, private val shahkarInquiry: ShahkarInquiry, - private val kycLevelUpdatedPublisher: KycLevelUpdatedPublisher + private val kycLevelUpdatedPublisher: KycLevelUpdatedPublisher, + private val otpProxy: OtpProxy, ) { private val logger = LoggerFactory.getLogger(ProfileManagement::class.java) suspend fun registerNewUser(event: UserCreatedEvent) { @@ -101,24 +103,50 @@ class ProfileManagement( profilePersister.updateUserLevel(userId, userLevel) } - //TODO Need OTP - suspend fun updateMobile(userId: String, mobile: String) { - val profile = profilePersister.getProfile(userId)?.awaitFirstOrNull() - ?: throw OpexError.NotFound.exception("profile not found") - if (profile.mobile.isNullOrEmpty()) + suspend fun requestUpdateMobile(userId: String, mobile: String): TempOtpResponse { + profilePersister.validateMobileForUpdate(userId, mobile) + return otpProxy.requestOtp( + NewOTPRequest( + userId, + listOf(OTPReceiver(userId, OTPType.SMS)), + "UPDATE_MOBILE" + ) + ) //TODO fix action + } + + suspend fun updateMobile(userId: String, mobile: String, otpCode: String) { + val verifyResponse = otpProxy.verifyOtp( + VerifyOTPRequest( + userId, + listOf(OTPCode(OTPType.SMS, otpCode)) + ) + ) + if (verifyResponse.result) profilePersister.updateMobile(userId, mobile) - else - throw OpexError.BadRequest.exception("Mobile cannot be changed") + else throw OpexError.InvalidOTP.exception() } - //TODO Need OTP - suspend fun updateEmail(userId: String, email: String) { - val profile = profilePersister.getProfile(userId)?.awaitFirstOrNull() - ?: throw OpexError.NotFound.exception("profile not found") - if (profile.email.isNullOrEmpty()) + suspend fun requestUpdateEmail(userId: String, email: String): TempOtpResponse { + profilePersister.validateEmailForUpdate(userId, email) + return otpProxy.requestOtp( + NewOTPRequest( + userId, + listOf(OTPReceiver(userId, OTPType.EMAIL)), + "UPDATE_EMAIL" + ) + ) //TODO fix action + } + + suspend fun updateEmail(userId: String, email: String, otpCode: String) { + val verifyResponse = otpProxy.verifyOtp( + VerifyOTPRequest( + userId, + listOf(OTPCode(OTPType.EMAIL, otpCode)) + ) + ) + if (verifyResponse.result) profilePersister.updateEmail(userId, email) - else - throw OpexError.BadRequest.exception("Email cannot be changed") + else throw OpexError.InvalidOTP.exception() } suspend fun completeProfile( diff --git a/profile/profile-app/src/main/resources/application.yml b/profile/profile-app/src/main/resources/application.yml index 00398823d..d7a58e4f8 100644 --- a/profile/profile-app/src/main/resources/application.yml +++ b/profile/profile-app/src/main/resources/application.yml @@ -51,6 +51,8 @@ app: cert-url: http://keycloak:8080/realms/opex/protocol/openid-connect/certs kyc: url: lb://opex-kyc/v2/admin/kyc/internal + otp: + url: http://opex-otp/v1 management: endpoints: web: @@ -63,4 +65,7 @@ management: metrics: enabled: true prometheus: - enabled: true \ No newline at end of file + enabled: true +inquiry: + shahkar: true + comparative: true \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/NewOTPRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/NewOTPRequest.kt new file mode 100644 index 000000000..ce9a1f99e --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/NewOTPRequest.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.profile.core.data.otp + +data class NewOTPRequest( + val userId: String, + val receivers: List, + val action: String? +) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPCode.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPCode.kt new file mode 100644 index 000000000..623f4173b --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPCode.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.core.data.otp + +data class OTPCode( + val type: OTPType, + val code: String +) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPReceiver.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPReceiver.kt new file mode 100644 index 000000000..55128575f --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPReceiver.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.core.data.otp + +data class OTPReceiver( + val receiver: String, + val type: OTPType, +) \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPResultType.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPResultType.kt new file mode 100644 index 000000000..6659ef91f --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPResultType.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.profile.core.data.otp + +enum class OTPResultType { + VALID, EXPIRED, INCORRECT, INVALID +} \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPType.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPType.kt new file mode 100644 index 000000000..5ff8c116a --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPType.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.profile.core.data.otp + +enum class OTPType(val compositeOrder: Int) { + + SMS(0), EMAIL(1), + COMPOSITE(99) +} \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPVerifyResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPVerifyResponse.kt new file mode 100644 index 000000000..984908862 --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPVerifyResponse.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.core.data.otp + +data class OTPVerifyResponse( + val result: Boolean, + val type: OTPResultType +) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/TempOtpResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/TempOtpResponse.kt new file mode 100644 index 000000000..5caec73ef --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/TempOtpResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.profile.core.data.otp + +data class TempOtpResponse(val otp: String) \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/VerifyOTPRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/VerifyOTPRequest.kt new file mode 100644 index 000000000..4d9129749 --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/VerifyOTPRequest.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.core.data.otp + +data class VerifyOTPRequest( + val userId: String, + val otpCodes: List +) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/OtpProxy.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/OtpProxy.kt new file mode 100644 index 000000000..e8f42b9aa --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/OtpProxy.kt @@ -0,0 +1,11 @@ +package co.nilin.opex.profile.core.spi + +import co.nilin.opex.profile.core.data.otp.NewOTPRequest +import co.nilin.opex.profile.core.data.otp.OTPVerifyResponse +import co.nilin.opex.profile.core.data.otp.TempOtpResponse +import co.nilin.opex.profile.core.data.otp.VerifyOTPRequest + +interface OtpProxy { + suspend fun requestOtp(newOTPRequest: NewOTPRequest) : TempOtpResponse + suspend fun verifyOtp(verifyOTPRequest: VerifyOTPRequest) : OTPVerifyResponse +} \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt index 77f6b1775..449803857 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt @@ -16,6 +16,8 @@ interface ProfilePersister { suspend fun getAllProfile(offset: Int, size: Int, profileRequest: ProfileRequest): Flow? suspend fun getHistory(userId: String, offset: Int, size: Int): List suspend fun updateUserLevel(userId: String, userLevel: KycLevel) + suspend fun validateEmailForUpdate(userId: String, email: String) + suspend fun validateMobileForUpdate(userId: String, mobile: String) suspend fun updateMobile(userId: String, mobile: String) suspend fun updateEmail(userId: String, email: String) } diff --git a/profile/profile-ports/profile-otp-proxy/.gitignore b/profile/profile-ports/profile-otp-proxy/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/profile/profile-ports/profile-otp-proxy/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/profile/profile-ports/profile-otp-proxy/pom.xml b/profile/profile-ports/profile-otp-proxy/pom.xml new file mode 100644 index 000000000..4b0c744b6 --- /dev/null +++ b/profile/profile-ports/profile-otp-proxy/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + co.nilin.opex + profile + 1.0.1-beta.7 + ../../pom.xml + + co.nilin.opex.profile.ports + profile-otp-proxy + profile-otp-proxy + profile-otp-proxy + + + + org.springframework.boot + spring-boot-starter + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib + + + + org.springframework.boot + spring-boot-starter-test + test + + + + co.nilin.opex.profile + profile-core + + + + + diff --git a/profile/profile-ports/profile-otp-proxy/src/main/kotlin/co/nilin/opex/profile/ports/otp/impl/OtpProxyImpl.kt b/profile/profile-ports/profile-otp-proxy/src/main/kotlin/co/nilin/opex/profile/ports/otp/impl/OtpProxyImpl.kt new file mode 100644 index 000000000..c8fa2a788 --- /dev/null +++ b/profile/profile-ports/profile-otp-proxy/src/main/kotlin/co/nilin/opex/profile/ports/otp/impl/OtpProxyImpl.kt @@ -0,0 +1,45 @@ +package co.nilin.opex.profile.ports.otp.impl + +import co.nilin.opex.profile.core.data.otp.NewOTPRequest +import co.nilin.opex.profile.core.data.otp.OTPVerifyResponse +import co.nilin.opex.profile.core.data.otp.TempOtpResponse +import co.nilin.opex.profile.core.data.otp.VerifyOTPRequest +import co.nilin.opex.profile.core.spi.OtpProxy +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBody + +@Component +class OtpProxyImpl(private val webClient: WebClient) : OtpProxy { + + @Value("\${app.otp.url}") + private lateinit var baseUrl: String + + override suspend fun requestOtp(newOTPRequest: NewOTPRequest): TempOtpResponse { + return webClient.post().uri("$baseUrl/otp") + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(newOTPRequest)) + .retrieve() + .awaitBody() + } + + override suspend fun verifyOtp(verifyOTPRequest: VerifyOTPRequest): OTPVerifyResponse { + val request = object { + val userId = verifyOTPRequest.userId + val otpCodes = verifyOTPRequest.otpCodes.map { + object { + val type = it.type + val code = it.code + } + } + } + return webClient.post().uri("$baseUrl/otp/verify") + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(request)) + .retrieve() + .awaitBody() + } +} diff --git a/profile/profile-ports/profile-otp-proxy/src/main/resources/application.properties b/profile/profile-ports/profile-otp-proxy/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/profile/profile-ports/profile-otp-proxy/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt index c4c512299..0134d84a8 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/dao/ProfileRepository.kt @@ -47,5 +47,8 @@ interface ProfileRepository : ReactiveCrudRepository { """) fun findByUserIdOrEmailOrMobile(userId: String, email: String? , mobile : String?): Mono? + fun findByMobile(mobile : String?): Mono? + + fun findByEmail(email: String?): Mono? } \ No newline at end of file diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt index df35d92dc..87df0fdfc 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt @@ -37,6 +37,8 @@ class ProfileManagementImp( private var kycProxyImp: KycProxyImp, ) : ProfilePersister { private val logger = LoggerFactory.getLogger(ProfileManagementImp::class.java) + private val EmailRegex = Regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") + private val MobileRegex = Regex("^09\\d{9}$") @Transactional override suspend fun updateProfile(id: String, data: UpdateProfileRequest): Mono { @@ -192,18 +194,44 @@ class ProfileManagementImp( } ?: throw OpexError.UserNotFound.exception() } + override suspend fun validateEmailForUpdate(userId: String, email: String) { + validateEmailFormat(email) + + if (profileRepository.findByEmail(email)?.awaitFirstOrNull() != null) + throw OpexError.EmailAlreadyExists.exception() + + val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() + ?: throw OpexError.ProfileNotfound.exception() + + if (!profile.email.isNotEmpty()) + throw OpexError.EmailAlreadySet.exception() + } + + override suspend fun validateMobileForUpdate(userId: String, mobile: String) { + validateMobileFormat(mobile) + + if (profileRepository.findByMobile(mobile)?.awaitFirstOrNull() != null) + throw OpexError.MobileAlreadyExists.exception() + + val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() + ?: throw OpexError.ProfileNotfound.exception() + + if (!profile.mobile.isNullOrEmpty()) + throw OpexError.MobileAlreadySet.exception() + } + override suspend fun updateMobile(userId: String, mobile: String) { - val profileModel = profileRepository.findByUserId(userId)?.awaitFirstOrNull() - ?: throw OpexError.UserNotFound.exception() - profileModel.mobile = mobile - profileRepository.save(profileModel).awaitFirstOrNull() + val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() + ?: throw OpexError.ProfileNotfound.exception() + profile.mobile = mobile + profileRepository.save(profile).awaitFirstOrNull() } override suspend fun updateEmail(userId: String, email: String) { - val profileModel = profileRepository.findByUserId(userId)?.awaitFirstOrNull() - ?: throw OpexError.UserNotFound.exception() - profileModel.email = email - profileRepository.save(profileModel).awaitFirstOrNull() + val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() + ?: throw OpexError.ProfileNotfound.exception() + profile.email = email + profileRepository.save(profile).awaitFirstOrNull() } fun isMajorChanges(oldData: ProfileModel, newData: UpdateProfileRequest): Boolean { @@ -258,4 +286,12 @@ class ProfileManagementImp( this.description = reason }) } + + private fun validateEmailFormat(email: String) { + if (!EmailRegex.matches(email)) throw OpexError.InvalidEmail.exception() + } + + private fun validateMobileFormat(mobile: String) { + if (!MobileRegex.matches(mobile)) throw OpexError.InvalidMobile.exception() + } } \ No newline at end of file From a006f21e7ec86b84ea3672650e6e1f0a13bf4397 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Mon, 21 Jul 2025 17:05:13 +0330 Subject: [PATCH 2/9] Complete OTP flow and update profile data in keycloak --- .../opex/auth/controller/UserController.kt | 28 ++++++++-- .../co/nilin/opex/auth/model/UserUpdate.kt | 17 ++++++ .../co/nilin/opex/auth/proxy/KeycloakProxy.kt | 52 ++++++++++++++++++- .../co/nilin/opex/auth/service/UserService.kt | 12 +++++ profile/pom.xml | 11 ++++ profile/profile-app/pom.xml | 8 +++ .../app/controller/ProfileController.kt | 36 ++++++------- .../profile/app/dto/UpdateEmailRequest.kt | 5 ++ .../profile/app/dto/UpdateMobileRequest.kt | 5 ++ .../app/dto/VerifyUpdateEmailRequest.kt | 6 +++ .../app/dto/VerifyUpdateMobileRequest.kt | 6 +++ .../profile/app/service/ProfileManagement.kt | 31 ++++++----- .../src/main/resources/application.yml | 2 + .../opex/profile/core/data/otp/OTPAction.kt | 5 ++ .../nilin/opex/profile/core/spi/AuthProxy.kt | 9 ++++ .../profile-auth-proxy/.gitignore | 33 ++++++++++++ .../profile-ports/profile-auth-proxy/pom.xml | 43 +++++++++++++++ .../profile/ports/auth/impl/AuthProxyImpl.kt | 39 ++++++++++++++ .../src/main/resources/application.properties | 1 + .../postgres/imp/ProfileManagementImp.kt | 14 ++--- .../ports/postgres/model/base/Profile.kt | 2 +- 21 files changed, 321 insertions(+), 44 deletions(-) create mode 100644 auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserUpdate.kt create mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt create mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt create mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt create mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPAction.kt create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AuthProxy.kt create mode 100644 profile/profile-ports/profile-auth-proxy/.gitignore create mode 100644 profile/profile-ports/profile-auth-proxy/pom.xml create mode 100644 profile/profile-ports/profile-auth-proxy/src/main/kotlin/co/nilin/opex/profile/ports/auth/impl/AuthProxyImpl.kt create mode 100644 profile/profile-ports/profile-auth-proxy/src/main/resources/application.properties diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt index 54325c1d2..2913c1e11 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt @@ -1,12 +1,13 @@ package co.nilin.opex.auth.controller +import co.nilin.opex.auth.model.UpdateEmailRequest +import co.nilin.opex.auth.model.UpdateMobileRequest +import co.nilin.opex.auth.model.UpdateNameRequest import co.nilin.opex.auth.service.UserService import co.nilin.opex.auth.utils.jwtAuthentication import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/v1/user") @@ -17,5 +18,26 @@ class UserController(private val userService: UserService) { userService.logout(securityContext.jwtAuthentication().name) } + @PutMapping("/email") + suspend fun updateEmail( + @RequestBody request: UpdateEmailRequest + ) { + userService.updateEmail(request) + } + + @PutMapping("/mobile") + suspend fun updateMobile( + @RequestBody request: UpdateMobileRequest + ) { + userService.updateMobile(request) + + } + + @PutMapping("/name") + suspend fun updateName( + @RequestBody request: UpdateNameRequest + ) { + userService.updateName(request) + } } diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserUpdate.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserUpdate.kt new file mode 100644 index 000000000..558eb55ce --- /dev/null +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/model/UserUpdate.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.auth.model + +data class UpdateMobileRequest( + val userId :String, + val mobile: String +) + +data class UpdateEmailRequest( + val userId :String, + val email: String +) + +data class UpdateNameRequest( + val userId :String, + val firstName: String, + val lastName: String +) \ No newline at end of file diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt index eb3a0cc42..ae57deefe 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt @@ -13,7 +13,10 @@ import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.stereotype.Service -import org.springframework.web.reactive.function.client.* +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBodilessEntity +import org.springframework.web.reactive.function.client.awaitBody +import org.springframework.web.reactive.function.client.bodyToMono @Service class KeycloakProxy( @@ -305,4 +308,51 @@ class KeycloakProxy( return this } + suspend fun updateUserMobile(userId: String, newMobile: String) { + val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/$userId" + val patch = mapOf( + "attributes" to mapOf("mobile" to newMobile) + ) + keycloakClient.put() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .withAdminToken() + .bodyValue(patch) + .retrieve() + .toBodilessEntity() + .awaitSingleOrNull() + } + + suspend fun updateUserEmail(userId: String, newEmail: String) { + val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/$userId" + val patch = mapOf( + "email" to newEmail, + "emailVerified" to true + ) + keycloakClient.put() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .withAdminToken() + .bodyValue(patch) + .retrieve() + .toBodilessEntity() + .awaitSingleOrNull() + } + + suspend fun updateUserName(userId: String, firstName: String?, lastName: String?) { + val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/$userId" + val patch = mutableMapOf() + + firstName?.let { patch["firstName"] = it } + lastName?.let { patch["lastName"] = it } + + keycloakClient.put() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .withAdminToken() + .bodyValue(patch) + .retrieve() + .toBodilessEntity() + .awaitSingleOrNull() + } } \ No newline at end of file diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt index 7c55e5aca..fba7ac631 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt @@ -139,6 +139,18 @@ class UserService( keycloakProxy.resetPassword(user.id, request.newPassword) } + suspend fun updateMobile(request: UpdateMobileRequest) { + keycloakProxy.updateUserMobile(request.userId, request.mobile) + } + + suspend fun updateEmail(request: UpdateEmailRequest) { + keycloakProxy.updateUserEmail(request.userId, request.email) + } + + suspend fun updateName(request: UpdateNameRequest) { + keycloakProxy.updateUserName(request.userId, request.firstName, request.lastName) + } + private suspend fun isUserDuplicate(username: Username): Boolean { val user = keycloakProxy.findUserByUsername(username) return if (user == null) diff --git a/profile/pom.xml b/profile/pom.xml index 776b45e13..be123605a 100644 --- a/profile/pom.xml +++ b/profile/pom.xml @@ -27,6 +27,7 @@ profile-ports/profile-kyc-proxy profile-ports/profile-shahkar-proxy profile-ports/profile-otp-proxy + profile-ports/profile-auth-proxy @@ -81,6 +82,16 @@ profile-shahkar-proxy ${project.version} + + co.nilin.opex.profile.ports + profile-otp-proxy + ${project.version} + + + co.nilin.opex.profile.ports + profile-auth-proxy + ${project.version} + diff --git a/profile/profile-app/pom.xml b/profile/profile-app/pom.xml index d5e0d7e3c..d818fb5e6 100644 --- a/profile/profile-app/pom.xml +++ b/profile/profile-app/pom.xml @@ -85,6 +85,14 @@ co.nilin.opex.profile.ports profile-submitter-kafka + + co.nilin.opex.profile.ports + profile-otp-proxy + + + co.nilin.opex.profile.ports + profile-auth-proxy + org.springframework spring-tx diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt index e1cc316c0..c00bc4777 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt @@ -1,5 +1,9 @@ package co.nilin.opex.profile.app.controller +import co.nilin.opex.profile.app.dto.UpdateEmailRequest +import co.nilin.opex.profile.app.dto.UpdateMobileRequest +import co.nilin.opex.profile.app.dto.VerifyUpdateEmailRequest +import co.nilin.opex.profile.app.dto.VerifyUpdateMobileRequest import co.nilin.opex.profile.app.service.ProfileManagement import co.nilin.opex.profile.core.data.otp.TempOtpResponse import co.nilin.opex.profile.core.data.profile.CompleteProfileRequest @@ -38,39 +42,35 @@ class ProfileController(val profileManagement: ProfileManagement) { return profileManagement.completeProfile(securityContext.authentication.name, completeProfileRequest) } - //TODO use dto for input - @PutMapping("/request/mobile/{mobile}") + @PostMapping("/mobile") suspend fun requestUpdateMobile( - @PathVariable mobile: String, + @RequestBody request: UpdateMobileRequest, @CurrentSecurityContext securityContext: SecurityContext ): TempOtpResponse { - return profileManagement.requestUpdateMobile(securityContext.authentication.name, mobile) + return profileManagement.requestUpdateMobile(securityContext.authentication.name, request.mobile) } - @PutMapping("/mobile/{mobile}/{otpCode}") - suspend fun updateMobile( - @PathVariable mobile: String, - @PathVariable otpCode: String, + @PutMapping("/mobile") + suspend fun verifyUpdateMobile( + @RequestBody request: VerifyUpdateMobileRequest, @CurrentSecurityContext securityContext: SecurityContext ) { - profileManagement.updateMobile(securityContext.authentication.name, mobile, otpCode) + profileManagement.updateMobile(securityContext.authentication.name, request.mobile, request.otp) } - //TODO use dto for input - @PutMapping("/request/email/{email}") + @PostMapping("/email") suspend fun requestUpdateEmail( - @PathVariable email: String, + @RequestBody request: UpdateEmailRequest, @CurrentSecurityContext securityContext: SecurityContext ): TempOtpResponse { - return profileManagement.requestUpdateEmail(securityContext.authentication.name, email) + return profileManagement.requestUpdateEmail(securityContext.authentication.name, request.email) } - @PutMapping("/email/{email}/{otpCode}") - suspend fun updateEmail( - @PathVariable email: String, - @PathVariable otpCode: String, + @PutMapping("/email") + suspend fun verifyUpdateEmail( + @RequestBody request: VerifyUpdateEmailRequest, @CurrentSecurityContext securityContext: SecurityContext ) { - profileManagement.updateEmail(securityContext.authentication.name, email, otpCode) + profileManagement.updateEmail(securityContext.authentication.name, request.email, request.otp) } } \ No newline at end of file diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt new file mode 100644 index 000000000..d3d93a6bb --- /dev/null +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.profile.app.dto + +data class UpdateEmailRequest( + val email: String +) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt new file mode 100644 index 000000000..356a43baf --- /dev/null +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.profile.app.dto + +data class UpdateMobileRequest( + val mobile: String +) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt new file mode 100644 index 000000000..e67aa3b83 --- /dev/null +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.app.dto + +data class VerifyUpdateEmailRequest( + val email: String, + val otp: String, +) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt new file mode 100644 index 000000000..2c4a9c220 --- /dev/null +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.app.dto + +data class VerifyUpdateMobileRequest( + val mobile: String, + val otp: String, +) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt index d8e50a05c..33c5427a6 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt @@ -25,6 +25,7 @@ class ProfileManagement( private val shahkarInquiry: ShahkarInquiry, private val kycLevelUpdatedPublisher: KycLevelUpdatedPublisher, private val otpProxy: OtpProxy, + private val authProxy: AuthProxy, ) { private val logger = LoggerFactory.getLogger(ProfileManagement::class.java) suspend fun registerNewUser(event: UserCreatedEvent) { @@ -107,46 +108,48 @@ class ProfileManagement( profilePersister.validateMobileForUpdate(userId, mobile) return otpProxy.requestOtp( NewOTPRequest( - userId, - listOf(OTPReceiver(userId, OTPType.SMS)), - "UPDATE_MOBILE" + mobile, + listOf(OTPReceiver(mobile, OTPType.SMS)), + OTPAction.UPDATE_MOBILE.name ) - ) //TODO fix action + ) } suspend fun updateMobile(userId: String, mobile: String, otpCode: String) { val verifyResponse = otpProxy.verifyOtp( VerifyOTPRequest( - userId, + mobile, listOf(OTPCode(OTPType.SMS, otpCode)) ) ) - if (verifyResponse.result) + if (verifyResponse.result) { + authProxy.updateMobile(userId, mobile) profilePersister.updateMobile(userId, mobile) - else throw OpexError.InvalidOTP.exception() + } else throw OpexError.InvalidOTP.exception() } suspend fun requestUpdateEmail(userId: String, email: String): TempOtpResponse { profilePersister.validateEmailForUpdate(userId, email) return otpProxy.requestOtp( NewOTPRequest( - userId, - listOf(OTPReceiver(userId, OTPType.EMAIL)), - "UPDATE_EMAIL" + email, + listOf(OTPReceiver(email, OTPType.EMAIL)), + OTPAction.UPDATE_EMAIL.name ) - ) //TODO fix action + ) } suspend fun updateEmail(userId: String, email: String, otpCode: String) { val verifyResponse = otpProxy.verifyOtp( VerifyOTPRequest( - userId, + email, listOf(OTPCode(OTPType.EMAIL, otpCode)) ) ) - if (verifyResponse.result) + if (verifyResponse.result) { + authProxy.updateEmail(userId, email) profilePersister.updateEmail(userId, email) - else throw OpexError.InvalidOTP.exception() + } else throw OpexError.InvalidOTP.exception() } suspend fun completeProfile( diff --git a/profile/profile-app/src/main/resources/application.yml b/profile/profile-app/src/main/resources/application.yml index d7a58e4f8..fb094b331 100644 --- a/profile/profile-app/src/main/resources/application.yml +++ b/profile/profile-app/src/main/resources/application.yml @@ -53,6 +53,8 @@ app: url: lb://opex-kyc/v2/admin/kyc/internal otp: url: http://opex-otp/v1 + auth-gateway: + url: http://opex-auth-gateway management: endpoints: web: diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPAction.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPAction.kt new file mode 100644 index 000000000..d24f325e6 --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/otp/OTPAction.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.profile.core.data.otp + +enum class OTPAction { + UPDATE_MOBILE, UPDATE_EMAIL +} \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AuthProxy.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AuthProxy.kt new file mode 100644 index 000000000..695f3871f --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/AuthProxy.kt @@ -0,0 +1,9 @@ +package co.nilin.opex.profile.core.spi + +interface AuthProxy { + + suspend fun updateEmail(userId: String, email: String) + suspend fun updateMobile(userId: String, mobile: String) + suspend fun updateName(userId: String, firstName: String , lastName: String) + +} \ No newline at end of file diff --git a/profile/profile-ports/profile-auth-proxy/.gitignore b/profile/profile-ports/profile-auth-proxy/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/profile/profile-ports/profile-auth-proxy/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/profile/profile-ports/profile-auth-proxy/pom.xml b/profile/profile-ports/profile-auth-proxy/pom.xml new file mode 100644 index 000000000..eb9e42404 --- /dev/null +++ b/profile/profile-ports/profile-auth-proxy/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + co.nilin.opex + profile + 1.0.1-beta.7 + ../../pom.xml + + co.nilin.opex.profile.ports + profile-auth-proxy + profile-auth-proxy + profile-auth-proxy + + + + org.springframework.boot + spring-boot-starter + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib + + + + org.springframework.boot + spring-boot-starter-test + test + + + + co.nilin.opex.profile + profile-core + + + + + diff --git a/profile/profile-ports/profile-auth-proxy/src/main/kotlin/co/nilin/opex/profile/ports/auth/impl/AuthProxyImpl.kt b/profile/profile-ports/profile-auth-proxy/src/main/kotlin/co/nilin/opex/profile/ports/auth/impl/AuthProxyImpl.kt new file mode 100644 index 000000000..6fc55c460 --- /dev/null +++ b/profile/profile-ports/profile-auth-proxy/src/main/kotlin/co/nilin/opex/profile/ports/auth/impl/AuthProxyImpl.kt @@ -0,0 +1,39 @@ +package co.nilin.opex.profile.ports.auth.impl + +import co.nilin.opex.profile.core.spi.AuthProxy +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBodilessEntity + +@Component +class AuthProxyImpl(private val webClient: WebClient) : AuthProxy { + + @Value("\${app.auth-gateway.url}") + private lateinit var baseUrl: String + + override suspend fun updateEmail(userId: String, email: String) { + webClient.put().uri("$baseUrl/v1/user/email") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(mapOf("userId" to userId, "email" to email)) + .retrieve() + .awaitBodilessEntity() + } + + override suspend fun updateMobile(userId: String, mobile: String) { + webClient.put().uri("$baseUrl/v1/user/mobile") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(mapOf("userId" to userId, "mobile" to mobile)) + .retrieve() + .awaitBodilessEntity() + } + + override suspend fun updateName(userId: String, firstName: String, lastName: String) { + webClient.put().uri("$baseUrl/v1/user/name") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(mapOf("userId" to userId, "firstName" to firstName, "lastName" to lastName)) + .retrieve() + .awaitBodilessEntity() + } +} diff --git a/profile/profile-ports/profile-auth-proxy/src/main/resources/application.properties b/profile/profile-ports/profile-auth-proxy/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/profile/profile-ports/profile-auth-proxy/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt index 87df0fdfc..7e944c8d3 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt @@ -197,27 +197,27 @@ class ProfileManagementImp( override suspend fun validateEmailForUpdate(userId: String, email: String) { validateEmailFormat(email) - if (profileRepository.findByEmail(email)?.awaitFirstOrNull() != null) - throw OpexError.EmailAlreadyExists.exception() - val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() ?: throw OpexError.ProfileNotfound.exception() - if (!profile.email.isNotEmpty()) + if (!profile.email.isNullOrEmpty()) throw OpexError.EmailAlreadySet.exception() + + if (profileRepository.findByEmail(email)?.awaitFirstOrNull() != null) + throw OpexError.EmailAlreadyExists.exception() } override suspend fun validateMobileForUpdate(userId: String, mobile: String) { validateMobileFormat(mobile) - if (profileRepository.findByMobile(mobile)?.awaitFirstOrNull() != null) - throw OpexError.MobileAlreadyExists.exception() - val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() ?: throw OpexError.ProfileNotfound.exception() if (!profile.mobile.isNullOrEmpty()) throw OpexError.MobileAlreadySet.exception() + + if (profileRepository.findByMobile(mobile)?.awaitFirstOrNull() != null) + throw OpexError.MobileAlreadyExists.exception() } override suspend fun updateMobile(userId: String, mobile: String) { diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt index 133bf7dcb..d0c01bcb2 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt @@ -6,7 +6,7 @@ import co.nilin.opex.profile.core.data.profile.UserStatus import java.time.LocalDateTime open class Profile { - lateinit var email: String + var email: String? = null lateinit var userId: String var firstName: String? = null var lastName: String? = null From 5f3a0edbf4bcc9e892b01010a2f639d2803ed983 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 23 Jul 2025 17:46:33 +0330 Subject: [PATCH 3/9] Add mobile and personal identity inquiries --- .../kotlin/co/nilin/opex/common/OpexError.kt | 4 +- profile/pom.xml | 4 +- .../profile/app/config/WebClientConfig.kt | 36 +++++--- .../app/controller/ProfileController.kt | 2 +- .../profile/app/service/ProfileManagement.kt | 86 +++++++++++++++---- .../src/main/resources/application.yml | 7 +- .../data/profile/CompleteProfileRequest.kt | 5 +- .../data/profile/CompleteProfileResponse.kt | 3 +- .../opex/profile/core/spi/InquiryProxy.kt | 14 +++ .../opex/profile/core/spi/ProfilePersister.kt | 8 +- .../opex/profile/core/spi/ShahkarInquiry.kt | 5 -- .../.gitignore | 0 .../pom.xml | 7 +- .../ports/inquiry/data/ComparativeResponse.kt | 6 ++ .../ports/inquiry/data/ShahkarResponse.kt | 3 + .../ports/inquiry/data/TokenResponse.kt | 5 ++ .../ports/inquiry/imp/InquiryProxyImp.kt | 64 ++++++++++++++ .../ports/inquiry/utils/DateConvertor.kt | 20 +++++ .../ports/inquiry/utils/TokenProvider.kt | 55 ++++++++++++ .../src/main/resources/application.properties | 0 .../profile-ports/profile-postgres/pom.xml | 2 +- .../postgres/convertor/ProfileConvertor.kt | 3 +- .../postgres/imp/ProfileManagementImp.kt | 44 ++++++---- .../ports/postgres/model/base/Profile.kt | 3 +- .../src/main/resources/schema.sql | 54 ++++++++++-- .../ports/shahkar/imp/ShahkarProxyImp.kt | 12 --- 26 files changed, 366 insertions(+), 86 deletions(-) create mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/InquiryProxy.kt delete mode 100644 profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt rename profile/profile-ports/{profile-shahkar-proxy => profile-inquiry-proxy}/.gitignore (100%) rename profile/profile-ports/{profile-shahkar-proxy => profile-inquiry-proxy}/pom.xml (87%) create mode 100644 profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ComparativeResponse.kt create mode 100644 profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ShahkarResponse.kt create mode 100644 profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/TokenResponse.kt create mode 100644 profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt create mode 100644 profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/DateConvertor.kt create mode 100644 profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/TokenProvider.kt rename profile/profile-ports/{profile-shahkar-proxy => profile-inquiry-proxy}/src/main/resources/application.properties (100%) delete mode 100644 profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt diff --git a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt index f46de600d..3e10d49aa 100644 --- a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt +++ b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt @@ -143,7 +143,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus DuplicateAccount(13004, " Duplicate account", HttpStatus.BAD_REQUEST), InvalidIban(13005, " Invalid iban number", HttpStatus.BAD_REQUEST), InvalidCard(13006, " Invalid card number", HttpStatus.BAD_REQUEST), - VerificationFailed(13007, "Verification Failed", HttpStatus.BAD_REQUEST), + ShahkarVerificationFailed(13007, "Shahkar Verification Failed", HttpStatus.BAD_REQUEST), ProfileApprovalRequestAlreadyExists(13008, "Request Already Exists", HttpStatus.BAD_REQUEST), ProfileNotfound(13009, "Profile not found", HttpStatus.NOT_FOUND), EmailAlreadyExists(13010, "Email already exists", HttpStatus.BAD_REQUEST), @@ -152,6 +152,8 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus InvalidMobile(13013, "Invalid mobile", HttpStatus.BAD_REQUEST), MobileAlreadySet(13014, "Mobile already set", HttpStatus.BAD_REQUEST), EmailAlreadySet(13015, "Email already set", HttpStatus.BAD_REQUEST), + ProfileAlreadyCompleted(13016, "Profile already completed", HttpStatus.BAD_REQUEST), + ComparativeVerificationFailed(13017, "Comparative Verification Failed", HttpStatus.BAD_REQUEST), ; override fun code() = this.code diff --git a/profile/pom.xml b/profile/pom.xml index be123605a..1beae7882 100644 --- a/profile/pom.xml +++ b/profile/pom.xml @@ -25,7 +25,7 @@ profile-ports/profile-eventlistener-kafka profile-ports/profile-submitter-kafka profile-ports/profile-kyc-proxy - profile-ports/profile-shahkar-proxy + profile-ports/profile-inquiry-proxy profile-ports/profile-otp-proxy profile-ports/profile-auth-proxy @@ -79,7 +79,7 @@ co.nilin.opex.profile.ports - profile-shahkar-proxy + profile-inquiry-proxy ${project.version} diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt index c1f71c861..03bb0ebcb 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/WebClientConfig.kt @@ -16,23 +16,35 @@ class WebClientConfig { @Qualifier("loadBalanced") fun loadBalancedWebClient(loadBalancerFactory: ReactiveLoadBalancer.Factory): WebClient { return WebClient.builder() - .filter(ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, emptyList())) - .exchangeStrategies( - ExchangeStrategies.builder() - .codecs { it.defaultCodecs().maxInMemorySize(20 * 1024 * 1024) } - .build() - ) - .build() + .filter(ReactorLoadBalancerExchangeFilterFunction(loadBalancerFactory, emptyList())) + .exchangeStrategies( + ExchangeStrategies.builder() + .codecs { it.defaultCodecs().maxInMemorySize(20 * 1024 * 1024) } + .build() + ) + .build() } @Bean fun webClient(loadBalancerFactory: ReactiveLoadBalancer.Factory): WebClient { return WebClient.builder() - .filter( - ReactorLoadBalancerExchangeFilterFunction( - loadBalancerFactory, LoadBalancerProperties(), emptyList() - ) + .filter( + ReactorLoadBalancerExchangeFilterFunction( + loadBalancerFactory, LoadBalancerProperties(), emptyList() ) - .build() + ) + .build() + } + + @Bean + @Qualifier("plainWebClient") + fun plainWebClient(): WebClient { + return WebClient.builder() + .exchangeStrategies( + ExchangeStrategies.builder() + .codecs { it.defaultCodecs().maxInMemorySize(20 * 1024 * 1024) } + .build() + ) + .build() } } diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt index c00bc4777..bfccdecb6 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt @@ -34,7 +34,7 @@ class ProfileController(val profileManagement: ProfileManagement) { return profileManagement.update(securityContext.authentication.name, newProfile)?.awaitFirstOrNull() } - @PostMapping("/Completion") + @PostMapping("/completion") suspend fun completeProfile( @RequestBody completeProfileRequest: CompleteProfileRequest, @CurrentSecurityContext securityContext: SecurityContext diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt index 33c5427a6..4571d751c 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt @@ -10,8 +10,10 @@ import co.nilin.opex.profile.core.data.profile.* import co.nilin.opex.profile.core.spi.* import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.reactive.awaitFirst import kotlinx.coroutines.reactive.awaitFirstOrNull import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import reactor.core.publisher.Mono import java.time.LocalDateTime @@ -22,10 +24,17 @@ class ProfileManagement( private val linkedAccountPersister: LinkedAccountPersister, private val limitationPersister: LimitationPersister, private val profileApprovalRequestPersister: ProfileApprovalRequestPersister, - private val shahkarInquiry: ShahkarInquiry, + private val shahkarInquiry: InquiryProxy, private val kycLevelUpdatedPublisher: KycLevelUpdatedPublisher, private val otpProxy: OtpProxy, private val authProxy: AuthProxy, + private val inquiryProxy: InquiryProxy, + + @Value("\${inquiry.mobile-indentiy}") + private var mobileIdentityEnabled: Boolean, + + @Value("\${inquiry.personal-indentiy}") + private var personalIdentityEnabled: Boolean, ) { private val logger = LoggerFactory.getLogger(ProfileManagement::class.java) suspend fun registerNewUser(event: UserCreatedEvent) { @@ -147,40 +156,83 @@ class ProfileManagement( ) ) if (verifyResponse.result) { - authProxy.updateEmail(userId, email) + authProxy.updateEmail(userId, email) //TODO rollback in error ? profilePersister.updateEmail(userId, email) } else throw OpexError.InvalidOTP.exception() } suspend fun completeProfile( userId: String, - completeProfileRequest: CompleteProfileRequest + request: CompleteProfileRequest ): CompleteProfileResponse { + val profile = profilePersister.getProfile(userId)?.awaitFirstOrNull() - ?: throw OpexError.NotFound.exception("profile not found") + ?: throw OpexError.ProfileNotfound.exception() + if (profile.kycLevel == KycLevel.Level2) { - throw OpexError.BadRequest.exception("Profile already completed") + throw OpexError.ProfileAlreadyCompleted.exception() } - val isIranian = completeProfileRequest.nationality == "Iranian" + + val isIranian = request.nationality == "IR" + var useMobileIdentity = false + var usePersonalIdentity = false + if (isIranian) { - if (!shahkarInquiry.getInquiryResult( - completeProfileRequest.identifier, - profile.mobile ?: throw OpexError.BadRequest.exception("Profile mobile is empty") + if (mobileIdentityEnabled) { + verifyMobileIdentity(request.identifier, profile.mobile!!) + useMobileIdentity = true + } + + if (personalIdentityEnabled) { + verifyPersonalIdentity( + request.identifier, + request.firstName, + request.lastName, + request.birthDate ) - ) { - throw OpexError.VerificationFailed.exception("Mobile and identifier do not match") + usePersonalIdentity = true } - completeProfileRequest.verificationStatus = true } - val completedProfile = profilePersister.completeProfile(userId, completeProfileRequest).awaitFirstOrNull() - ?: throw OpexError.BadRequest.exception("profile not found for userId: $userId") - if (isIranian) - kycLevelUpdatedPublisher.publish(KycLevelUpdatedEvent(userId, KycLevel.Level2, LocalDateTime.now())) - else + + val completedProfile = profilePersister + .completeProfile(userId, request, useMobileIdentity, usePersonalIdentity) + .awaitFirst() + + if (isIranian) { + kycLevelUpdatedPublisher.publish( + KycLevelUpdatedEvent(userId, KycLevel.Level2, LocalDateTime.now()) + ) + } else { saveProfileApprovalRequest(completedProfile.id) + } + return completedProfile } + + private suspend fun verifyMobileIdentity(identifier: String, mobile: String) { + if (!inquiryProxy.getShahkarInquiryResult( + identifier, + mobile + ) + ) throw OpexError.ShahkarVerificationFailed.exception() + } + + private suspend fun verifyPersonalIdentity( + identifier: String, + firstName: String, + lastName: String, + birthDate: LocalDateTime + ) { + if (!inquiryProxy.getComparativeInquiryResult( + identifier, + birthDate, + firstName, + lastName + ) + ) throw OpexError.ComparativeVerificationFailed.exception() + } + private suspend fun saveProfileApprovalRequest(profileId: Long) { profileApprovalRequestPersister.save( ProfileApprovalRequest( diff --git a/profile/profile-app/src/main/resources/application.yml b/profile/profile-app/src/main/resources/application.yml index fb094b331..d807b36bf 100644 --- a/profile/profile-app/src/main/resources/application.yml +++ b/profile/profile-app/src/main/resources/application.yml @@ -69,5 +69,8 @@ management: prometheus: enabled: true inquiry: - shahkar: true - comparative: true \ No newline at end of file + mobile-indentiy: true + personal-indentiy: true + url: ${JIBIT_URL} + api-key: ${JIBIT_API_KEY} + secret-key: ${JIBIT_SECRET_KEY} diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt index 12dd887c7..c1c8ec304 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileRequest.kt @@ -1,18 +1,15 @@ package co.nilin.opex.profile.core.data.profile -import co.nilin.opex.profile.core.data.kyc.KycLevel import java.time.LocalDateTime data class CompleteProfileRequest( var firstName: String, var lastName: String, - var address: String ? = null, + var address: String? = null, var telephone: String? = null, var postalCode: String? = null, var nationality: String, var identifier: String, var gender: Gender, var birthDate: LocalDateTime, - var kycLevel: KycLevel? = KycLevel.Level1, - var verificationStatus: Boolean? = false ) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt index 49d8d44a6..fe86ecfd2 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/data/profile/CompleteProfileResponse.kt @@ -19,5 +19,6 @@ open class CompleteProfileResponse( var birthDate: LocalDateTime? = null, var status: UserStatus? = null, var kycLevel: KycLevel? = null, - var verificationStatus: Boolean? = false + var mobileIdentityMatch : Boolean? = false, + var personalIdentityMatch : Boolean? = false ) diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/InquiryProxy.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/InquiryProxy.kt new file mode 100644 index 000000000..7e383359d --- /dev/null +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/InquiryProxy.kt @@ -0,0 +1,14 @@ +package co.nilin.opex.profile.core.spi + +import java.time.LocalDateTime + +interface InquiryProxy { + suspend fun getShahkarInquiryResult(identifier: String, mobile: String): Boolean + + suspend fun getComparativeInquiryResult( + identifier: String, + birthDate: LocalDateTime, + firstName: String, + lastName: String + ): Boolean +} \ No newline at end of file diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt index 449803857..0c4712da4 100644 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt +++ b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ProfilePersister.kt @@ -8,7 +8,13 @@ import reactor.core.publisher.Mono interface ProfilePersister { suspend fun updateProfile(id: String, data: UpdateProfileRequest): Mono - suspend fun completeProfile(id: String, data: CompleteProfileRequest): Mono + suspend fun completeProfile( + id: String, + data: CompleteProfileRequest, + mobileIdentityMatch: Boolean, + personalIdentityMatch: Boolean + ): Mono + suspend fun updateProfileAsAdmin(id: String, data: Profile): Mono suspend fun createProfile(data: Profile): Mono suspend fun getProfile(userId: String): Mono? diff --git a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt b/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt deleted file mode 100644 index 9032799d5..000000000 --- a/profile/profile-core/src/main/kotlin/co/nilin/opex/profile/core/spi/ShahkarInquiry.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.nilin.opex.profile.core.spi - -interface ShahkarInquiry { - suspend fun getInquiryResult(identifier: String, mobile: String): Boolean -} \ No newline at end of file diff --git a/profile/profile-ports/profile-shahkar-proxy/.gitignore b/profile/profile-ports/profile-inquiry-proxy/.gitignore similarity index 100% rename from profile/profile-ports/profile-shahkar-proxy/.gitignore rename to profile/profile-ports/profile-inquiry-proxy/.gitignore diff --git a/profile/profile-ports/profile-shahkar-proxy/pom.xml b/profile/profile-ports/profile-inquiry-proxy/pom.xml similarity index 87% rename from profile/profile-ports/profile-shahkar-proxy/pom.xml rename to profile/profile-ports/profile-inquiry-proxy/pom.xml index 0f6207882..f4c211b5e 100644 --- a/profile/profile-ports/profile-shahkar-proxy/pom.xml +++ b/profile/profile-ports/profile-inquiry-proxy/pom.xml @@ -9,7 +9,7 @@ ../../pom.xml co.nilin.opex.profile.ports - profile-shahkar-proxy + profile-inquiry-proxy profile-shahkar-proxy profile-shahkar-proxy @@ -37,6 +37,11 @@ co.nilin.opex.profile profile-core + + com.ibm.icu + icu4j + 74.1 + diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ComparativeResponse.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ComparativeResponse.kt new file mode 100644 index 000000000..83d59c061 --- /dev/null +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ComparativeResponse.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.ports.inquiry.data + +data class ComparativeResponse( + val firstNameSimilarityPercentage: Int, + val lastNameSimilarityPercentage: Int, +) \ No newline at end of file diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ShahkarResponse.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ShahkarResponse.kt new file mode 100644 index 000000000..058a6a3cb --- /dev/null +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/ShahkarResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.profile.ports.inquiry.data + +data class ShahkarResponse(val matched: Boolean) diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/TokenResponse.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/TokenResponse.kt new file mode 100644 index 000000000..6b4604d3e --- /dev/null +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/data/TokenResponse.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.profile.ports.inquiry.data + +data class TokenResponse( + val accessToken: String +) diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt new file mode 100644 index 000000000..aeced0800 --- /dev/null +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt @@ -0,0 +1,64 @@ +package co.nilin.opex.profile.ports.inquiry.imp + +import co.nilin.opex.profile.core.spi.InquiryProxy +import co.nilin.opex.profile.ports.inquiry.data.ComparativeResponse +import co.nilin.opex.profile.ports.inquiry.data.ShahkarResponse +import co.nilin.opex.profile.ports.inquiry.utils.TokenProvider +import co.nilin.opex.profile.ports.inquiry.utils.toPersianDateFormatted +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBody +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Component +class InquiryProxyImp( + @Qualifier("plainWebClient") private val webClient: WebClient, + private val tokenProvider: TokenProvider +) : InquiryProxy { + + @Value("\${inquiry.url}") + private lateinit var baseUrl: String + + override suspend fun getShahkarInquiryResult(identifier: String, mobile: String): Boolean { + return webClient.get() + .uri("$baseUrl/v1/services/matching") { + it.queryParam("nationalCode", identifier) + it.queryParam("mobileNumber", mobile) + it.build() + } + .header(HttpHeaders.AUTHORIZATION, "Bearer ${tokenProvider.getToken()}") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() + .matched + } + + override suspend fun getComparativeInquiryResult( + identifier: String, + birthDate: LocalDateTime, + firstName: String, + lastName: String + ): Boolean { + val birthDateFormatted = birthDate.toPersianDateFormatted() + + val response = webClient.get() + .uri("$baseUrl/v1/services/identity/similarity") { + it.queryParam("nationalCode", identifier) + it.queryParam("birthDate", birthDateFormatted) + it.queryParam("firstName", firstName) + it.queryParam("lastName", lastName) + it.build() + } + .header(HttpHeaders.AUTHORIZATION, "Bearer ${tokenProvider.getToken()}") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() + + return (response.firstNameSimilarityPercentage >= 95 && response.lastNameSimilarityPercentage >= 95) + } +} \ No newline at end of file diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/DateConvertor.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/DateConvertor.kt new file mode 100644 index 000000000..26d4f8211 --- /dev/null +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/DateConvertor.kt @@ -0,0 +1,20 @@ +package co.nilin.opex.profile.ports.inquiry.utils + +import com.ibm.icu.util.Calendar +import com.ibm.icu.util.ULocale +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* + +fun LocalDateTime.toPersianDateFormatted(): String { + val instant = this.atZone(ZoneId.systemDefault()).toInstant() + val date = Date.from(instant) + val calendar = Calendar.getInstance(ULocale("fa_IR@calendar=persian")) + calendar.time = date + + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + 1 + val day = calendar.get(Calendar.DAY_OF_MONTH) + + return String.format("%04d%02d%02d", year, month, day) +} \ No newline at end of file diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/TokenProvider.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/TokenProvider.kt new file mode 100644 index 000000000..b52abd37c --- /dev/null +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/utils/TokenProvider.kt @@ -0,0 +1,55 @@ +package co.nilin.opex.profile.ports.inquiry.utils + +import co.nilin.opex.profile.ports.inquiry.data.TokenResponse +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.reactive.function.client.WebClient +import java.time.Duration +import java.time.Instant + +@Service +class TokenProvider( + @Qualifier("plainWebClient") private val webClient: WebClient, + @Value("\${inquiry.url}") private var baseUrl: String, + @Value("\${inquiry.api-key}") private var apiKey: String, + @Value("\${inquiry.secret-key}") private var secretKey: String, +) { + + @Volatile + private var cachedToken: String? = null + + @Volatile + private var expiryTime: Instant? = null + + private val tokenLock = Mutex() + + suspend fun getToken(): String { + + val now = Instant.now() + if (cachedToken != null && expiryTime != null && now.isBefore(expiryTime)) { + return cachedToken!! + } + tokenLock.withLock { + if (cachedToken == null || expiryTime == null || now.isAfter(expiryTime)) { + val response = + webClient.post() + .uri("$baseUrl/v1/tokens/generate") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(mapOf("apiKey" to apiKey, "secretKey" to secretKey)) + .retrieve() + .bodyToMono(TokenResponse::class.java) + .awaitSingle() + + cachedToken = response.accessToken + expiryTime = now.plus(Duration.ofHours(23)) + } + + return cachedToken!! + } + } +} \ No newline at end of file diff --git a/profile/profile-ports/profile-shahkar-proxy/src/main/resources/application.properties b/profile/profile-ports/profile-inquiry-proxy/src/main/resources/application.properties similarity index 100% rename from profile/profile-ports/profile-shahkar-proxy/src/main/resources/application.properties rename to profile/profile-ports/profile-inquiry-proxy/src/main/resources/application.properties diff --git a/profile/profile-ports/profile-postgres/pom.xml b/profile/profile-ports/profile-postgres/pom.xml index bc2e91dc9..7e8464535 100644 --- a/profile/profile-ports/profile-postgres/pom.xml +++ b/profile/profile-ports/profile-postgres/pom.xml @@ -57,7 +57,7 @@ co.nilin.opex.profile.ports - profile-shahkar-proxy + profile-inquiry-proxy diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt index aa1478210..38f888616 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/convertor/ProfileConvertor.kt @@ -20,6 +20,7 @@ fun convertProfileModelToCompleteProfileResponse(profileModel: ProfileModel): Co birthDate = profileModel.birthDate, status = profileModel.status, kycLevel = profileModel.kycLevel, - verificationStatus = profileModel.verificationStatus + mobileIdentityMatch = profileModel.mobileIdentityMatch, + personalIdentityMatch = profileModel.personalIdentityMatch, ) } diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt index 7e944c8d3..8c9b0ed4f 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt @@ -75,27 +75,37 @@ class ProfileManagementImp( } ?: throw OpexError.UserNotFound.exception() } - override suspend fun completeProfile(id: String, data: CompleteProfileRequest): Mono { - return profileRepository.findByUserId(id)?.awaitFirstOrNull()?.let { it -> + override suspend fun completeProfile( + id: String, + data: CompleteProfileRequest, + mobileIdentityMatch: Boolean, + personalIdentityMatch: Boolean + ): Mono { + val existingProfile = profileRepository.findByUserId(id)?.awaitFirstOrNull() + ?: throw OpexError.ProfileNotfound.exception() - var newProfileModel = data.convert(ProfileModel::class.java) - newProfileModel.email = it.email - newProfileModel.mobile = it.mobile - newProfileModel.id = it.id - newProfileModel.userId = it.userId - newProfileModel.status = it.status - newProfileModel.createDate = it.createDate - newProfileModel.lastUpdateDate = LocalDateTime.now() - profileRepository.save(newProfileModel) - .map { savedProfile -> - convertProfileModelToCompleteProfileResponse(savedProfile).apply { - if (it.nationality == "Iranian") - kycLevel = KycLevel.Level2 - } + var newProfileModel = data.convert(ProfileModel::class.java) + newProfileModel.email = existingProfile.email + newProfileModel.mobile = existingProfile.mobile + newProfileModel.id = existingProfile.id + newProfileModel.userId = existingProfile.userId + newProfileModel.status = existingProfile.status + newProfileModel.createDate = existingProfile.createDate + newProfileModel.lastUpdateDate = LocalDateTime.now() + newProfileModel.mobileIdentityMatch = mobileIdentityMatch + newProfileModel.personalIdentityMatch = personalIdentityMatch + + return profileRepository.save(newProfileModel) + .map { saved -> + val response = convertProfileModelToCompleteProfileResponse(saved) + if (saved.nationality == "IR") { + response.kycLevel = KycLevel.Level2 } - } ?: throw OpexError.UserNotFound.exception() + response + } } + //todo //update shared fields in keycloak override suspend fun updateProfileAsAdmin(id: String, data: Profile): Mono { diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt index d0c01bcb2..641e3aa92 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/model/base/Profile.kt @@ -23,6 +23,7 @@ open class Profile { var lastUpdateDate: LocalDateTime? = null var creator: String? = null var kycLevel: KycLevel? = KycLevel.Level1 - var verificationStatus : Boolean? = false + var mobileIdentityMatch : Boolean? = false + var personalIdentityMatch : Boolean? = false } \ No newline at end of file diff --git a/profile/profile-ports/profile-postgres/src/main/resources/schema.sql b/profile/profile-ports/profile-postgres/src/main/resources/schema.sql index a4eb00a93..7ee77ad16 100644 --- a/profile/profile-ports/profile-postgres/src/main/resources/schema.sql +++ b/profile/profile-ports/profile-postgres/src/main/resources/schema.sql @@ -303,23 +303,63 @@ BEGIN AND column_name = 'email') THEN ALTER TABLE profile ALTER COLUMN email DROP NOT NULL; END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'unique_identifier' + ) THEN + ALTER TABLE profile + ADD CONSTRAINT unique_identifier UNIQUE (identifier); + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'unique_mobile' + ) THEN + ALTER TABLE profile + ADD CONSTRAINT unique_mobile UNIQUE (mobile); + END IF; IF EXISTS (SELECT 1 FROM information_schema.columns - WHERE table_name = 'profile' - AND column_name = 'identifier') THEN ALTER TABLE profile - ADD CONSTRAINT unique_identifier UNIQUE (identifier); + WHERE table_name = 'profile_history' + AND column_name = 'email') THEN ALTER TABLE profile_history + ALTER COLUMN email DROP NOT NULL; END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'profile' - AND column_name = 'mobile') THEN ALTER TABLE profile - ADD CONSTRAINT unique_mobile UNIQUE (mobile); + AND column_name = 'verification_status') THEN ALTER TABLE profile + DROP COLUMN verification_status; END IF; IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'profile_history' - AND column_name = 'email') THEN ALTER TABLE profile_history - ALTER COLUMN email DROP NOT NULL; + AND column_name = 'verification_status') THEN ALTER TABLE profile_history + DROP COLUMN verification_status; + END IF; + + IF NOT EXISTS (SELECT 1 + FROM information_schema.columns + WHERE table_name = 'profile' AND column_name = 'mobile_identity_match') THEN ALTER TABLE profile + ADD COLUMN mobile_identity_match BOOLEAN; + END IF; + IF NOT EXISTS (SELECT 1 + FROM information_schema.columns + WHERE table_name = 'profile_history' + AND column_name = 'mobile_identity_match') THEN ALTER TABLE profile_history + ADD COLUMN mobile_identity_match BOOLEAN; + END IF; + IF NOT EXISTS (SELECT 1 + FROM information_schema.columns + WHERE table_name = 'profile' AND column_name = 'personal_identity_match') THEN ALTER TABLE profile + ADD COLUMN personal_identity_match BOOLEAN; + END IF; + IF NOT EXISTS (SELECT 1 + FROM information_schema.columns + WHERE table_name = 'profile_history' + AND column_name = 'personal_identity_match') THEN ALTER TABLE profile_history + ADD COLUMN personal_identity_match BOOLEAN; END IF; END $$; \ No newline at end of file diff --git a/profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt b/profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt deleted file mode 100644 index 41b89a0f8..000000000 --- a/profile/profile-ports/profile-shahkar-proxy/src/main/kotlin/co/nilin/opex/profile/ports/shahkar/imp/ShahkarProxyImp.kt +++ /dev/null @@ -1,12 +0,0 @@ -package co.nilin.opex.profile.ports.shahkar.imp - -import co.nilin.opex.profile.core.spi.ShahkarInquiry -import org.springframework.stereotype.Component - -@Component -class ShahkarProxyImp : ShahkarInquiry { - override suspend fun getInquiryResult(identifier: String, mobile: String): Boolean { - //TODO implement - return true - } -} \ No newline at end of file From 603fac832648f256b44431f2663005cc356e87ac Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 23 Jul 2025 18:16:38 +0330 Subject: [PATCH 4/9] Update docker-compose.yml --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 10bd08219..370ab8075 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -524,13 +524,16 @@ services: profile: image: ghcr.io/opexdev/profile environment: - - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + - JAVA_OPTS=-Xmx256m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - CONSUL_HOST=consul - DB_IP_PORT=postgres-profile - KAFKA_IP_PORT=kafka-1:29092,kafka-2:29092,kafka-3:29092 - BACKEND_USER=${BACKEND_USER} - VAULT_HOST=vault - SWAGGER_AUTH_URL=$KEYCLOAK_FRONTEND_URL + - JIBIT_URL=${JIBIT_URL} + - JIBIT_API_KEY=${JIBIT_API_KEY} + - JIBIT_SECRET_KEY=${JIBIT_SECRET_KEY} depends_on: - kafka-1 - kafka-2 From e150ef5e6cccf15b7bdebde457269ac778fe9732 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 26 Jul 2025 16:35:02 +0330 Subject: [PATCH 5/9] Some improvement --- .../kotlin/co/nilin/opex/common/OpexError.kt | 4 + .../opex/profile/app/config/SecurityConfig.kt | 3 +- .../app/controller/ProfileAdminController.kt | 2 +- .../app/controller/ProfileController.kt | 15 +- .../ProfileApprovalRequestManagement.kt | 8 +- .../profile/app/service/ProfileManagement.kt | 1 + .../ports/inquiry/imp/InquiryProxyImp.kt | 4 +- .../postgres/imp/ProfileManagementImp.kt | 14 +- .../src/main/resources/schema.sql | 135 +++--------------- 9 files changed, 45 insertions(+), 141 deletions(-) diff --git a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt index 3e10d49aa..51f5abf56 100644 --- a/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt +++ b/common/src/main/kotlin/co/nilin/opex/common/OpexError.kt @@ -154,6 +154,10 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus EmailAlreadySet(13015, "Email already set", HttpStatus.BAD_REQUEST), ProfileAlreadyCompleted(13016, "Profile already completed", HttpStatus.BAD_REQUEST), ComparativeVerificationFailed(13017, "Comparative Verification Failed", HttpStatus.BAD_REQUEST), + ProfileApprovalRequestNotfound(13018, "Profile approval request not found", HttpStatus.NOT_FOUND), + InvalidProfileApprovalRequestStatus(13018, "Invalid profile approval request status", HttpStatus.BAD_REQUEST), + ShahkarInquiryUnavailable(13019,"Shahkar inquiry unavailable" , HttpStatus.SERVICE_UNAVAILABLE), + ComparativeInquiryUnavailable(13020,"Comparative inquiry unavailable" , HttpStatus.SERVICE_UNAVAILABLE), ; override fun code() = this.code diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt index bb86727e4..57b978f42 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/config/SecurityConfig.kt @@ -20,8 +20,7 @@ class SecurityConfig(private val webClient: WebClient) { fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? { http.csrf().disable() .authorizeExchange() - // .pathMatchers("/**").permitAll() - .pathMatchers("**/admin/**").hasRole("SCOPE_trust", "admin_system") + .pathMatchers("/admin/**").hasAuthority("ROLE_admin") .pathMatchers("/actuator/**").permitAll() .anyExchange().authenticated() .and() diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt index 0623942be..c9107dff8 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileAdminController.kt @@ -18,7 +18,7 @@ import org.springframework.security.core.context.SecurityContext import org.springframework.web.bind.annotation.* @RestController -@RequestMapping("/v2/admin/profile") +@RequestMapping("/admin/profile") class ProfileAdminController( val profileManagement: ProfileManagement, diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt index bfccdecb6..d77dbd08e 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt @@ -25,14 +25,13 @@ class ProfileController(val profileManagement: ProfileManagement) { return profileManagement.getProfile(securityContext.authentication.name)?.awaitFirstOrNull() } - - @PutMapping("") - suspend fun update( - @RequestBody newProfile: UpdateProfileRequest, - @CurrentSecurityContext securityContext: SecurityContext - ): Profile? { - return profileManagement.update(securityContext.authentication.name, newProfile)?.awaitFirstOrNull() - } +// @PutMapping("") +// suspend fun update( +// @RequestBody newProfile: UpdateProfileRequest, +// @CurrentSecurityContext securityContext: SecurityContext +// ): Profile? { +// return profileManagement.update(securityContext.authentication.name, newProfile)?.awaitFirstOrNull() +// } @PostMapping("/completion") suspend fun completeProfile( diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt index 11a8e7e0d..f4f27bf19 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileApprovalRequestManagement.kt @@ -26,13 +26,13 @@ class ProfileApprovalRequestManagement( suspend fun getApprovalRequest(id: Long): ProfileApprovalResponse { return profileApprovalRequestPersister.getRequestById(id).awaitFirstOrNull() - ?: throw OpexError.NotFound.exception("Request not found") + ?: throw OpexError.ProfileApprovalRequestNotfound.exception() } suspend fun approveRequest(id: Long, updater: String, description: String): ProfileApprovalResponse { val request = changeRequestStatus(id, updater, ProfileApprovalRequestStatus.APPROVED, description) val profile = profilePersister.getProfile(request.profileId)?.awaitFirstOrNull() - ?: throw OpexError.NotFound.exception("Request not found") + ?: throw OpexError.ProfileNotfound.exception() kycLevelUpdatedPublisher.publish(KycLevelUpdatedEvent(profile.userId!!, KycLevel.Level2, LocalDateTime.now())) return request } @@ -48,9 +48,9 @@ class ProfileApprovalRequestManagement( description: String ): ProfileApprovalResponse { var request = (profileApprovalRequestPersister.getRequestById(id).awaitFirstOrNull() - ?: throw OpexError.NotFound.exception("Request not found")) + ?: throw OpexError.ProfileApprovalRequestNotfound.exception()) if (request.status != ProfileApprovalRequestStatus.PENDING) - throw OpexError.BadRequest.exception("Only pending requests can be changed") + throw OpexError.InvalidProfileApprovalRequestStatus.exception() request.apply { status = newStatus updateDate = LocalDateTime.now() diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt index 4571d751c..ad8f925ea 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/service/ProfileManagement.kt @@ -197,6 +197,7 @@ class ProfileManagement( val completedProfile = profilePersister .completeProfile(userId, request, useMobileIdentity, usePersonalIdentity) .awaitFirst() + authProxy.updateName(userId, request.firstName, request.lastName) if (isIranian) { kycLevelUpdatedPublisher.publish( diff --git a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt index aeced0800..0a19aa352 100644 --- a/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt +++ b/profile/profile-ports/profile-inquiry-proxy/src/main/kotlin/co/nilin/opex/profile/ports/inquiry/imp/InquiryProxyImp.kt @@ -1,5 +1,6 @@ package co.nilin.opex.profile.ports.inquiry.imp +import co.nilin.opex.common.OpexError import co.nilin.opex.profile.core.spi.InquiryProxy import co.nilin.opex.profile.ports.inquiry.data.ComparativeResponse import co.nilin.opex.profile.ports.inquiry.data.ShahkarResponse @@ -13,7 +14,6 @@ import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.awaitBody import java.time.LocalDateTime -import java.time.format.DateTimeFormatter @Component class InquiryProxyImp( @@ -34,6 +34,7 @@ class InquiryProxyImp( .header(HttpHeaders.AUTHORIZATION, "Bearer ${tokenProvider.getToken()}") .accept(MediaType.APPLICATION_JSON) .retrieve() + .onStatus({ t -> t.isError }, { throw OpexError.ShahkarInquiryUnavailable.exception() }) .awaitBody() .matched } @@ -57,6 +58,7 @@ class InquiryProxyImp( .header(HttpHeaders.AUTHORIZATION, "Bearer ${tokenProvider.getToken()}") .accept(MediaType.APPLICATION_JSON) .retrieve() + .onStatus({ t -> t.isError }, { throw OpexError.ComparativeInquiryUnavailable.exception() }) .awaitBody() return (response.firstNameSimilarityPercentage >= 95 && response.lastNameSimilarityPercentage >= 95) diff --git a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt index 8c9b0ed4f..5f10726b7 100644 --- a/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt +++ b/profile/profile-ports/profile-postgres/src/main/kotlin/co/nilin/opex/profile/ports/postgres/imp/ProfileManagementImp.kt @@ -91,6 +91,8 @@ class ProfileManagementImp( newProfileModel.userId = existingProfile.userId newProfileModel.status = existingProfile.status newProfileModel.createDate = existingProfile.createDate + newProfileModel.creator = existingProfile.creator + newProfileModel.kycLevel = existingProfile.kycLevel newProfileModel.lastUpdateDate = LocalDateTime.now() newProfileModel.mobileIdentityMatch = mobileIdentityMatch newProfileModel.personalIdentityMatch = personalIdentityMatch @@ -148,7 +150,7 @@ class ProfileManagementImp( override suspend fun getProfile(id: Long): Mono { val profile: Profile = profileRepository.findById(id).awaitFirstOrNull()?.convert(Profile::class.java) - ?: throw OpexError.UserNotFound.exception() + ?: throw OpexError.ProfileNotfound.exception() return Mono.just(profile) } @@ -212,9 +214,6 @@ class ProfileManagementImp( if (!profile.email.isNullOrEmpty()) throw OpexError.EmailAlreadySet.exception() - - if (profileRepository.findByEmail(email)?.awaitFirstOrNull() != null) - throw OpexError.EmailAlreadyExists.exception() } override suspend fun validateMobileForUpdate(userId: String, mobile: String) { @@ -225,12 +224,11 @@ class ProfileManagementImp( if (!profile.mobile.isNullOrEmpty()) throw OpexError.MobileAlreadySet.exception() - - if (profileRepository.findByMobile(mobile)?.awaitFirstOrNull() != null) - throw OpexError.MobileAlreadyExists.exception() } override suspend fun updateMobile(userId: String, mobile: String) { + if (profileRepository.findByMobile(mobile)?.awaitFirstOrNull() != null) + throw OpexError.MobileAlreadyExists.exception() val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() ?: throw OpexError.ProfileNotfound.exception() profile.mobile = mobile @@ -238,6 +236,8 @@ class ProfileManagementImp( } override suspend fun updateEmail(userId: String, email: String) { + if (profileRepository.findByEmail(email)?.awaitFirstOrNull() != null) + throw OpexError.EmailAlreadyExists.exception() val profile = profileRepository.findByUserId(userId)?.awaitFirstOrNull() ?: throw OpexError.ProfileNotfound.exception() profile.email = email diff --git a/profile/profile-ports/profile-postgres/src/main/resources/schema.sql b/profile/profile-ports/profile-postgres/src/main/resources/schema.sql index 7ee77ad16..dd44fa5c6 100644 --- a/profile/profile-ports/profile-postgres/src/main/resources/schema.sql +++ b/profile/profile-ports/profile-postgres/src/main/resources/schema.sql @@ -1,15 +1,15 @@ CREATE TABLE IF NOT EXISTS profile ( id SERIAL PRIMARY KEY, - email VARCHAR(100) NOT NULL UNIQUE, + email VARCHAR(100) UNIQUE, last_name VARCHAR(256), user_id VARCHAR(100) NOT NULL UNIQUE, create_date TIMESTAMP, - identifier VARCHAR(100), + identifier VARCHAR(100) UNIQUE, address VARCHAR(256), first_name VARCHAR(256), telephone VARCHAR(256), - mobile VARCHAR(256), + mobile VARCHAR(256) UNIQUE, nationality VARCHAR(256), gender VARCHAR(50), birth_date TIMESTAMP, @@ -17,7 +17,9 @@ CREATE TABLE IF NOT EXISTS profile postal_code VARCHAR(100), creator VARCHAR(100), last_update_date TIMESTAMP DEFAULT CURRENT_DATE, - kyc_level varchar(100) + kyc_level varchar(100), + personal_identity_match BOOLEAN, + mobile_identity_match BOOLEAN ); CREATE TABLE IF NOT EXISTS profile_history @@ -33,7 +35,7 @@ CREATE TABLE IF NOT EXISTS profile_history telephone VARCHAR(256), mobile VARCHAR(256), nationality VARCHAR(256), - gender BOOLEAN, + gender VARCHAR(50), birth_date TIMESTAMP, status VARCHAR(100), last_update_date TIMESTAMP, @@ -43,7 +45,9 @@ CREATE TABLE IF NOT EXISTS profile_history postal_code VARCHAR(100), change_request_date TIMESTAMP, change_request_type VARCHAR(100), - kyc_level varchar(100) + kyc_level varchar(100), + personal_identity_match BOOLEAN, + mobile_identity_match BOOLEAN ); @@ -133,11 +137,11 @@ $BODY$ BEGIN INSERT INTO public.profile_history (original_data_id, change_request_date, change_request_type, email, user_id, create_date, identifier, address, first_name, last_name, mobile, telephone, - nationality, gender, birth_date, status, postal_code, creator, kyc_level) + nationality, gender, birth_date, status, postal_code, creator, kyc_level,personal_identity_match,mobile_identity_match) VALUES (OLD.id, now(), 'UPDATE', OLD.email, OLD.user_id, OLD.create_date, OLD.identifier, OLD.address, OLD.first_name, OLD.last_name, OLD.mobile, OLD.telephone, OLD.nationality, OLD.gender, OLD.birth_date, OLD.status, - OLD.postal_code, OLD.creator, OLD.kyc_level); + OLD.postal_code, OLD.creator, OLD.kyc_level,OLD.personal_identity_match,OLD.mobile_identity_match); RETURN NULL; END; $BODY$ language plpgsql; @@ -149,11 +153,11 @@ $BODY$ BEGIN INSERT INTO public.profile_history (original_data_id, change_request_date, change_request_type, email, user_id, create_date, identifier, address, first_name, last_name, mobile, telephone, - nationality, gender, birth_date, status, postal_code, creator, kyc_level) + nationality, gender, birth_date, status, postal_code, creator, kyc_level,personal_identity_match,mobile_identity_match) VALUES (OLD.id, now(), 'DELETE', OLD.email, OLD.user_id, OLD.create_date, OLD.identifier, OLD.address, OLD.first_name, OLD.last_name, OLD.mobile, OLD.telephone, OLD.nationality, OLD.gender, OLD.birth_date, OLD.status, - OLD.postal_code, OLD.creator, OLD.kyc_level); + OLD.postal_code, OLD.creator, OLD.kyc_level,OLD.personal_identity_match,OLD.mobile_identity_match); RETURN NULL; END; $BODY$ language plpgsql; @@ -255,111 +259,6 @@ CREATE TABLE IF NOT EXISTS profile_approval_request create_date TIMESTAMP, update_date TIMESTAMP, updater VARCHAR(100), - description VARCHAR(255) -); - -DO -$$ - BEGIN - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile' AND column_name = 'verification_status') THEN ALTER TABLE profile - ADD COLUMN verification_status VARCHAR(255); - END IF; - END -$$; - -DO -$$ - BEGIN - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile_history' - AND column_name = 'verification_status') THEN ALTER TABLE profile_history - ADD COLUMN verification_status VARCHAR(255); - END IF; - END -$$; - -DO -$$ - BEGIN - IF EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile_history' - AND column_name = 'gender' - AND data_type != 'character varying') THEN ALTER TABLE profile_history - ALTER COLUMN gender SET DATA TYPE VARCHAR(50); - END IF; - END -$$; - -DO -$$ -BEGIN - IF EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile' - AND column_name = 'email') THEN ALTER TABLE profile - ALTER COLUMN email DROP NOT NULL; - END IF; - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conname = 'unique_identifier' - ) THEN - ALTER TABLE profile - ADD CONSTRAINT unique_identifier UNIQUE (identifier); - END IF; - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conname = 'unique_mobile' - ) THEN - ALTER TABLE profile - ADD CONSTRAINT unique_mobile UNIQUE (mobile); - END IF; - IF EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile_history' - AND column_name = 'email') THEN ALTER TABLE profile_history - ALTER COLUMN email DROP NOT NULL; - END IF; - - IF EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile' - AND column_name = 'verification_status') THEN ALTER TABLE profile - DROP COLUMN verification_status; - END IF; - IF EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile_history' - AND column_name = 'verification_status') THEN ALTER TABLE profile_history - DROP COLUMN verification_status; - END IF; - - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile' AND column_name = 'mobile_identity_match') THEN ALTER TABLE profile - ADD COLUMN mobile_identity_match BOOLEAN; - END IF; - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile_history' - AND column_name = 'mobile_identity_match') THEN ALTER TABLE profile_history - ADD COLUMN mobile_identity_match BOOLEAN; - END IF; - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile' AND column_name = 'personal_identity_match') THEN ALTER TABLE profile - ADD COLUMN personal_identity_match BOOLEAN; - END IF; - IF NOT EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'profile_history' - AND column_name = 'personal_identity_match') THEN ALTER TABLE profile_history - ADD COLUMN personal_identity_match BOOLEAN; - END IF; -END -$$; \ No newline at end of file + description VARCHAR(255), + UNIQUE (profile_id, status) +); \ No newline at end of file From 8ea81f31fa7ef4b6586c0df6151eda71e7149c76 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Sat, 26 Jul 2025 16:50:53 +0330 Subject: [PATCH 6/9] Update auth security config --- .../main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt | 2 ++ .../kotlin/co/nilin/opex/auth/controller/UserController.kt | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt index 1d6780e30..9e16a1dbb 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/config/SecurityConfig.kt @@ -3,6 +3,7 @@ package co.nilin.opex.auth.config import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod import org.springframework.security.config.Customizer import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.ServerHttpSecurity @@ -27,6 +28,7 @@ class SecurityConfig( .pathMatchers("/v1/oauth/protocol/openid-connect/**").permitAll() .pathMatchers("/v1/oauth.***").permitAll() .pathMatchers("/v1/user/public/**").permitAll() + .pathMatchers("/v1/user/update/**").permitAll() .anyExchange().authenticated() } .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) } diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt index 2913c1e11..acd3a15a9 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/controller/UserController.kt @@ -18,14 +18,14 @@ class UserController(private val userService: UserService) { userService.logout(securityContext.jwtAuthentication().name) } - @PutMapping("/email") + @PutMapping("/update/email") suspend fun updateEmail( @RequestBody request: UpdateEmailRequest ) { userService.updateEmail(request) } - @PutMapping("/mobile") + @PutMapping("/update/mobile") suspend fun updateMobile( @RequestBody request: UpdateMobileRequest ) { @@ -33,7 +33,7 @@ class UserController(private val userService: UserService) { } - @PutMapping("/name") + @PutMapping("/update/name") suspend fun updateName( @RequestBody request: UpdateNameRequest ) { From baaf9fe9e1519d5586407bf84893e397d59f4933 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 30 Jul 2025 13:18:44 +0330 Subject: [PATCH 7/9] fix conflicts --- .../src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt | 3 +++ .../src/main/kotlin/co/nilin/opex/auth/service/UserService.kt | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt index 482b29ddd..ca6594961 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/proxy/KeycloakProxy.kt @@ -361,6 +361,7 @@ class KeycloakProxy( header(HttpHeaders.AUTHORIZATION, "Bearer ${token ?: getAdminAccessToken()}") return this } + suspend fun updateUserMobile(userId: String, newMobile: String) { val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/users/$userId" val patch = mapOf( @@ -407,6 +408,8 @@ class KeycloakProxy( .retrieve() .toBodilessEntity() .awaitSingleOrNull() + } + private suspend fun generateRandomInternalID(): String { var internalId: String; var attempts = 0 diff --git a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt index 526224ab8..efcdb9f25 100644 --- a/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt +++ b/auth-gateway/auth-gateway-app/src/main/kotlin/co/nilin/opex/auth/service/UserService.kt @@ -160,6 +160,7 @@ class UserService( suspend fun updateName(request: UpdateNameRequest) { keycloakProxy.updateUserName(request.userId, request.firstName, request.lastName) + } suspend fun fetchActiveSessions(uuid: String, currentSessionId: String): List { return keycloakProxy.fetchActiveSessions(uuid, currentSessionId) @@ -213,6 +214,5 @@ class UserService( return TokenData(false, "", OTPAction.REGISTER) } } - + } - From 0c760c963a3141be141b8ec7797ecd1fc0692b18 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 30 Jul 2025 13:51:15 +0330 Subject: [PATCH 8/9] Update application.yml --- profile/profile-app/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/profile/profile-app/src/main/resources/application.yml b/profile/profile-app/src/main/resources/application.yml index d807b36bf..4b0ea5c6a 100644 --- a/profile/profile-app/src/main/resources/application.yml +++ b/profile/profile-app/src/main/resources/application.yml @@ -52,9 +52,9 @@ app: kyc: url: lb://opex-kyc/v2/admin/kyc/internal otp: - url: http://opex-otp/v1 + url: lb://opex-otp/v1 auth-gateway: - url: http://opex-auth-gateway + url: lb://opex-auth-gateway management: endpoints: web: From 8a6a6f2556e929b4f6ff95923334e3e74ca86382 Mon Sep 17 00:00:00 2001 From: Amir Rajabi Date: Wed, 30 Jul 2025 17:57:16 +0330 Subject: [PATCH 9/9] update profile controller --- .../app/controller/ProfileController.kt | 56 +++++++++---------- .../app/dto/ContactUpdateConfirmRequest.kt | 7 +++ .../profile/app/dto/ContactUpdateRequest.kt | 6 ++ .../profile/app/dto/UpdateEmailRequest.kt | 5 -- .../profile/app/dto/UpdateMobileRequest.kt | 5 -- .../app/dto/VerifyUpdateEmailRequest.kt | 6 -- .../app/dto/VerifyUpdateMobileRequest.kt | 6 -- 7 files changed, 39 insertions(+), 52 deletions(-) create mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateConfirmRequest.kt create mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateRequest.kt delete mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt delete mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt delete mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt delete mode 100644 profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt index d77dbd08e..445b2f38f 100644 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/controller/ProfileController.kt @@ -1,15 +1,13 @@ package co.nilin.opex.profile.app.controller -import co.nilin.opex.profile.app.dto.UpdateEmailRequest -import co.nilin.opex.profile.app.dto.UpdateMobileRequest -import co.nilin.opex.profile.app.dto.VerifyUpdateEmailRequest -import co.nilin.opex.profile.app.dto.VerifyUpdateMobileRequest +import co.nilin.opex.common.OpexError +import co.nilin.opex.profile.app.dto.ContactUpdateConfirmRequest +import co.nilin.opex.profile.app.dto.ContactUpdateRequest import co.nilin.opex.profile.app.service.ProfileManagement import co.nilin.opex.profile.core.data.otp.TempOtpResponse import co.nilin.opex.profile.core.data.profile.CompleteProfileRequest import co.nilin.opex.profile.core.data.profile.CompleteProfileResponse import co.nilin.opex.profile.core.data.profile.Profile -import co.nilin.opex.profile.core.data.profile.UpdateProfileRequest import kotlinx.coroutines.reactive.awaitFirstOrNull import org.springframework.security.core.annotation.CurrentSecurityContext import org.springframework.security.core.context.SecurityContext @@ -33,7 +31,7 @@ class ProfileController(val profileManagement: ProfileManagement) { // return profileManagement.update(securityContext.authentication.name, newProfile)?.awaitFirstOrNull() // } - @PostMapping("/completion") + @PutMapping("/completion") suspend fun completeProfile( @RequestBody completeProfileRequest: CompleteProfileRequest, @CurrentSecurityContext securityContext: SecurityContext @@ -41,35 +39,33 @@ class ProfileController(val profileManagement: ProfileManagement) { return profileManagement.completeProfile(securityContext.authentication.name, completeProfileRequest) } - @PostMapping("/mobile") - suspend fun requestUpdateMobile( - @RequestBody request: UpdateMobileRequest, + @PostMapping("/contact/update/otp-request") + suspend fun requestContactUpdate( + @RequestBody request: ContactUpdateRequest, @CurrentSecurityContext securityContext: SecurityContext ): TempOtpResponse { - return profileManagement.requestUpdateMobile(securityContext.authentication.name, request.mobile) + val username = securityContext.authentication.name + return if (!request.email.isNullOrBlank()) { + profileManagement.requestUpdateEmail(username, request.email) + } else if (!request.mobile.isNullOrBlank()) { + profileManagement.requestUpdateMobile(username, request.mobile) + } else { + throw OpexError.BadRequest.exception("Either email or mobile must be provided.") + } } - @PutMapping("/mobile") - suspend fun verifyUpdateMobile( - @RequestBody request: VerifyUpdateMobileRequest, + @PatchMapping("/contact/update/otp-verification") + suspend fun confirmContactUpdate( + @RequestBody request: ContactUpdateConfirmRequest, @CurrentSecurityContext securityContext: SecurityContext ) { - profileManagement.updateMobile(securityContext.authentication.name, request.mobile, request.otp) - } - - @PostMapping("/email") - suspend fun requestUpdateEmail( - @RequestBody request: UpdateEmailRequest, - @CurrentSecurityContext securityContext: SecurityContext - ): TempOtpResponse { - return profileManagement.requestUpdateEmail(securityContext.authentication.name, request.email) - } - - @PutMapping("/email") - suspend fun verifyUpdateEmail( - @RequestBody request: VerifyUpdateEmailRequest, - @CurrentSecurityContext securityContext: SecurityContext - ) { - profileManagement.updateEmail(securityContext.authentication.name, request.email, request.otp) + val username = securityContext.authentication.name + if (!request.email.isNullOrBlank()) { + profileManagement.updateEmail(username, request.email, request.otpCode) + } else if (!request.mobile.isNullOrBlank()) { + profileManagement.updateMobile(username, request.mobile, request.otpCode) + } else { + throw OpexError.BadRequest.exception("Either email or mobile must be provided.") + } } } \ No newline at end of file diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateConfirmRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateConfirmRequest.kt new file mode 100644 index 000000000..2f263efe8 --- /dev/null +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateConfirmRequest.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.profile.app.dto + +data class ContactUpdateConfirmRequest( + val email: String? = null, + val mobile: String? = null, + val otpCode: String, +) \ No newline at end of file diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateRequest.kt new file mode 100644 index 000000000..30a26e587 --- /dev/null +++ b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/ContactUpdateRequest.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.profile.app.dto + +data class ContactUpdateRequest( + val email: String? = null, + val mobile: String? = null +) \ No newline at end of file diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt deleted file mode 100644 index d3d93a6bb..000000000 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateEmailRequest.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.nilin.opex.profile.app.dto - -data class UpdateEmailRequest( - val email: String -) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt deleted file mode 100644 index 356a43baf..000000000 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/UpdateMobileRequest.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.nilin.opex.profile.app.dto - -data class UpdateMobileRequest( - val mobile: String -) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt deleted file mode 100644 index e67aa3b83..000000000 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateEmailRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package co.nilin.opex.profile.app.dto - -data class VerifyUpdateEmailRequest( - val email: String, - val otp: String, -) diff --git a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt b/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt deleted file mode 100644 index 2c4a9c220..000000000 --- a/profile/profile-app/src/main/kotlin/co/nilin/opex/profile/app/dto/VerifyUpdateMobileRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package co.nilin.opex.profile.app.dto - -data class VerifyUpdateMobileRequest( - val mobile: String, - val otp: String, -)