10 Commits

Author SHA1 Message Date
hlaeja
721a8e826a [RELEASE] - Release version: 0.5.0 2025-07-29 18:35:13 +00:00
f32bd4ac24 update gradlew 2025-07-29 20:34:41 +02:00
324c3f8ac2 add GitHub action
- update release in README.md
- add action run checks
- add action release
- remove release.sh
2025-07-29 20:34:41 +02:00
3510822c45 remove metrics collection 2025-07-29 20:34:41 +02:00
791b7aca36 update project 2025-07-29 20:34:41 +02:00
b80cfacaaf add actuator.http 2025-07-29 20:34:41 +02:00
6715354be3 [RELEASE] - bump version 2025-01-02 07:26:44 +01:00
35c7712f85 [RELEASE] - release version: 0.4.0 2025-01-02 07:26:41 +01:00
84d09f6dbb replace local jwt with library version
- update ConfigurationController to handle hlaeja jwt instead of jwtService
- update MeasurementController to handle hlaeja jwt instead of jwtService
- add dependency for hlaeja jwt
- remove dependencies for jjwt
- remove JwtService.kt
- remove PublicKeyProvider.kt
- remove jwt key property explanation from additional-spring-configuration-metadata.json
2025-01-02 06:48:12 +01:00
c5ff6e555a [RELEASE] - bump version 2024-12-28 08:05:01 +01:00
24 changed files with 101 additions and 322 deletions

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 }}

4
.gitignore vendored
View File

@@ -39,5 +39,5 @@ out/
### Kotlin ### ### Kotlin ###
.kotlin .kotlin
### cert ### #### Hlæja ###
cert/ /cert/

View File

@@ -4,36 +4,30 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose
## Properties for deployment ## Properties for deployment
| name | required | info | | name | required | info |
|----------------------------------------------|:--------:|----------------------------------------------| |-------------------------------|:--------:|------------------------------|
| spring.profiles.active | ✓ | Spring Boot environment | | spring.profiles.active | ✓ | Spring Boot environment |
| server.port | ✓ | HTTP port | | server.port | ✓ | HTTP port |
| server.ssl.enabled | ✓ | HTTP Enable SSL | | server.ssl.enabled | ✓ | HTTP Enable SSL |
| server.ssl.key-store | ✓ | HTTP Keystore | | server.ssl.key-store | ✓ | HTTP Keystore |
| server.ssl.key-store-type | ✓ | HTTP Cert Type | | server.ssl.key-store-type | ✓ | HTTP Cert Type |
| server.ssl.key-store-password | ✗ | HTTP Cert Pass | | server.ssl.key-store-password | ✗ | HTTP Cert Pass |
| spring.cache.type | | Cache type (redis) | | spring.cache.type | | Cache type (redis) |
| spring.data.redis.host | ✓ | Redis host | | spring.data.redis.host | ✓ | Redis host |
| spring.data.redis.port | | Redis port | | spring.data.redis.port | | Redis port |
| spring.data.redis.database | ✓ | Redis database | | spring.data.redis.database | ✓ | Redis database |
| cache.time-to-live | | Cache time to live (minutes) | | spring.data.redis.password | ✗ | Redis password |
| jwt.public-key | ✓ | JWT public key | | cache.time-to-live | | Cache time to live (minutes) |
| device-registry.url | ✓ | Device Register URL | | jwt.public-key | ✓ | JWT public key |
| device-data.url | ✓ | Device Data URL | | device-registry.url | ✓ | Device Register URL |
| device-configuration.url | ✓ | Device Configuration URL | | device-data.url | ✓ | Device Data URL |
| management.influx.metrics.export.api-version | | InfluxDB API version | | device-configuration.url | ✓ | Device Configuration URL |
| management.influx.metrics.export.enabled | | Enable/Disable exporting metrics to InfluxDB |
| management.influx.metrics.export.bucket | ✓ | InfluxDB bucket name |
| management.influx.metrics.export.org | ✓ | InfluxDB organization |
| management.influx.metrics.export.token | ✗ | InfluxDB token |
| management.influx.metrics.export.uri | ✓ | InfluxDB URL |
| management.metrics.tags.application | ✓ | Application instance tag for metrics |
*Required: ✓ can be stored as text, and ✗ need to be stored as secret.* *Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
## Releasing Service ## Releasing Service
Run `release.sh` script from `master` branch. Run release pipeline from `master` branch.
## Development Configuration ## Development Configuration

