17 Commits

Author SHA1 Message Date
hlaeja
afedd19143 [RELEASE] - Release version: 0.4.0 2025-08-18 10:46:29 +00:00
f0ff324cf2 add sql script to fix user 2025-08-18 12:45:48 +02:00
cf1b78ae0a clean up for change to test library and logging 2025-08-18 12:45:48 +02:00
69e293a25f update updateAccount in AccountController for PublicEventService 2025-08-18 12:45:48 +02:00
dec6b99281 add PublicEventService 2025-08-18 12:45:48 +02:00
93aad65385 set up kafka 2025-08-18 12:45:48 +02:00
3effd930ad add AccountUtil.kt with detectChanges 2025-08-18 12:45:48 +02:00
97b8becd08 update test remove role_ for user in DB 2025-08-18 12:45:48 +02:00
hlaeja
5e0ba7ed2a [RELEASE] - Bump version 2025-07-29 18:05:42 +00:00
hlaeja
32a630d6a3 [RELEASE] - Release version: 0.3.0 2025-07-29 18:05:41 +00:00
c468a5ffa3 update gradlew 2025-07-29 20:04:46 +02:00
3849fa8676 update project 2025-07-29 20:04:46 +02:00
1ee306c151 add GitHub action
- update release in README.md
- add action run checks
- add action release
- remove release.sh
2025-07-29 20:04:46 +02:00
18e95f7213 add actuator.http 2025-07-29 20:04:46 +02:00
82c590dc30 update sql files 2025-07-29 20:04:46 +02:00
da491cecfa update release.sh to move sql files to version folder. 2025-03-05 14:43:15 +01:00
3bc5805a87 [RELEASE] - bump version 2025-02-07 17:13:48 +01:00
41 changed files with 319 additions and 190 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
max_line_length = 120 max_line_length = 120
tab_width = 4 tab_width = 4
[*.{md,sh,sql,yaml,yml}] [*.{md,sh,sql,xml,xsd,yaml,yml}]
max_line_length = 1024 max_line_length = 1024
indent_size = 2 indent_size = 2
tab_width = 2 tab_width = 2

12
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Release
on:
workflow_dispatch:
jobs:
release:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/release.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
with:
TYPE: service

12
.github/workflows/run-checks.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Pull request validation
on:
pull_request:
paths-ignore:
- '.github/**'
jobs:
validate:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/run-checks.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}

View File

@@ -16,7 +16,7 @@ In twilight's hush, where mythic tales unfold, A ledger of legends, the bravest
## Development Configuration ## Development Configuration
Run `release.sh` script from `master` branch. Run release pipeline from `master` branch.
## Development Information ## Development Information

View File

@@ -1,10 +1,10 @@
plugins { plugins {
alias(hlaeja.plugins.kotlin.jvm) alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring) alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate) alias(hlaeja.plugins.spring.boot)
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
alias(hlaeja.plugins.spring.dependency.management) alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot) alias(hlaeja.plugins.certificate)
alias(hlaeja.plugins.service)
} }
dependencies { dependencies {
@@ -14,6 +14,7 @@ dependencies {
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.common.messages) implementation(hlaeja.library.common.messages)
implementation(hlaeja.library.jwt) implementation(hlaeja.library.jwt)
implementation(hlaeja.springboot.kafka)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.security) implementation(hlaeja.springboot.starter.security)
@@ -28,18 +29,19 @@ dependencies {
testImplementation(hlaeja.projectreactor.reactor.test) testImplementation(hlaeja.projectreactor.reactor.test)
testImplementation(hlaeja.kotlin.test.junit5) testImplementation(hlaeja.kotlin.test.junit5)
testImplementation(hlaeja.kotlinx.coroutines.test) testImplementation(hlaeja.kotlinx.coroutines.test)
testImplementation(hlaeja.springboot.kafka.test)
testImplementation(hlaeja.springboot.starter.test) testImplementation(hlaeja.springboot.starter.test)
testRuntimeOnly(hlaeja.junit.platform.launcher) testRuntimeOnly(hlaeja.junit.platform.launcher)
integrationTestImplementation(hlaeja.assertj.core) testIntegrationImplementation(hlaeja.assertj.core)
integrationTestImplementation(hlaeja.library.test) testIntegrationImplementation(hlaeja.library.test)
integrationTestImplementation(hlaeja.projectreactor.reactor.test) testIntegrationImplementation(hlaeja.projectreactor.reactor.test)
integrationTestImplementation(hlaeja.kotlin.test.junit5) testIntegrationImplementation(hlaeja.kotlin.test.junit5)
integrationTestImplementation(hlaeja.kotlinx.coroutines.test) testIntegrationImplementation(hlaeja.kotlinx.coroutines.test)
integrationTestImplementation(hlaeja.springboot.starter.test) testIntegrationImplementation(hlaeja.springboot.starter.test)
integrationTestRuntimeOnly(hlaeja.junit.platform.launcher) testIntegrationRuntimeOnly(hlaeja.junit.platform.launcher)
} }
group = "ltd.hlaeja" group = "ltd.hlaeja"

