Skip to content

Commit

Permalink
Add profile editing
Browse files Browse the repository at this point in the history
  • Loading branch information
sbont committed Jan 5, 2024
1 parent 7c8dcfd commit 7652c80
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,6 @@ class ChoristerConfiguration(
return buildOAuthClient(oauth2Client)
}

/*@Bean
fun zitadelClient(
authorizedClientManager: OAuth2AuthorizedClientManager
): WebClient {
val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauth2Client.setDefaultClientRegistrationId("zitadel")
return buildOAuthClient(oauth2Client)
}*/

private fun buildOAuthClient(oauth2Client: ServletOAuth2AuthorizedClientExchangeFilterFunction) =
WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
Expand All @@ -170,13 +161,6 @@ class ChoristerConfiguration(
)
.build()


@Bean
fun zitadelClientService(
zitadelConfiguration: ZitadelProperties,
oauthClientManager: OAuth2AuthorizedClientManager
): UserAuthorizationService = ZitadelUserService(zitadelConfiguration, authorizedWebClient(oauthClientManager))

@Bean
fun registrationService(
userRepository: UserRepository,
Expand All @@ -185,17 +169,28 @@ class ChoristerConfiguration(
userAuthorizationService: UserAuthorizationService,
categorisationService: CategorisationService,
userService: UserService
): RegistrationService = RegistrationService(userRepository, choirRepository, inviteRepository, userAuthorizationService, categorisationService, userService)
): RegistrationService = RegistrationService(
userRepository,
choirRepository,
inviteRepository,
userAuthorizationService,
categorisationService,
userService
)


@Bean
fun categorisationService(choristerProperties: ChoristerProperties, categoryRepository: CategoryRepository): CategorisationService = CategorisationService(choristerProperties, categoryRepository)
fun categorisationService(
choristerProperties: ChoristerProperties,
categoryRepository: CategoryRepository
): CategorisationService = CategorisationService(choristerProperties, categoryRepository)