View File

@@ -1,12 +1,10 @@
import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer
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 {
@@ -15,16 +13,13 @@ dependencies {
implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.micrometer.registry.influx) implementation(hlaeja.library.common.messages)
implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.library.jwt)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.cache) implementation(hlaeja.springboot.starter.cache)
implementation(hlaeja.springboot.starter.redis) implementation(hlaeja.springboot.starter.redis)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl)
runtimeOnly(hlaeja.jjwt.jackson)
testImplementation(hlaeja.kotlin.test.junit5) testImplementation(hlaeja.kotlin.test.junit5)
testImplementation(hlaeja.kotlinx.coroutines.test) testImplementation(hlaeja.kotlinx.coroutines.test)
testImplementation(hlaeja.mockk) testImplementation(hlaeja.mockk)
@@ -36,17 +31,6 @@ dependencies {
group = "ltd.hlaeja" group = "ltd.hlaeja"
fun influxDbToken(): String = config.findOrDefault("influxdb.token", "INFLUXDB_TOKEN", "") tasks.named("processResources") {
dependsOn("copyCertificates")
tasks {
named("containerCreate", DockerCreateContainer::class) {
withEnvVar("MANAGEMENT_INFLUX_METRICS_EXPORT_TOKEN", influxDbToken())
}
withType<ProcessResources> {
filesMatching("**/application.yml") { filter { it.replace("%INFLUXDB_TOKEN%", influxDbToken()) } }
onlyIf { file("src/main/resources/application.yml").exists() }
}
named("processResources") {
dependsOn("copyCertificates")
}
} }

View File

@@ -1,7 +1,7 @@
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx1g org.gradle.jvmargs=-Xmx1g
version=0.3.0 version=0.5.0
catalog=0.7.0 catalog=0.11.0
docker.port.expose=8443 docker.port.expose=8443
container.port.expose=8443 container.port.expose=8443
container.port.host=9000 container.port.host=9000

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.10.2-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

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

@@ -1,7 +1,9 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.jwt.service.PublicJwtService
import ltd.hlaeja.service.DeviceConfigurationService import ltd.hlaeja.service.DeviceConfigurationService
import ltd.hlaeja.service.JwtService import ltd.hlaeja.service.DeviceRegistryService
import ltd.hlaeja.util.toDeviceResponse import ltd.hlaeja.util.toDeviceResponse
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestHeader
@@ -12,12 +14,17 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/configuration") @RequestMapping("/configuration")
class ConfigurationController( class ConfigurationController(
private val configurationService: DeviceConfigurationService, private val configurationService: DeviceConfigurationService,
private val jwtService: JwtService, private val deviceRegistry: DeviceRegistryService,
private val publicJwtService: PublicJwtService,
) { ) {
@GetMapping @GetMapping
suspend fun getNodeConfiguration( suspend fun getNodeConfiguration(
@RequestHeader("Identity") identityToken: String, @RequestHeader("Identity") identityToken: String,
): Map<String, String> = jwtService.getIdentity(identityToken) ): Map<String, String> = readIdentityToken(identityToken)
.let { deviceRegistry.getIdentityFromDevice(it) }
.let { configurationService.getConfiguration(it.node).toDeviceResponse() } .let { configurationService.getConfiguration(it.node).toDeviceResponse() }
private fun readIdentityToken(identityToken: String): UUID = publicJwtService
.verify(identityToken) { claims -> UUID.fromString(claims.payload["device"] as String) }
} }

View File

@@ -1,8 +1,11 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.jwt.service.PublicJwtService
import ltd.hlaeja.library.deviceData.MeasurementData import ltd.hlaeja.library.deviceData.MeasurementData
import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.service.DeviceDataService import ltd.hlaeja.service.DeviceDataService
import ltd.hlaeja.service.JwtService import ltd.hlaeja.service.DeviceRegistryService
import org.springframework.http.HttpStatus.CREATED import org.springframework.http.HttpStatus.CREATED
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
@@ -16,13 +19,14 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/measurement") @RequestMapping("/measurement")
class MeasurementController( class MeasurementController(
private val dataService: DeviceDataService, private val dataService: DeviceDataService,
private val jwtService: JwtService, private val deviceRegistry: DeviceRegistryService,
private val publicJwtService: PublicJwtService,
) { ) {
@GetMapping @GetMapping
suspend fun getNodeMeasurement( suspend fun getNodeMeasurement(
@RequestHeader("Identity") identityToken: String, @RequestHeader("Identity") identityToken: String,
): Map<String, Number> = jwtService.getIdentity(identityToken) ): Map<String, Number> = readIdentityToken(identityToken)
.let { dataService.getMeasurement(it.client, it.node).fields } .let { dataService.getMeasurement(it.client, it.node).fields }
@PostMapping @PostMapping
@@ -31,7 +35,7 @@ class MeasurementController(
@RequestHeader("Identity") identityToken: String, @RequestHeader("Identity") identityToken: String,
@RequestBody measurement: Map<String, Number>, @RequestBody measurement: Map<String, Number>,
) { ) {
return jwtService.getIdentity(identityToken) return readIdentityToken(identityToken)
.let { .let {
dataService.addMeasurement( dataService.addMeasurement(
it.client, it.client,
@@ -45,4 +49,8 @@ class MeasurementController(
) )
} }
} }
private suspend fun readIdentityToken(identityToken: String): Identity.Response = publicJwtService
.verify(identityToken) { claims -> UUID.fromString(claims.payload["device"] as String) }
.let { deviceRegistry.getIdentityFromDevice(it) }
} }

View File

@@ -1,8 +1,6 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceConfiguration.Node import ltd.hlaeja.library.deviceConfiguration.Node
import ltd.hlaeja.property.DeviceConfigurationProperty import ltd.hlaeja.property.DeviceConfigurationProperty
@@ -18,29 +16,17 @@ private val log = KotlinLogging.logger {}
@Service @Service
class DeviceConfigurationService( class DeviceConfigurationService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceConfigurationProperty: DeviceConfigurationProperty, private val deviceConfigurationProperty: DeviceConfigurationProperty,
) { ) {
private val deviceConfigurationSuccess = Counter.builder("device.configuration.success")
.description("Number of successful device configuration calls")
.register(meterRegistry)
private val deviceConfigurationFailure = Counter.builder("device.configuration.failure")
.description("Number of failed device configuration calls")
.register(meterRegistry)
suspend fun getConfiguration( suspend fun getConfiguration(
node: UUID, node: UUID,
): Node.Response = try { ): Node.Response = try {
webClient.deviceConfigurationGetConfiguration(node, deviceConfigurationProperty) webClient.deviceConfigurationGetConfiguration(node, deviceConfigurationProperty)
.also { deviceConfigurationSuccess.increment() }
} catch (e: ErrorResponseException) { } catch (e: ErrorResponseException) {
deviceConfigurationFailure.increment()
throw e throw e
} catch (e: WebClientRequestException) { } catch (e: WebClientRequestException) {
deviceConfigurationFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE) throw ResponseStatusException(SERVICE_UNAVAILABLE)
} }

View File

@@ -1,8 +1,6 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceData.MeasurementData import ltd.hlaeja.library.deviceData.MeasurementData
import ltd.hlaeja.property.DeviceDataProperty import ltd.hlaeja.property.DeviceDataProperty
@@ -20,30 +18,18 @@ private val log = KotlinLogging.logger {}
@Service @Service
class DeviceDataService( class DeviceDataService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceDataProperty: DeviceDataProperty, private val deviceDataProperty: DeviceDataProperty,
) { ) {
private val deviceDataSuccess = Counter.builder("device.data.success")
.description("Number of successful device data calls")
.register(meterRegistry)
private val deviceDataFailure = Counter.builder("device.data.failure")
.description("Number of failed device data calls")
.register(meterRegistry)
suspend fun getMeasurement( suspend fun getMeasurement(
client: UUID, client: UUID,
node: UUID, node: UUID,
): MeasurementData.Response = try { ): MeasurementData.Response = try {
webClient.deviceDataGetMeasurement(client, node, deviceDataProperty) webClient.deviceDataGetMeasurement(client, node, deviceDataProperty)
.also { deviceDataSuccess.increment() }
} catch (e: ErrorResponseException) { } catch (e: ErrorResponseException) {
deviceDataFailure.increment()
throw e throw e
} catch (e: WebClientRequestException) { } catch (e: WebClientRequestException) {
deviceDataFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE) throw ResponseStatusException(SERVICE_UNAVAILABLE)
} }
@@ -53,12 +39,9 @@ class DeviceDataService(
request: MeasurementData.Request, request: MeasurementData.Request,
): ResponseEntity<Void> = try { ): ResponseEntity<Void> = try {
webClient.deviceDataAddMeasurement(client, request, deviceDataProperty) webClient.deviceDataAddMeasurement(client, request, deviceDataProperty)
.also { deviceDataSuccess.increment() }
} catch (e: ErrorResponseException) { } catch (e: ErrorResponseException) {
deviceDataFailure.increment()
throw e throw e
} catch (e: WebClientRequestException) { } catch (e: WebClientRequestException) {
deviceDataFailure.increment()
log.error(e) { "Error device registry" } log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE) throw ResponseStatusException(SERVICE_UNAVAILABLE)
} }

