7 Commits

Author SHA1 Message Date
e4a70e0e43 [RELEASE] - release version: 0.3.0 2024-12-28 08:04:58 +01:00
6522809dce add Redis cache and some clean up
- update and cleanup in README.md
- update getIdentity to throw response exception with 401 in JwtService
- update http files to use identity from env
- add cacheable to getIdentityFromDevice in DeviceRegistryService
- add RedisCacheConfiguration
- add CacheProperty
- set up cache property
- set up data redis cache
2024-12-28 05:45:23 +01:00
19f46bd01f add metrics collection for API
- extract webclient call from getConfiguration and add metrics in DeviceConfigurationService
- extract webclient call from getMeasurement, addMeasurement and add metrics in DeviceDataService
- extract webclient call from getIdentityFromDevice and add metrics in DeviceRegistryService
- add MeterRegistry dependency
- fix test application.yml
- update kotlin logging dependency
2024-12-27 23:37:55 +01:00
e48cf674a5 [RELEASE] - bump version 2024-12-10 23:53:59 +01:00
36721abdb9 [RELEASE] - release version: 0.2.0 2024-12-10 23:53:56 +01:00
0e5c4e3e9f update for common plugin v0.2.0
- cleanup Mapper.kt
- update build.gradle.kts to use certificate plugin
- update catalog version
2024-12-10 22:37:46 +01:00
5c1a0693b3 [RELEASE] - bump version 2024-11-30 20:53:19 +01:00
20 changed files with 379 additions and 116 deletions

View File