@Bean
fun databaseInitializer(choirRepository: ChoirRepository,
userRepository: UserRepository,
songbookRepository: SongbookRepository,
songRepository: SongRepository
fun databaseInitializer(
choirRepository: ChoirRepository,
userRepository: UserRepository,
songbookRepository: SongbookRepository,
songRepository: SongRepository
) = ApplicationRunner {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ package nl.stevenbontenbal.chorister.event

import nl.stevenbontenbal.chorister.model.entities.User
import nl.stevenbontenbal.chorister.repository.ChoirRepository
import nl.stevenbontenbal.chorister.service.UserService
import org.springframework.data.rest.core.annotation.HandleBeforeCreate
import org.springframework.data.rest.core.annotation.HandleBeforeSave
import org.springframework.data.rest.core.annotation.RepositoryEventHandler
import org.springframework.stereotype.Component

@Component
@RepositoryEventHandler(User::class)
class UserEventHandler(private val choirRepository: ChoirRepository) {
class UserEventHandler(
private val choirRepository: ChoirRepository,
private val userService: UserService
) {

@HandleBeforeCreate
fun handleUserCreate(u: User) {
saveChoir(u)
}

@HandleBeforeSave
fun handleUserSave(u: User) {
userService.setUserEmail(u)
}

fun saveChoir(u: User) {
choirRepository.save(u.choir ?: return)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nl.stevenbontenbal.chorister.model.dto

import java.util.*

data class ZitadelResourceDetails(
var sequence: Int?,
var creationDate: Date?,
var changeDate: Date?,
var resourceOwner: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package nl.stevenbontenbal.chorister.model.dto

data class ZitadelUserEmailGetResponse(
var details: ZitadelResourceDetails,
var email: EmailInfo?
)

data class EmailInfo(
var email: String,
var isEmailVerified: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package nl.stevenbontenbal.chorister.model.dto

data class ZitadelUserEmailPutRequest(
var email: String,
var isEmailVerified: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nl.stevenbontenbal.chorister.model.dto

data class ZitadelUsernamePutRequest(
var userName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@ import org.springframework.stereotype.Component
@Component
@Lazy
@DependsOn("accessPermissionEvaluator")
class UserService(private val userRepository: UserRepository) {

class UserService(private val userRepository: UserRepository, private val zitadelUserService: ZitadelUserService) {
fun getCurrentUser(): User {
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
val userId = jwt.subject
val userId = getUserId()
return userRepository.findByZitadelId(userId) ?: throw AuthException("User with Zitadel ID $userId not found.")
}

private fun getUserId(): String {
val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
return jwt.subject
}

fun setUserEmail(user: User) {
val userId = getUserId()
val newEmail = user.email
if (newEmail != null) {
zitadelUserService.setEmailAddress(userId, newEmail)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import nl.stevenbontenbal.chorister.model.dto.*
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.util.UriComponentsBuilder
import reactor.core.publisher.Mono
import java.net.URI

@Component
class ZitadelUserService(
private val zitadelConfiguration: ZitadelProperties,
private val webClient: WebClient
Expand All @@ -23,15 +24,8 @@ class ZitadelUserService(
val request = createUserPostRequest(registrationRequest)
val response = webClient
.post()
.uri(
UriComponentsBuilder
.fromHttpUrl(zitadelConfiguration.baseUrl)
.path("/users/human/_import")
.build()
.toUri()
)
.uri(createUri("/users/human/_import"))
.headers { it.setBearerAuth(zitadelConfiguration.adminAccessToken) }
// .attributes(clientRegistrationId("zitadel"))
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(request))
.retrieve()
Expand All @@ -51,6 +45,74 @@ class ZitadelUserService(
return Result.success(response?.body?.userId)
}

fun setEmailAddress(userId: String, email: String): Result<String> {
val response = getCurrentEmail(userId)
if (response?.body?.email?.email == email)
return Result.success(email)

val emailRequest = ZitadelUserEmailPutRequest(email, false)
changeEmail(userId, emailRequest)

val userNameRequest = ZitadelUsernamePutRequest(email)
changeUserName(userId, userNameRequest)

return Result.success(email)
}

private fun changeUserName(
userId: String,
request: ZitadelUsernamePutRequest
) {
webClient
.put()
.uri(createUri("/users/$userId/username"))
.headers { it.setBearerAuth(zitadelConfiguration.adminAccessToken) }
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(request))
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) {
it.bodyToMono(ZitadelError::class.java).flatMap { err ->
Mono.error(InvalidInputException("Error while updating user: ${err.message}"))
}
}
.onStatus(HttpStatusCode::is5xxServerError) {
Mono.error(RuntimeException("Zitadel server error: ${it.statusCode()}"))
}
.toEntity(ZitadelResourceDetails::class.java)
.block()
}

private fun changeEmail(
userId: String,
request: ZitadelUserEmailPutRequest
) {
webClient
.put()
.uri(createUri("/users/$userId/email"))
.headers { it.setBearerAuth(zitadelConfiguration.adminAccessToken) }
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(request))
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) {
it.bodyToMono(ZitadelError::class.java).flatMap { err ->
Mono.error(InvalidInputException("Error while updating user: ${err.message}"))
}
}
.onStatus(HttpStatusCode::is5xxServerError) {
Mono.error(RuntimeException("Zitadel server error: ${it.statusCode()}"))
}
.toEntity(ZitadelUserEmailGetResponse::class.java)
.block()
}

private fun getCurrentEmail(userId: String) = webClient
.get()
.uri(createUri("/users/$userId/email"))
.headers { it.setBearerAuth(zitadelConfiguration.adminAccessToken) }
.retrieve()
.toEntity(ZitadelUserEmailGetResponse::class.java)
.block()

private fun createUserPostRequest(registrationRequest: RegistrationRequest): ZitadelUserPostRequest =
ZitadelUserPostRequest(
userName = registrationRequest.email,
Expand All @@ -59,9 +121,16 @@ class ZitadelUserService(
),
password = registrationRequest.password,
profile = Profile(
firstName = registrationRequest.displayName.substring(0,1),
firstName = registrationRequest.displayName.substring(0, 1),
lastName = registrationRequest.displayName.substring(1),
displayName = registrationRequest.displayName
)
)

private fun createUri(path: String): URI =
UriComponentsBuilder
.fromHttpUrl(zitadelConfiguration.baseUrl)
.path(path)
.build()
.toUri()
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package nl.stevenbontenbal.chorister.service

import io.mockk.Answer
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.slot
import nl.stevenbontenbal.chorister.configuration.ChoristerConfiguration
Expand All @@ -14,19 +12,22 @@ import nl.stevenbontenbal.chorister.repository.CategoryRepository
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringRunner


@RunWith(SpringRunner::class)
@ContextConfiguration(classes = [ChoristerConfiguration::class])
class CategorisationServiceTests @Autowired constructor(
) {
class CategorisationServiceTests {
@Test
fun `when createDefaultCategories then exist categories`() {
// Arrange
val properties = ChoristerProperties("", "", "", ChoristerProperties.DefaultCategories(listOf("Test1", "Test2"), listOf("Test3")))
val properties = ChoristerProperties(
"",
"",
"",
ChoristerProperties.DefaultCategories(listOf("Test1", "Test2"), listOf("Test3"))
)
val choir = Choir.create(null)
val slot = slot<Iterable<Category>>()
val categoryRepository: CategoryRepository = mockk(relaxed = true)
Expand All @@ -38,5 +39,4 @@ class CategorisationServiceTests @Autowired constructor(
assertThat(slot.captured.iterator().hasNext()).isTrue
}


}
Loading

0 comments on commit 7652c80

Please sign in to comment.