View File

@@ -1,4 +1,4 @@
kotlin.code.style=official kotlin.code.style=official
version=0.2.0 version=0.4.0
catalog=0.9.0 catalog=0.12.0
container.port.host=9050 container.port.host=9050

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@@ -10,8 +10,8 @@ Content-Type: application/json
"password": "p4ssw0rd", "password": "p4ssw0rd",
"enabled": true, "enabled": true,
"roles": [ "roles": [
"ROLE_ADMIN", "ADMIN",
"ROLE_TEST" "TEST"
] ]
} }
@@ -24,7 +24,7 @@ Content-Type: application/json
"password": "pass", "password": "pass",
"enabled": true, "enabled": true,
"roles": [ "roles": [
"ROLE_TEST" "TEST"
] ]
} }
@@ -36,6 +36,6 @@ Content-Type: application/json
"username": "user", "username": "user",
"enabled": true, "enabled": true,
"roles": [ "roles": [
"ROLE_TEST" "TEST"
] ]
} }

5
http/actuator.http Normal file
View File

@@ -0,0 +1,5 @@
### get actuator
GET {{hostname}}/actuator
### get actuator health
GET {{hostname}}/actuator/health

View File

@@ -1,89 +0,0 @@
#!/bin/sh
### This should be a pipeline, but for this example let use this ###
check_active_branch() {
if [ "$(git rev-parse --abbrev-ref HEAD)" != "$1" ]; then
echo "Error: The current branch is not $1."
exit 1
fi
}
check_uncommitted_changes() {
if [ -n "$(git status --porcelain)" ]; then
echo "Error: There are uncommitted changes in the repository."
exit 1
fi
}
prepare_environment() {
git fetch origin
}
check_last_commit() {
last_commit_message=$(git log -1 --pretty=format:%s)
if [ "$last_commit_message" = "[RELEASE] - bump version" ]; then
echo "Warning: Nothing to release!!!"
exit 1
fi
}
check_differences() {
if ! git diff --quiet origin/"$1" "$1"; then
echo "Error: The branches origin/$1 and $1 have differences."
exit 1
fi
}
un_snapshot_version() {
sed -i "s/\($1\s*=\s*[0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/" gradle.properties
}
current_version() {
awk -F '=' '/version\s*=\s*[0-9.]*/ {gsub(/^ +| +$/,"",$2); print $2}' gradle.properties
}
stage_files() {
for file in "$@"; do
if git diff --exit-code --quiet -- "$file"; then
echo "No changes in $file"
else
git add "$file"
echo "Changes in $file staged for commit"
fi
done
}
commit_change() {
stage_files gradle.properties
git commit -m "[RELEASE] - $1"
git push --porcelain origin master
}
add_release_tag() {
gitTag="v$(current_version)"
git tag -a "$gitTag" -m "Release version $gitTag"
git push --porcelain origin "$gitTag"
}
snapshot_version() {
new_version="$(current_version | awk -F '.' '{print $1 "." $2+1 ".0"}')"
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
}
# check and prepare for release
check_active_branch master
check_uncommitted_changes
prepare_environment
check_last_commit
check_differences master
# un-snapshot version for release
un_snapshot_version version
un_snapshot_version catalog
# release changes and prepare for next release
commit_change "release version: $(current_version)"
add_release_tag
snapshot_version
commit_change 'bump version'