@@ -5,19 +5,31 @@ 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 |
| jwt.public-key | * | JWT public key | | spring.cache.type | | Cache type (redis) |
| device-registry.url | * | Device Register URL | | spring.data.redis.host | ✓ | Redis host |
| device-data.url | * | Device Data URL | | spring.data.redis.port | | Redis port |
| device-configuration.url | * | Device Configuration URL | | spring.data.redis.database | ✓ | Redis database |
| cache.time-to-live | | Cache time to live (minutes) |
| jwt.public-key | ✓ | JWT public key |
| device-registry.url | ✓ | Device Register URL |
| device-data.url | ✓ | Device Data URL |
| device-configuration.url | ✓ | Device Configuration URL |
| management.influx.metrics.export.api-version | | InfluxDB API version |
| 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
@@ -27,43 +39,32 @@ Run `release.sh` script from `master` branch.
### Developer Keystore ### Developer Keystore
1. Open `hosts` file: We use a keystore to enable HTTPS for our API. To set up your developer environment for local development, please refer to [generate keystore](https://github.com/swordsteel/hlaeja-development/blob/master/doc/keystore.md) documentation. When generating and exporting the certificate for local development, please store it in the `./cert/keystore.p12` folder at the project root.
* On Unix-like systems (Linux, macOS), this directory is typically `/etc/hosts`.
* On Windows, this directory is typically `%SystemRoot%\System32\drivers\etc\hosts`.
2. Add the following lines to the `hosts` file:
```text
127.0.0.1 deviceapi # Hlæja Device API
```
3. Generate Keystores
```shell
keytool -genkeypair -alias device-api -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=deviceapi" -keypass password -keystore ./cert/keystore.p12 -storetype PKCS12 -storepass password
```
4. Export the public certificate
```shell
keytool -export -alias device-api -keystore ./cert/keystore.p12 -storepass password -file ./cert/device-api.cer -rfc
```
### Public RSA Key ### Public RSA Key
To validate devices, copy file named `public_key.pem` from `./cert` generated for local development in **Hlæja Device Register** in to `./cert`. This service uses the public key from **[Hlæja Device Register](https://github.com/swordsteel/hlaeja-device-registry)** to identify devices. To set up device identification for local development, copy the `public_key.pem` file from the `./cert` directory in **Hlæja Device Register** into the `./cert` directory of this project.
### Global gradle properties *Note: For more information on generating RSA keys, please refer to our [generate RSA key](https://github.com/swordsteel/hlaeja-development/blob/master/doc/rsa_key.md) documentation.*
To authenticate with Gradle to access repositories that require authentication, you can set your user and token in the `gradle.properties` file. ### Global Settings
Here's how you can do it: This services rely on a set of global settings to configure development environments. These settings, managed through Gradle properties or environment variables.
1. Open or create the `gradle.properties` file in your Gradle user home directory: *Note: For more information on global properties, please refer to our [global settings](https://github.com/swordsteel/hlaeja-development/blob/master/doc/global_settings.md) documentation.*
- On Unix-like systems (Linux, macOS), this directory is typically `~/.gradle/`. #### Gradle Properties
- On Windows, this directory is typically `C:\Users\<YourUsername>\.gradle\`.
2. Add the following lines to the `gradle.properties` file:
```properties ```properties
repository.user=your_user repository.user=your_user
repository.token=your_token_value repository.token=your_token_value
influxdb.token=your_token_value
```
#### Environment Variables
```properties
REPOSITORY_USER=your_user
REPOSITORY_TOKEN=your_token_value
INFLUXDB_TOKEN=your_token_value
``` ```
or use environment variables `REPOSITORY_USER` and `REPOSITORY_TOKEN`

View File

@@ -1,6 +1,9 @@
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.ltd.hlaeja.plugin.service) 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.springframework.boot)
@@ -12,8 +15,11 @@ 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.hlaeja.common.messages) implementation(hlaeja.library.hlaeja.common.messages)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.cache)
implementation(hlaeja.springboot.starter.redis)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl) runtimeOnly(hlaeja.jjwt.impl)
@@ -30,20 +36,17 @@ dependencies {
group = "ltd.hlaeja" group = "ltd.hlaeja"
fun influxDbToken(): String = config.findOrDefault("influxdb.token", "INFLUXDB_TOKEN", "")
tasks { 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") { named("processResources") {
dependsOn("copyKeystore", "copyPublicKey") dependsOn("copyCertificates")
}
register<Copy>("copyKeystore") {
group = "hlaeja"
from("cert/keystore.p12")
into("${layout.buildDirectory.get()}/resources/main/cert")
onlyIf { file("cert/keystore.p12").exists() }
}
register<Copy>("copyPublicKey") {
group = "hlaeja"
from("cert/public_key.pem")
into("${layout.buildDirectory.get()}/resources/main/cert")
onlyIf { file("cert/public_key.pem").exists() }
} }
} }

View File

@@ -1,7 +1,7 @@
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx1g org.gradle.jvmargs=-Xmx1g
version=0.1.0 version=0.3.0
catalog=0.5.0 catalog=0.7.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

View File

@@ -1,3 +1,3 @@
### get measurement ### get configuration
GET {{hostname}}/configuration GET {{hostname}}/configuration
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}

View File

@@ -1,8 +1,10 @@
{ {
"development": { "development": {
"hostname": "https://localhost:8443" "hostname": "https://localhost:8443",
"identity": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
}, },
"docker": { "docker": {
"hostname": "https://localhost:9000" "hostname": "https://localhost:9000",
"identity": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
} }
} }

View File

@@ -1,11 +1,11 @@
### get measurement ### get measurement
GET {{hostname}}/measurement GET {{hostname}}/measurement
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}
### add measurement for all ### add measurement for all
POST {{hostname}}/measurement POST {{hostname}}/measurement
Content-Type: application/json Content-Type: application/json
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}
{ {
"button0": 0, "button0": 0,
@@ -16,8 +16,8 @@ Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVW
### add measurement for one ### add measurement for one
POST {{hostname}}/measurement POST {{hostname}}/measurement
Content-Type: application/json Content-Type: application/json
Identity: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ Identity: {{identity}}
{ {
"button0": 0 "button0": 1
} }

View File

@@ -1,5 +1,6 @@
package ltd.hlaeja package ltd.hlaeja
import ltd.hlaeja.property.CacheProperty
import ltd.hlaeja.property.DeviceConfigurationProperty import ltd.hlaeja.property.DeviceConfigurationProperty
import ltd.hlaeja.property.DeviceDataProperty import ltd.hlaeja.property.DeviceDataProperty
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
@@ -7,8 +8,11 @@ import ltd.hlaeja.property.JwtProperty
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
@EnableCaching
@EnableConfigurationProperties( @EnableConfigurationProperties(
CacheProperty::class,
DeviceConfigurationProperty::class, DeviceConfigurationProperty::class,
DeviceDataProperty::class, DeviceDataProperty::class,
DeviceRegistryProperty::class, DeviceRegistryProperty::class,

View File

@@ -0,0 +1,26 @@
package ltd.hlaeja.configuration
import java.time.Duration
import ltd.hlaeja.exception.CacheException
import ltd.hlaeja.property.CacheProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.core.RedisTemplate
@Configuration
class RedisCacheConfiguration(
private val cacheProperty: CacheProperty,
) {
@Bean
fun cacheManager(
redisTemplate: RedisTemplate<String, String>,
): RedisCacheManager = redisTemplate.connectionFactory
?.let { RedisCacheManager.builder(it).cacheDefaults(getRedisCacheConfiguration()).build() }
?: throw CacheException("Redis connection factory is not set")
private fun getRedisCacheConfiguration(): RedisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(cacheProperty.timeToLive))
}

View File

@@ -0,0 +1,23 @@
package ltd.hlaeja.exception
@Suppress("unused")
class CacheException : Exception {
constructor() : super()
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
constructor(
message: String,
cause: Throwable,
) : super(message, cause)
constructor(
message: String,
cause: Throwable,
enableSuppression: Boolean,
writableStackTrace: Boolean,
) : super(message, cause, enableSuppression, writableStackTrace)
}

View File

@@ -0,0 +1,8 @@
package ltd.hlaeja.property
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "cache")
data class CacheProperty(
val timeToLive: Long,
)

View File

@@ -1,28 +1,47 @@
package ltd.hlaeja.service package ltd.hlaeja.service
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
import ltd.hlaeja.util.logCall import ltd.hlaeja.util.deviceConfigurationGetConfiguration
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.http.HttpStatus.NO_CONTENT
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.ErrorResponseException
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBodyOrNull import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
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 = webClient.get() ): Node.Response = try {
.uri("${deviceConfigurationProperty.url}/node-$node".also(::logCall)) webClient.deviceConfigurationGetConfiguration(node, deviceConfigurationProperty)
.retrieve() .also { deviceConfigurationSuccess.increment() }
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } } catch (e: ErrorResponseException) {
.awaitBodyOrNull<Node.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT) deviceConfigurationFailure.increment()
throw e
} catch (e: WebClientRequestException) {
deviceConfigurationFailure.increment()
log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
} }

View File

@@ -1,39 +1,65 @@
package ltd.hlaeja.service package ltd.hlaeja.service
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
import ltd.hlaeja.util.logCall import ltd.hlaeja.util.deviceDataAddMeasurement
import org.springframework.http.HttpStatus.NOT_FOUND import ltd.hlaeja.util.deviceDataGetMeasurement
import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.ErrorResponseException
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBodilessEntity import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.reactive.function.client.awaitBodyOrNull
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
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 = webClient.get() ): MeasurementData.Response = try {
.uri("${deviceDataProperty.url}/client-$client/node-$node".also(::logCall)) webClient.deviceDataGetMeasurement(client, node, deviceDataProperty)
.retrieve() .also { deviceDataSuccess.increment() }
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } } catch (e: ErrorResponseException) {
.awaitBodyOrNull<MeasurementData.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT) deviceDataFailure.increment()
throw e
} catch (e: WebClientRequestException) {
deviceDataFailure.increment()
log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
suspend fun addMeasurement( suspend fun addMeasurement(
client: UUID, client: UUID,
request: MeasurementData.Request, request: MeasurementData.Request,
) = webClient.post() ): ResponseEntity<Void> = try {
.uri("${deviceDataProperty.url}/client-$client".also(::logCall)) webClient.deviceDataAddMeasurement(client, request, deviceDataProperty)
.bodyValue(request) .also { deviceDataSuccess.increment() }
.retrieve() } catch (e: ErrorResponseException) {
.awaitBodilessEntity() deviceDataFailure.increment()
throw e
} catch (e: WebClientRequestException) {
deviceDataFailure.increment()
log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
} }

View File

@@ -1,28 +1,49 @@
package ltd.hlaeja.service package ltd.hlaeja.service
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
import ltd.hlaeja.util.logCall import ltd.hlaeja.util.deviceRegistryIdentityDevice
import org.springframework.http.HttpStatus.NOT_ACCEPTABLE import org.springframework.cache.annotation.Cacheable
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.ErrorResponseException
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBodyOrNull import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
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")
suspend fun getIdentityFromDevice( suspend fun getIdentityFromDevice(
device: UUID, device: UUID,
): Identity.Response = webClient.get() ): Identity.Response = try {
.uri("${deviceRegistryProperty.url}/identity/device-$device".also(::logCall)) webClient.deviceRegistryIdentityDevice(device, deviceRegistryProperty)
.retrieve() .also { identityDeviceSuccess.increment() }
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_ACCEPTABLE) } } catch (e: ErrorResponseException) {
.awaitBodyOrNull<Identity.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT) identityDeviceFailure.increment()
throw e
} catch (e: WebClientRequestException) {
identityDeviceFailure.increment()
log.error(e) { "Error device identity" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
} }

View File

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

View File

@@ -1,9 +1,7 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import mu.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
fun logCall(url: String) { fun logCall(url: String) = log.debug { "calling: $url" }
log.debug("calling: {}", url)
}

View File

@@ -5,6 +5,6 @@ import ltd.hlaeja.library.deviceConfiguration.Node
fun Node.Response.toDeviceResponse(): Map<String, String> { fun Node.Response.toDeviceResponse(): Map<String, String> {
return mapOf( return mapOf(
"version" to timestamp.toEpochSecond().toString(), "version" to timestamp.toEpochSecond().toString(),
"data" to configuration "data" to configuration,
) )
} }

View File

@@ -0,0 +1,56 @@
package ltd.hlaeja.util
import java.util.UUID
import ltd.hlaeja.library.deviceConfiguration.Node
import ltd.hlaeja.library.deviceData.MeasurementData
import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.DeviceConfigurationProperty
import ltd.hlaeja.property.DeviceDataProperty
import ltd.hlaeja.property.DeviceRegistryProperty
import org.springframework.http.HttpStatus.NOT_ACCEPTABLE
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.http.HttpStatus.NO_CONTENT
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT
import org.springframework.http.ResponseEntity
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBodilessEntity
import org.springframework.web.reactive.function.client.awaitBodyOrNull
import org.springframework.web.server.ResponseStatusException
suspend fun WebClient.deviceRegistryIdentityDevice(
device: UUID,
property: DeviceRegistryProperty,
): Identity.Response = get()
.uri("${property.url}/identity/device-$device".also(::logCall))
.retrieve()
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_ACCEPTABLE) }
.awaitBodyOrNull<Identity.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT)
suspend fun WebClient.deviceDataGetMeasurement(
client: UUID,
node: UUID,
property: DeviceDataProperty,
): MeasurementData.Response = get()
.uri("${property.url}/client-$client/node-$node".also(::logCall))
.retrieve()
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) }
.awaitBodyOrNull<MeasurementData.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT)
suspend fun WebClient.deviceDataAddMeasurement(
client: UUID,
request: MeasurementData.Request,
property: DeviceDataProperty,
): ResponseEntity<Void> = post()
.uri("${property.url}/client-$client".also(::logCall))
.bodyValue(request)
.retrieve()
.awaitBodilessEntity()
suspend fun WebClient.deviceConfigurationGetConfiguration(
node: UUID,
property: DeviceConfigurationProperty,
): Node.Response = get()
.uri("${property.url}/node-$node".also(::logCall))
.retrieve()
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) }
.awaitBodyOrNull<Node.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT)

View File

@@ -39,6 +39,11 @@
"name": "device-configuration.url", "name": "device-configuration.url",
"type": "java.lang.String", "type": "java.lang.String",
"description": "Url for device configuration service." "description": "Url for device configuration service."
},
{
"name": "cache.time-to-live",
"type": "java.lang.Long",
"description": "Cache expiration in minutes."
} }
] ]
} }

View File

@@ -9,6 +9,34 @@ spring:
os: os:
name: "%APP_BUILD_OS_NAME%" name: "%APP_BUILD_OS_NAME%"
version: "%APP_BUILD_OS_VERSION%" version: "%APP_BUILD_OS_VERSION%"
cache:
type: redis
data:
redis:
port: 6379
database: 1
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: "health,info"
endpoint:
health:
enabled: true
show-details: always
info:
enabled: true
influx:
metrics:
export:
api-version: v2
bucket: hlaeja
org: hlaeja_ltd
cache:
time-to-live: 10
jwt: jwt:
public-key: cert/public_key.pem public-key: cert/public_key.pem
@@ -21,6 +49,19 @@ spring:
config: config:
activate: activate:
on-profile: development on-profile: development
data:
redis:
host: localhost
management:
metrics:
tags:
application: device-api
influx:
metrics:
export:
enabled: false
token: %INFLUXDB_TOKEN%
server: server:
port: 8443 port: 8443
@@ -47,6 +88,18 @@ spring:
config: config:
activate: activate:
on-profile: docker on-profile: docker
data:
redis:
host: Redis
management:
metrics:
tags:
application: device-api
influx:
metrics:
export:
uri: http://InfluxDB:8086
server: server:
port: 8443 port: 8443

View File

@@ -0,0 +1,10 @@
jwt:
public-key: cert/valid-public-key.pem
device-registry:
url: http://localhost:9010
device-data:
url: http://localhost:9020
device-configuration:
url: http://localhost:9030
cache:
time-to-live: 10