View File

@@ -1,8 +1,6 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
@@ -19,30 +17,18 @@ private val log = KotlinLogging.logger {}
@Service @Service
class DeviceRegistryService( class DeviceRegistryService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceRegistryProperty: DeviceRegistryProperty, private val deviceRegistryProperty: DeviceRegistryProperty,
) { ) {
private val identityDeviceSuccess = Counter.builder("device.identity.success")
.description("Number of successful device identity calls")
.register(meterRegistry)
private val identityDeviceFailure = Counter.builder("device.identity.failure")
.description("Number of failed device identity calls")
.register(meterRegistry)
@Cacheable(value = ["identity"], key = "#device") @Cacheable(value = ["identity"], key = "#device")
suspend fun getIdentityFromDevice( suspend fun getIdentityFromDevice(
device: UUID, device: UUID,
): Identity.Response = try { ): Identity.Response = try {
webClient.deviceRegistryIdentityDevice(device, deviceRegistryProperty) webClient.deviceRegistryIdentityDevice(device, deviceRegistryProperty)
.also { identityDeviceSuccess.increment() }
} catch (e: ErrorResponseException) { } catch (e: ErrorResponseException) {
identityDeviceFailure.increment()
throw e throw e
} catch (e: WebClientRequestException) { } catch (e: WebClientRequestException) {
identityDeviceFailure.increment()
log.error(e) { "Error device identity" } log.error(e) { "Error device identity" }
throw ResponseStatusException(SERVICE_UNAVAILABLE) throw ResponseStatusException(SERVICE_UNAVAILABLE)
} }

View File

@@ -1,42 +0,0 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.JwtException
import io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.JwtProperty
import ltd.hlaeja.util.PublicKeyProvider
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {}
@Service
class JwtService(
jwtProperty: JwtProperty,
private val deviceRegistry: DeviceRegistryService,
) {
private val parser: JwtParser = Jwts.parser()
.verifyWith(PublicKeyProvider.load(jwtProperty.publicKey))
.build()
suspend fun getIdentity(
identityToken: String,
): Identity.Response = try {
readIdentity(identityToken)
.let { deviceRegistry.getIdentityFromDevice(it) }
} catch (e: JwtException) {
log.warn { e.localizedMessage }
throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
}
private suspend fun readIdentity(
identity: String,
): UUID = parser.parseSignedClaims(identity)
.let { UUID.fromString(it.payload["device"] as String) }
.also { log.debug { "Identified client device: $it" } }
}

View File

@@ -1,35 +0,0 @@
package ltd.hlaeja.util
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.Base64.getDecoder
import ltd.hlaeja.exception.KeyProviderException
object PublicKeyProvider {
fun load(
pemFile: String,
): RSAPublicKey = readPublicPemFile(pemFile)
.let(::makePublicKey)
private fun makePublicKey(
publicKeyBytes: ByteArray,
): RSAPublicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(publicKeyBytes)) as RSAPublicKey
private fun readPublicPemFile(
publicKey: String,
): ByteArray = javaClass.classLoader
.getResource(publicKey)
?.readText()
?.let(::getPublicKeyByteArray)
?: throw KeyProviderException("Could not load public key")
private fun getPublicKeyByteArray(
keyText: String,
): ByteArray = keyText.replace(Regex("[\r\n]+"), "")
.removePrefix("-----BEGIN PUBLIC KEY-----")
.removeSuffix("-----END PUBLIC KEY-----")
.let { getDecoder().decode(it) }
}