View File

@@ -0,0 +1,6 @@
UPDATE public.accounts
SET
roles = REPLACE(roles, 'ROLE_', ''),
updated_at = CURRENT_TIMESTAMP
WHERE
roles LIKE '%ROLE_%';

View File

@@ -56,10 +56,12 @@ CREATE DATABASE account_registry
WITH WITH
OWNER = role_administrator OWNER = role_administrator
ENCODING = 'UTF8' ENCODING = 'UTF8'
LC_COLLATE = 'en_US.utf8' LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.utf8' LC_CTYPE = 'en_US.UTF-8'
LOCALE_PROVIDER = 'libc' LOCALE_PROVIDER = 'libc'
TABLESPACE = pg_default TABLESPACE = pg_default
CONNECTION LIMIT = -1 CONNECTION LIMIT = -1
IS_TEMPLATE = False; IS_TEMPLATE = False;
COMMENT ON DATABASE account_registry
IS 'Primary database for user account registration and identity management';

View File

@@ -1,4 +1,5 @@
insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles) insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles)
values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_ADMIN'), values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ADMIN'),
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'), ('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER'),
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'); ('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER'),
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER');

View File

@@ -1,8 +0,0 @@
jwt:
private-key: cert/valid-private-key.pem
spring:
r2dbc:
url: r2dbc:pool:postgresql://localhost:5432/test
username: test
password: test

View File

@@ -1 +0,0 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9.D6pK86XPWcdu1imV_y_4nAM6R4WEZvJpQ7oGaPAYe0_rg3UWdmVMa8Iw7L21bRgFoyIa7FQBwb_0AXojFVdb2mdOVDeGOwxQZAx23dwqeicOGd8yUMnuBaRSnd7z4P65KPMbbf0NOTQtho0Iv5mBAwFMJoF67sw-yntfx3cD_bfrI-Rf4oZaZsVn38Y2HJBe2sO2QI4e5_7s82ikxac416OX7PcIEgaf3IeEK1fSzSjRG_dyBGT_Jq_vAzVURsSu4ep976kI-k5ZXNE9EMxKu1S-n5c5eiaqo96ObnaSl4eWFik5q8vLhNLYIYO-bQi1xlJKnStwZqtUwlR763Gd5w

View File

@@ -1 +0,0 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJST0xFX1VTRVIifQ.GvZIq0VF9xB8UY3PUGdnc6JNeUXtv4LzHJ56hWSeqUS6BXH0M_QJ5Lu9ndh9_P85CECp3eKrW4fKymGYe-NUXCtrzhr9-SSZLF6D7GRzAJ4yZjVRCOa_dgqe1RGuIZyZpli36z4NPqeBFqtHJ3Cs5rAI-WdvxGfWPgtM2kzpSJ_0zFihp9mVcZBlWP57HlN7-oKzDJWVpO2E17fWZTy-y4pdrIUsff63c256Cy8NhiAgux9aqZTdzaqp9TsXw59bRsS5d0YH7-gJuBd4xctZwgy_41BOcRk2q-nLyLZgWJs1wmCa_zaW0Fj6fjAsYvpdPNegkpIqrHJcQpGd7nE0KQ

View File

@@ -1,13 +1,15 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.validator.ValidAccount
import ltd.hlaeja.entity.AccountEntity import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.service.AccountService import ltd.hlaeja.service.AccountService
import ltd.hlaeja.service.PublicEventService
import ltd.hlaeja.util.detectChanges
import ltd.hlaeja.util.toAccountEntity import ltd.hlaeja.util.toAccountEntity
import ltd.hlaeja.util.toAccountResponse import ltd.hlaeja.util.toAccountResponse
import ltd.hlaeja.util.updateAccountEntity import ltd.hlaeja.util.updateAccountEntity
import ltd.hlaeja.validator.ValidAccount
import org.springframework.http.HttpStatus.ACCEPTED import org.springframework.http.HttpStatus.ACCEPTED
import org.springframework.http.HttpStatus.CREATED import org.springframework.http.HttpStatus.CREATED
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
@@ -25,6 +27,7 @@ import reactor.core.publisher.Mono
class AccountController( class AccountController(
private val accountService: AccountService, private val accountService: AccountService,
private val passwordEncoder: PasswordEncoder, private val passwordEncoder: PasswordEncoder,
private val publicEventService: PublicEventService,
) { ) {
@GetMapping("/account-{uuid}") @GetMapping("/account-{uuid}")
@@ -40,9 +43,13 @@ class AccountController(
): Mono<Account.Response> = accountService.getUserById(uuid) ): Mono<Account.Response> = accountService.getUserById(uuid)
.map { user -> .map { user ->
user.updateAccountEntity(request, passwordEncoder) user.updateAccountEntity(request, passwordEncoder)
.also { if (hasChange(user, it)) throw ResponseStatusException(ACCEPTED) } .let { it to it.detectChanges(user) }
.also { if (it.second.isEmpty()) throw ResponseStatusException(ACCEPTED) }
}
.flatMap { (updated: AccountEntity, changes: List<String>) ->
accountService.updateAccount(updated)
.flatMap { publicEventService.updateAccount(it, changes) }
} }
.flatMap { accountService.updateAccount(it) }
.map { it.toAccountResponse() } .map { it.toAccountResponse() }
@PostMapping("/account") @PostMapping("/account")
@@ -51,12 +58,4 @@ class AccountController(
@RequestBody @ValidAccount request: Account.Request, @RequestBody @ValidAccount request: Account.Request,
): Mono<Account.Response> = accountService.addAccount(request.toAccountEntity(passwordEncoder)) ): Mono<Account.Response> = accountService.addAccount(request.toAccountEntity(passwordEncoder))
.map { it.toAccountResponse() } .map { it.toAccountResponse() }
private fun hasChange(
user: AccountEntity,
update: AccountEntity,
): Boolean = user.password == update.password &&
user.username == update.username &&
user.enabled == update.enabled &&
user.roles == update.roles
} }

View File

@@ -0,0 +1,25 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.event.AccountMessage
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {}
@Service
class PublicEventService(
private val kafkaTemplate: KafkaTemplate<String, AccountMessage>,
) {
fun updateAccount(
account: AccountEntity,
changes: List<String>,
): Mono<AccountEntity> = Mono
.fromFuture(kafkaTemplate.send("account", "change", AccountMessage(account.id!!, changes)))
.doOnSuccess { log.trace { "Sent Kafka created event for user ${account.id}" } }
.doOnError { e -> log.error(e) { "Failed to send Kafka event" } }
.thenReturn(account)
}

View File

@@ -0,0 +1,25 @@
package ltd.hlaeja.util
import ltd.hlaeja.entity.AccountEntity
import org.springframework.http.HttpStatus.ACCEPTED
import org.springframework.web.server.ResponseStatusException
fun AccountEntity.detectChanges(original: AccountEntity): List<String> {
val changes: MutableList<String> = mutableListOf()
if (original.password != password) {
changes.add("password")
}
if (original.username != username) {
changes.add("username")
}
if (original.enabled != enabled) {
changes.add("enabled")
}
if (original.roles != roles) {
changes.add("roles")
}
if (changes.isEmpty()) {
throw ResponseStatusException(ACCEPTED)
}
return changes
}

View File