View File

@@ -20,11 +20,6 @@
"type": "java.lang.String", "type": "java.lang.String",
"description": "Application build os version." "description": "Application build os version."
}, },
{
"name": "jwt.public-key",
"type": "java.lang.String",
"description": "Jwt public key file."
},
{ {
"name": "device-registry.url", "name": "device-registry.url",
"type": "java.lang.String", "type": "java.lang.String",

View File

@@ -18,22 +18,17 @@ spring:
management: management:
endpoints: endpoints:
enabled-by-default: false access:
default: none
web: web:
exposure: exposure:
include: "health,info" include: "health,info"
endpoint: endpoint:
health: health:
enabled: true
show-details: always show-details: always
access: read_only
info: info:
enabled: true access: read_only
influx:
metrics:
export:
api-version: v2
bucket: hlaeja
org: hlaeja_ltd
cache: cache:
time-to-live: 10 time-to-live: 10
@@ -53,16 +48,6 @@ spring:
redis: redis:
host: localhost host: localhost
management:
metrics:
tags:
application: device-api
influx:
metrics:
export:
enabled: false
token: %INFLUXDB_TOKEN%
server: server:
port: 8443 port: 8443
ssl: ssl:
@@ -92,15 +77,6 @@ spring:
redis: redis:
host: Redis host: Redis
management:
metrics:
tags:
application: device-api
influx:
metrics:
export:
uri: http://InfluxDB:8086
server: server:
port: 8443 port: 8443
ssl: ssl:

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

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3ZdlbISX729m5Ur1pVhg
XIvazcgUt0T0G32ML0tfwQ4aWTfwPII0SQRThaN6eiiBMRa0V8JMih1LT8JmGgst
dEx2nhMbVs/Osu8MhmP86c+HB/jPa1+0IR1TZKXoZoF52D2ZtoVf+mOWggAcm1R+
V0Fj2cR/pgLkVt3GKUE2OokFC1iFUQFjThd1EzKcOv53TUek8FY8t66npQ4t3unD
bXZKoGXMuXCqZVykMbGTUQFRuT3NAOXRrJP+UDeY2uM2Yk98J+8FtLDYD6jpmyi0
ghv6k8pK1w1n5NI3atVv5ZMUeQZ36AXL8SZi1105mamhLVQ0e0JixoMOPh7ziFyv
uwIDAQAB
-----END PUBLIC KEY-----