@@ -9,6 +9,25 @@ spring:
os: os:
name: "%APP_BUILD_OS_NAME%" name: "%APP_BUILD_OS_NAME%"
version: "%APP_BUILD_OS_VERSION%" version: "%APP_BUILD_OS_VERSION%"
r2dbc:
username: services
kafka:
producer:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
management:
endpoints:
access:
default: none
web:
exposure:
include: "health,info"
endpoint:
health:
show-details: always
access: read_only
info:
access: read_only
jwt: jwt:
private-key: cert/private_key.pem private-key: cert/private_key.pem
@@ -23,8 +42,9 @@ spring:
on-profile: development on-profile: development
r2dbc: r2dbc:
url: r2dbc:postgresql://localhost:5432/account_registry url: r2dbc:postgresql://localhost:5432/account_registry
username: services
password: password password: password
kafka:
bootstrap-servers: localhost:9091
--- ---
########################## ##########################
@@ -36,14 +56,17 @@ spring:
on-profile: docker on-profile: docker
r2dbc: r2dbc:
url: r2dbc:postgresql://PostgreSQL:5432/account_registry url: r2dbc:postgresql://PostgreSQL:5432/account_registry
username: services
password: password password: password
kafka:
bootstrap-servers: kafka:9092
--- ---
############################## ##############################
### Production environment ### ### Kubernetes environment ###
############################## ##############################
spring: spring:
config: config:
activate: activate:
on-profile: production on-profile: kubernetes
r2dbc:
url: r2dbc:postgresql://dependency-postgresql:5432/account_registry

View File

@@ -7,7 +7,10 @@
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<springProfile name="develop|docker"> <springProfile name="development">
<logger level="TRACE" name="ltd.hlaeja"/>
</springProfile>
<springProfile name="docker">
<logger level="DEBUG" name="ltd.hlaeja"/> <logger level="DEBUG" name="ltd.hlaeja"/>
</springProfile> </springProfile>
</configuration> </configuration>

View File

@@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger level="DEBUG" name="ltd.hlaeja"/>
</configuration>

View File

@@ -2,7 +2,7 @@ package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.PostgresContainer import ltd.hlaeja.test.container.KafkaPostgresTestContainer
import org.assertj.core.api.SoftAssertions import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
@@ -20,7 +20,7 @@ import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer @KafkaPostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT) @SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class) @ExtendWith(SoftAssertionsExtension::class)
class AccountEndpoint { class AccountEndpoint {
@@ -57,7 +57,7 @@ class AccountEndpoint {
softly.assertThat(it.responseBody?.username).isEqualTo("admin") softly.assertThat(it.responseBody?.username).isEqualTo("admin")
softly.assertThat(it.responseBody?.enabled).isTrue softly.assertThat(it.responseBody?.enabled).isTrue
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1) softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1)
softly.assertThat(it.responseBody?.roles?.get(0)).isEqualTo("ROLE_ADMIN") softly.assertThat(it.responseBody?.roles?.get(0)).isEqualTo("ADMIN")
} }
} }
@@ -97,7 +97,7 @@ class AccountEndpoint {
username = "usernameA", username = "usernameA",
password = "abc123", password = "abc123",
enabled = true, enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"), roles = listOf("USER", "TEST"),
) )
// when // when
@@ -111,8 +111,8 @@ class AccountEndpoint {
softly.assertThat(it.responseBody?.username).isEqualTo("usernameA") softly.assertThat(it.responseBody?.username).isEqualTo("usernameA")
softly.assertThat(it.responseBody?.enabled).isTrue softly.assertThat(it.responseBody?.enabled).isTrue
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(2) softly.assertThat(it.responseBody?.roles?.size).isEqualTo(2)
softly.assertThat(it.responseBody?.roles).contains("ROLE_USER") softly.assertThat(it.responseBody?.roles).contains("USER")
softly.assertThat(it.responseBody?.roles).contains("ROLE_TEST") softly.assertThat(it.responseBody?.roles).contains("TEST")
} }
} }
@@ -124,7 +124,7 @@ class AccountEndpoint {
username = "usernameB", username = "usernameB",
password = null, password = null,
enabled = false, enabled = false,
roles = listOf("ROLE_TEST"), roles = listOf("TEST"),
) )
// when // when
@@ -138,7 +138,7 @@ class AccountEndpoint {
softly.assertThat(it.responseBody?.username).isEqualTo("usernameB") softly.assertThat(it.responseBody?.username).isEqualTo("usernameB")
softly.assertThat(it.responseBody?.enabled).isFalse softly.assertThat(it.responseBody?.enabled).isFalse
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1) softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1)
softly.assertThat(it.responseBody?.roles).contains("ROLE_TEST") softly.assertThat(it.responseBody?.roles).contains("TEST")
} }
} }
@@ -150,7 +150,7 @@ class AccountEndpoint {
username = "user", username = "user",
password = null, password = null,
enabled = true, enabled = true,
roles = listOf("ROLE_USER"), roles = listOf("USER"),
) )
// when // when
@@ -168,7 +168,7 @@ class AccountEndpoint {
username = "admin", username = "admin",
password = null, password = null,
enabled = true, enabled = true,
roles = listOf("ROLE_USER"), roles = listOf("USER"),
) )
// when // when
@@ -186,7 +186,7 @@ class AccountEndpoint {
username = "admin", username = "admin",
password = null, password = null,
enabled = true, enabled = true,
roles = listOf("ROLE_USER"), roles = listOf("USER"),
) )
// when // when
@@ -202,9 +202,9 @@ class AccountEndpoint {
@ParameterizedTest @ParameterizedTest
@CsvSource( @CsvSource(
"new-user, new-pass, true, 2, ROLE_USER;ROLE_TEST", "new-user, new-pass, true, 2, USER;TEST",
"admin-user, admin-pass, false, 1, ROLE_ADMIN", "admin-user, admin-pass, false, 1, ADMIN",
"test-user, test-pass, true, 1, ROLE_USER", "test-user, test-pass, true, 1, USER",
) )
fun `success added account`( fun `success added account`(
username: String, username: String,
@@ -242,8 +242,8 @@ class AccountEndpoint {
@ParameterizedTest @ParameterizedTest
@CsvSource( @CsvSource(
"'', new-pass, ROLE_TEST", "'', new-pass, TEST",
"new-user, '', ROLE_ADMIN", "new-user, '', ADMIN",
"new-user, new-pass, ''", "new-user, new-pass, ''",
) )
fun `validation fail on empty values`( fun `validation fail on empty values`(
@@ -276,7 +276,7 @@ class AccountEndpoint {
username = "user", username = "user",
password = "new-pass", password = "new-pass",
enabled = true, enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"), roles = listOf("USER", "TEST"),
) )
// when // when
@@ -293,7 +293,7 @@ class AccountEndpoint {
username = "user", username = "user",
password = null, password = null,
enabled = true, enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"), roles = listOf("USER", "TEST"),
) )
// when // when

View File

@@ -1,7 +1,7 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.PostgresContainer import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -13,7 +13,7 @@ import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer @PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT) @SpringBootTest(webEnvironment = RANDOM_PORT)
class AccountsEndpoint { class AccountsEndpoint {

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import org.assertj.core.api.Assertions.assertThat
import ltd.hlaeja.library.accountRegistry.Authentication import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.test.compareToFile import ltd.hlaeja.test.compareToFile
import ltd.hlaeja.test.container.PostgresContainer import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
@@ -17,7 +17,7 @@ import org.springframework.http.HttpStatus
import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer @PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT) @SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class) @ExtendWith(SoftAssertionsExtension::class)
class AuthenticationEndpoint { class AuthenticationEndpoint {

View File

@@ -0,0 +1,19 @@
jwt:
private-key: cert/valid-private-key.pem
spring:
r2dbc:
url: r2dbc:postgresql://placeholder
username: placeholder
password: placeholder
kafka:
consumer:
group-id: test-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiQURNSU4ifQ.Z2mc__k9apWjJoZzJ3DWZuDiVN_jpisWtd0ecwrMnk1NrJ5Uw25pgrXPwn2aY0qYFAe0UGbE-4FhjUCxWLkknR0B-2_86IKHmN1A7z8lTqMRkK7qH-71uK0Y3o0kWKn117-FoSKDG-oefQE42NTwsSrzhiaEIzhUd0fsIyKuQCbDRol79rX5cU1HwOI8DHowpNEgvCLW1ogMWJDygq5GDgQI2HmV8vbnO1soFjKzvW3pz0sHWTimhAi76gl5mD_Lv_DdywcQWndwcGEoNj-SgHuKWktaG2_yzkoC9FQqWBgU7tukuycmLkbde_Oagydt2CAfPsBebu4Ac81UHGdUpw

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJVU0VSIn0.kpmQYxhkyKsIjo9mJaysBXW0xdv8UjlmNnVsYNfBu-Tdro_0nQFVzhCcjaD6_TUhx2-3vSkvTwDtmMHsP0JC5B43K473o2zQjyHYzCNakPcNHiste9llNj12n5qUCOUMgCKb7ZztLffSIsYlSL7hyRwwmTaz73MDMYvLWAa4AgSNm8JPe3HkTkqRJ4YZ-saKO9Q0Vb9LLftB7T3b9P5kHYqzwISBsRm1rYHRRpGYs5goR2Qax1hLJBbQR4bswaeTRfl3fQ66mIr6mZqiY279wCzzueLuGyJPFzeZQYiQ2JiYRq3H2NyXCsWKCt2bK-YNwol1K3fYLPSq9kap-AGasQ

View File

@@ -1,5 +1,5 @@
-- Test data -- Test data
insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles) insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles)
values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_ADMIN'), values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ADMIN'),
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'), ('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER'),
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'); ('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER');

View File

@@ -3,8 +3,8 @@ ALTER TABLE accounts DISABLE TRIGGER ALL;
ALTER TABLE accounts_audit DISABLE TRIGGER ALL; ALTER TABLE accounts_audit DISABLE TRIGGER ALL;
-- Truncate tables -- Truncate tables
TRUNCATE TABLE accounts_audit; TRUNCATE TABLE accounts_audit CASCADE;
TRUNCATE TABLE accounts; TRUNCATE TABLE accounts CASCADE;
-- Enable triggers on the account table -- Enable triggers on the account table
ALTER TABLE accounts ENABLE TRIGGER ALL; ALTER TABLE accounts ENABLE TRIGGER ALL;

View File

@@ -72,8 +72,3 @@ CREATE OR REPLACE TRIGGER accounts_audit_trigger
ON public.accounts ON public.accounts
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION public.accounts_audit(); EXECUTE FUNCTION public.accounts_audit();
-- Test data
insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles)
values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_ADMIN'),
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'),
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER');

View File

@@ -0,0 +1,109 @@
package ltd.hlaeja.util
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.web.server.ResponseStatusException
@ExtendWith(SoftAssertionsExtension::class)
class AccountUtilKtTest {
companion object {
val account = UUID.fromString("00000000-0000-0000-0000-000000000000")
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC"))
val originalUser = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = true,
username = "username",
password = "password",
roles = "ROLE_TEST",
)
}
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@Test
fun `no change detected`() {
// given
val account = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = true,
username = "username",
password = "password",
roles = "ROLE_TEST",
)
// then exception
assertThrows(ResponseStatusException::class.java) {
account.detectChanges(originalUser)
}
}
@ParameterizedTest
@CsvSource(
"false, username, password, ROLE_TEST, enabled",
"true, change, password, ROLE_TEST, username",
"true, username, change, ROLE_TEST, password",
"true, username, password, ROLE_CHANGE, roles",
)
fun `change one thing`(
enabled: Boolean,
username: String,
password: String,
roles: String,
expected: String,
) {
// given
val account = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = enabled,
username = username,
password = password,
roles = roles,
)
// when
val result = account.detectChanges(originalUser)
// then
softly.assertThat(result).hasSize(1)
softly.assertThat(result[0]).isEqualTo(expected)
}
@Test
fun `change everything`() {
// given
val account = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = false,
username = "change",
password = "change",
roles = "ROLE_CHANGE",
)
// when
val result = account.detectChanges(originalUser)
// then
softly.assertThat(result).hasSize(4)
softly.assertThat(result).contains("password", "username", "enabled", "roles")
}
}