Skip to content

Commit 3ee6344

Browse files
authored
make HTTP monitors explicit (UI, API, backend, DB) (#227)
* introduce v2 api and the necessary breaking changes * rename DB tables to be HTTP specific * rename models, services and variables to be more explicit * UI revamp to be more explicit about http monitors * improve test coverage * put back SMTP config to SettingsDto * improve HeaderApiKeyHandling.kt * improve translations
1 parent ec7e080 commit 3ee6344

File tree

215 files changed

+7626
-3927
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

215 files changed

+7626
-3927
lines changed

app/src/main/kotlin/com/kuvaszuptime/kuvasz/Application.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import io.swagger.v3.oas.annotations.tags.Tag
1717
),
1818
tags = [
1919
Tag(name = "Management"),
20+
Tag(name = "Monitors"),
21+
Tag(name = "HTTP monitors (V1, deprecated)"),
2022
Tag(name = "HTTP monitors"),
23+
Tag(name = "Settings (V1, deprecated)"),
2124
Tag(name = "Settings"),
2225
]
2326
)

app/src/main/kotlin/com/kuvaszuptime/kuvasz/config/AppConfig.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ class AppConfig {
2929

3030
var logEventHandler: Boolean = false
3131

32-
private var isExternalWriteDisabled = false
32+
private var isHttpMonitorExternalWriteDisabled = false
3333

3434
var uptimeCheckLockTimeoutMs: Long = UPTIME_CHECK_LOCK_TIMEOUT_MS
3535

36-
fun disableExternalWrite() {
37-
isExternalWriteDisabled = true
36+
fun disableHttpMonitorExternalWrite() {
37+
isHttpMonitorExternalWriteDisabled = true
3838
}
3939

40-
fun isExternalWriteDisabled() = isExternalWriteDisabled
40+
fun isHttpMonitorExternalWriteDisabled() = isHttpMonitorExternalWriteDisabled
4141
}

app/src/main/kotlin/com/kuvaszuptime/kuvasz/config/MonitorConfig.kt renamed to app/src/main/kotlin/com/kuvaszuptime/kuvasz/config/HttpMonitorConfig.kt

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,56 @@
11
package com.kuvaszuptime.kuvasz.config
22

33
import com.kuvaszuptime.kuvasz.jooq.enums.HttpMethod
4-
import com.kuvaszuptime.kuvasz.models.MonitorCreatorLike
5-
import com.kuvaszuptime.kuvasz.models.dto.MonitorDefaults
4+
import com.kuvaszuptime.kuvasz.models.HttpMonitorCreatorLike
5+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDefaults
66
import io.micronaut.context.annotation.EachProperty
77
import io.micronaut.core.annotation.Introspected
88
import io.micronaut.core.bind.annotation.Bindable
99

10-
@EachProperty(MonitorConfig.CONFIG_PREFIX, list = true)
10+
@EachProperty(HttpMonitorConfig.CONFIG_PREFIX, list = true)
1111
@Introspected
1212
@Suppress("ComplexInterface")
13-
interface MonitorConfig : MonitorCreatorLike {
13+
interface HttpMonitorConfig : HttpMonitorCreatorLike {
1414

1515
companion object {
16-
const val CONFIG_PREFIX = "monitors"
16+
const val LEGACY_CONFIG_PREFIX = "monitors"
17+
const val CONFIG_PREFIX = "http-monitors"
1718
}
1819

1920
override val name: String
2021
override val url: String
2122
override val uptimeCheckInterval: Int
2223

23-
@get:Bindable(defaultValue = MonitorDefaults.MONITOR_ENABLED.toString())
24+
@get:Bindable(defaultValue = HttpMonitorDefaults.MONITOR_ENABLED.toString())
2425
override val enabled: Boolean
2526

26-
@get:Bindable(defaultValue = MonitorDefaults.SSL_CHECK_ENABLED.toString())
27+
@get:Bindable(defaultValue = HttpMonitorDefaults.SSL_CHECK_ENABLED.toString())
2728
override val sslCheckEnabled: Boolean
2829

29-
@get:Bindable(defaultValue = MonitorDefaults.REQUEST_METHOD)
30+
@get:Bindable(defaultValue = HttpMonitorDefaults.REQUEST_METHOD)
3031
override val requestMethod: HttpMethod
3132

32-
@get:Bindable(defaultValue = MonitorDefaults.LATENCY_HISTORY_ENABLED.toString())
33+
@get:Bindable(defaultValue = HttpMonitorDefaults.LATENCY_HISTORY_ENABLED.toString())
3334
override val latencyHistoryEnabled: Boolean
3435

35-
@get:Bindable(defaultValue = MonitorDefaults.FORCE_NO_CACHE.toString())
36+
@get:Bindable(defaultValue = HttpMonitorDefaults.FORCE_NO_CACHE.toString())
3637
override val forceNoCache: Boolean
3738

38-
@get:Bindable(defaultValue = MonitorDefaults.FOLLOW_REDIRECTS.toString())
39+
@get:Bindable(defaultValue = HttpMonitorDefaults.FOLLOW_REDIRECTS.toString())
3940
override val followRedirects: Boolean
4041

41-
@get:Bindable(defaultValue = MonitorDefaults.SSL_EXPIRY_THRESHOLD_DAYS.toString())
42+
@get:Bindable(defaultValue = HttpMonitorDefaults.SSL_EXPIRY_THRESHOLD_DAYS.toString())
4243
override val sslExpiryThreshold: Int
4344

4445
override val integrations: List<String>?
4546
override val expectedStatusCodes: List<Int>?
4647
override val responseTimeThresholdMillis: Int?
4748
override val expectedKeyword: String?
4849

49-
@get:Bindable(defaultValue = MonitorDefaults.EXPECTED_KEYWORD_CASE_SENSITIVE.toString())
50+
@get:Bindable(defaultValue = HttpMonitorDefaults.EXPECTED_KEYWORD_CASE_SENSITIVE.toString())
5051
override val expectedKeywordCaseSensitive: Boolean
5152

52-
@get:Bindable(defaultValue = MonitorDefaults.EXPECTED_KEYWORD_NEGATED.toString())
53+
@get:Bindable(defaultValue = HttpMonitorDefaults.EXPECTED_KEYWORD_NEGATED.toString())
5354
override val expectedKeywordNegated: Boolean
5455

5556
override val requestHeaders: Map<String, String>?

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/ReadOnlyIfYaml.kt renamed to app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/CheckHttpMonitorsWritable.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import jakarta.inject.Singleton
1212
@Retention(AnnotationRetention.RUNTIME)
1313
@Target(AnnotationTarget.FUNCTION)
1414
@Around
15-
annotation class ReadOnlyIfYaml
15+
annotation class CheckHttpMonitorsWritable
1616

1717
@Singleton
18-
@InterceptorBean(ReadOnlyIfYaml::class)
19-
class ReadOnlyIfYamlInterceptor(private val appConfig: AppConfig) : MethodInterceptor<Any?, Any?>, Ordered {
18+
@InterceptorBean(CheckHttpMonitorsWritable::class)
19+
class HttpMonitorWriteInterceptor(private val appConfig: AppConfig) : MethodInterceptor<Any?, Any?>, Ordered {
2020

2121
override fun intercept(context: MethodInvocationContext<Any?, Any?>): Any? {
22-
context.findAnnotation(ReadOnlyIfYaml::class.java).ifPresent { _ ->
23-
if (appConfig.isExternalWriteDisabled()) throw ReadOnlyMonitorException()
22+
context.findAnnotation(CheckHttpMonitorsWritable::class.java).ifPresent { _ ->
23+
if (appConfig.isHttpMonitorExternalWriteDisabled()) throw ReadOnlyMonitorException()
2424
}
2525
return context.proceed()
2626
}

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/GlobalErrorHandler.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@
33
package com.kuvaszuptime.kuvasz.controllers
44

55
import com.fasterxml.jackson.core.JsonParseException
6+
import com.kuvaszuptime.kuvasz.controllers.ui.WebUIController.Companion.DASHBOARD_PATH
7+
import com.kuvaszuptime.kuvasz.controllers.ui.WebUIController.Companion.LOGIN_PATH
68
import com.kuvaszuptime.kuvasz.models.DuplicationException
79
import com.kuvaszuptime.kuvasz.models.MonitorNotFoundException
810
import com.kuvaszuptime.kuvasz.models.PersistenceException
911
import com.kuvaszuptime.kuvasz.models.SchedulingException
1012
import com.kuvaszuptime.kuvasz.models.ServiceError
1113
import com.kuvaszuptime.kuvasz.models.handlers.InvalidIntegrationIDException
14+
import com.kuvaszuptime.kuvasz.security.ui.AlreadyLoggedInError
15+
import com.kuvaszuptime.kuvasz.security.ui.WebAuthError
16+
import com.kuvaszuptime.kuvasz.util.toUri
1217
import com.kuvaszuptime.kuvasz.validation.NonExistingIntegrationIdException
1318
import io.micronaut.core.convert.exceptions.ConversionErrorException
1419
import io.micronaut.http.HttpRequest
1520
import io.micronaut.http.HttpResponse
1621
import io.micronaut.http.HttpStatus
1722
import io.micronaut.http.annotation.Controller
1823
import io.micronaut.http.annotation.Error
24+
import io.micronaut.views.htmx.http.HtmxRequestHeaders.HX_REQUEST
25+
import io.micronaut.views.htmx.http.HtmxResponseHeaders
1926
import jakarta.validation.ValidationException
2027

2128
@Controller
@@ -87,4 +94,22 @@ class GlobalErrorHandler {
8794
val error = ServiceError(ex.message)
8895
return HttpResponse.status<ServiceError>(HttpStatus.BAD_REQUEST).body(error)
8996
}
97+
98+
/**
99+
* Handles authentication errors by redirecting to the login page
100+
*/
101+
@Error(global = true)
102+
@Suppress("UnusedParameter")
103+
fun authError(request: HttpRequest<*>, authError: WebAuthError): HttpResponse<*> =
104+
if (request.headers.contains(HX_REQUEST)) {
105+
// HTMX handles redirects differently, need to return a 2xx response with the right header
106+
HttpResponse.noContent<Any>().header(HtmxResponseHeaders.HX_REDIRECT, LOGIN_PATH)
107+
} else {
108+
HttpResponse.seeOther<Any>(LOGIN_PATH.toUri())
109+
}
110+
111+
@Error(global = true)
112+
@Suppress("UnusedParameter")
113+
fun alreadyLoggedInError(request: HttpRequest<*>, authError: AlreadyLoggedInError): HttpResponse<*> =
114+
HttpResponse.seeOther<Any>(DASHBOARD_PATH.toUri())
90115
}

app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt renamed to app/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/HttpMonitorControllerV1.kt

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies
44
import com.fasterxml.jackson.databind.node.ObjectNode
55
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
66
import com.fasterxml.jackson.module.kotlin.kotlinModule
7-
import com.kuvaszuptime.kuvasz.config.MonitorConfig
7+
import com.kuvaszuptime.kuvasz.config.HttpMonitorConfig
88
import com.kuvaszuptime.kuvasz.jooq.enums.SslStatus
99
import com.kuvaszuptime.kuvasz.jooq.enums.UptimeStatus
1010
import com.kuvaszuptime.kuvasz.models.ServiceError
11-
import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto
12-
import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto
13-
import com.kuvaszuptime.kuvasz.models.dto.MonitorDto
14-
import com.kuvaszuptime.kuvasz.models.dto.MonitorExportDto
15-
import com.kuvaszuptime.kuvasz.models.dto.MonitorStatsDto
16-
import com.kuvaszuptime.kuvasz.models.dto.MonitoringStatsDto
11+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorCreateDto
12+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDetailsDto
13+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorDto
14+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorExportDto
15+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitorStatsDto
16+
import com.kuvaszuptime.kuvasz.models.dto.HttpMonitoringStatsDto
17+
import com.kuvaszuptime.kuvasz.models.dto.HttpUptimeEventDto
1718
import com.kuvaszuptime.kuvasz.models.dto.SSLEventDto
18-
import com.kuvaszuptime.kuvasz.models.dto.UptimeEventDto
19-
import com.kuvaszuptime.kuvasz.services.MonitorCrudService
2019
import com.kuvaszuptime.kuvasz.services.StatCalculator
20+
import com.kuvaszuptime.kuvasz.services.check.http.HttpMonitorCrudService
2121
import io.micronaut.http.HttpStatus
2222
import io.micronaut.http.MediaType
2323
import io.micronaut.http.annotation.Controller
@@ -41,19 +41,18 @@ import java.io.File
4141
import java.time.Duration
4242
import java.time.Instant
4343

44-
const val API_V1_PREFIX = "/api/v1"
45-
4644
@Controller("$API_V1_PREFIX/monitors", produces = [MediaType.APPLICATION_JSON])
4745
@Validated
48-
@Tag(name = "HTTP monitors")
46+
@Tag(name = "HTTP monitors (V1, deprecated)")
47+
@Deprecated("Use HttpMonitorControllerV2")
4948
@SecurityRequirements(
5049
SecurityRequirement(name = "apiKey"),
5150
SecurityRequirement(name = "bearerAuth")
5251
)
53-
class MonitorController(
54-
private val monitorCrudService: MonitorCrudService,
52+
class HttpMonitorControllerV1(
53+
private val monitorCrudService: HttpMonitorCrudService,
5554
private val statCalculator: StatCalculator,
56-
) : MonitorOperations {
55+
) : HttpMonitorOperationsV1 {
5756

5857
private val yamlMapper = YAMLMapper()
5958
.registerModules(kotlinModule())
@@ -63,7 +62,7 @@ class MonitorController(
6362
ApiResponse(
6463
responseCode = "200",
6564
description = "Successful query",
66-
content = [Content(array = ArraySchema(schema = Schema(implementation = MonitorDetailsDto::class)))]
65+
content = [Content(array = ArraySchema(schema = Schema(implementation = HttpMonitorDetailsDto::class)))]
6766
)
6867
)
6968
@ExecuteOn(TaskExecutors.IO)
@@ -72,7 +71,7 @@ class MonitorController(
7271
@QueryValue uptimeStatus: List<UptimeStatus>?,
7372
@QueryValue sslStatus: List<SslStatus>?,
7473
@QueryValue sslCheckEnabled: Boolean?,
75-
): List<MonitorDetailsDto> =
74+
): List<HttpMonitorDetailsDto> =
7675
monitorCrudService.getMonitorsWithDetails(
7776
enabled = enabled,
7877
uptimeStatus = uptimeStatus.orEmpty(),
@@ -84,7 +83,7 @@ class MonitorController(
8483
ApiResponse(
8584
responseCode = "200",
8685
description = "Successful query",
87-
content = [Content(schema = Schema(implementation = MonitorDetailsDto::class))]
86+
content = [Content(schema = Schema(implementation = HttpMonitorDetailsDto::class))]
8887
),
8988
ApiResponse(
9089
responseCode = "404",
@@ -93,15 +92,15 @@ class MonitorController(
9392
)
9493
)
9594
@ExecuteOn(TaskExecutors.IO)
96-
override fun getMonitorDetails(monitorId: Long): MonitorDetailsDto =
95+
override fun getMonitorDetails(monitorId: Long): HttpMonitorDetailsDto =
9796
monitorCrudService.getMonitorDetails(monitorId)
9897

9998
@Status(HttpStatus.CREATED)
10099
@ApiResponses(
101100
ApiResponse(
102101
responseCode = "201",
103102
description = "Successful creation",
104-
content = [Content(schema = Schema(implementation = MonitorDto::class))]
103+
content = [Content(schema = Schema(implementation = HttpMonitorDto::class))]
105104
),
106105
ApiResponse(
107106
responseCode = "400",
@@ -115,10 +114,10 @@ class MonitorController(
115114
)
116115
)
117116
@ExecuteOn(TaskExecutors.IO)
118-
@ReadOnlyIfYaml
119-
override fun createMonitor(@Valid monitor: MonitorCreateDto): MonitorDto {
117+
@CheckHttpMonitorsWritable
118+
override fun createMonitor(@Valid monitor: HttpMonitorCreateDto): HttpMonitorDto {
120119
val createdMonitor = monitorCrudService.createMonitor(monitor)
121-
return MonitorDto.fromMonitorRecord(createdMonitor)
120+
return HttpMonitorDto.fromMonitorRecord(createdMonitor)
122121
}
123122

124123
@Status(HttpStatus.NO_CONTENT)
@@ -139,14 +138,14 @@ class MonitorController(
139138
)
140139
)
141140
@ExecuteOn(TaskExecutors.IO)
142-
@ReadOnlyIfYaml
141+
@CheckHttpMonitorsWritable
143142
override fun deleteMonitor(monitorId: Long) = monitorCrudService.deleteMonitorById(monitorId)
144143

145144
@ApiResponses(
146145
ApiResponse(
147146
responseCode = "200",
148147
description = "Successful update",
149-
content = [Content(schema = Schema(implementation = MonitorDto::class))]
148+
content = [Content(schema = Schema(implementation = HttpMonitorDto::class))]
150149
),
151150
ApiResponse(
152151
responseCode = "400",
@@ -165,21 +164,21 @@ class MonitorController(
165164
)
166165
)
167166
@ExecuteOn(TaskExecutors.IO)
168-
@ReadOnlyIfYaml
169-
override fun updateMonitor(monitorId: Long, updates: ObjectNode): MonitorDto {
167+
@CheckHttpMonitorsWritable
168+
override fun updateMonitor(monitorId: Long, updates: ObjectNode): HttpMonitorDto {
170169
val updatedMonitor = monitorCrudService.updateMonitor(monitorId, updates)
171-
return MonitorDto.fromMonitorRecord(updatedMonitor)
170+
return HttpMonitorDto.fromMonitorRecord(updatedMonitor)
172171
}
173172

174173
@ApiResponses(
175174
ApiResponse(
176175
responseCode = "200",
177176
description = "Successful query",
178-
content = [Content(array = ArraySchema(schema = Schema(implementation = UptimeEventDto::class)))]
177+
content = [Content(array = ArraySchema(schema = Schema(implementation = HttpUptimeEventDto::class)))]
179178
)
180179
)
181180
@ExecuteOn(TaskExecutors.IO)
182-
override fun getUptimeEvents(monitorId: Long): List<UptimeEventDto> =
181+
override fun getUptimeEvents(monitorId: Long): List<HttpUptimeEventDto> =
183182
monitorCrudService.getUptimeEventsByMonitorId(monitorId)
184183

185184
@ApiResponses(
@@ -197,7 +196,7 @@ class MonitorController(
197196
ApiResponse(
198197
responseCode = "200",
199198
description = "Successful query",
200-
content = [Content(schema = Schema(implementation = MonitorStatsDto::class))]
199+
content = [Content(schema = Schema(implementation = HttpMonitorStatsDto::class))]
201200
),
202201
ApiResponse(
203202
responseCode = "404",
@@ -209,7 +208,7 @@ class MonitorController(
209208
override fun getMonitorStats(
210209
monitorId: Long,
211210
@QueryValue period: Duration?,
212-
): MonitorStatsDto {
211+
): HttpMonitorStatsDto {
213212
val effectivePeriod = period ?: Duration.ofDays(MONITOR_STATS_PERIOD_DEFAULT_DAYS)
214213
return monitorCrudService.getMonitorStats(
215214
monitorId = monitorId,
@@ -229,8 +228,8 @@ class MonitorController(
229228
override fun getYamlMonitorsExport(): SystemFile {
230229
val file = File.createTempFile("temp", EXPORT_FILE_NAME_PREFIX)
231230
val export = mapOf(
232-
MonitorConfig.CONFIG_PREFIX to monitorCrudService.getMonitorsExport()
233-
.map { MonitorExportDto.fromMonitorRecord(it) }
231+
HttpMonitorConfig.LEGACY_CONFIG_PREFIX to monitorCrudService.getHttpMonitorsExport()
232+
.map { HttpMonitorExportDto.fromMonitorRecord(it) }
234233
)
235234
yamlMapper.writeValue(file, export)
236235
val finalFileName = EXPORT_FILE_NAME_PREFIX + Instant.now().epochSecond + EXPORT_FILE_EXTENSION
@@ -242,12 +241,12 @@ class MonitorController(
242241
ApiResponse(
243242
responseCode = "200",
244243
description = "Successful query",
245-
content = [Content(schema = Schema(implementation = MonitoringStatsDto::class))]
244+
content = [Content(schema = Schema(implementation = HttpMonitoringStatsDto::class))]
246245
)
247246
)
248247
@ExecuteOn(TaskExecutors.IO)
249-
override fun getMonitoringStats(period: Duration?): MonitoringStatsDto {
250-
return statCalculator.calculateOverallStats(period ?: Duration.ofDays(MONITORING_STATS_PERIOD_DEFAULT_DAYS))
248+
override fun getMonitoringStats(period: Duration?): HttpMonitoringStatsDto {
249+
return statCalculator.calculateOverallHttpStats(period ?: Duration.ofDays(MONITORING_STATS_PERIOD_DEFAULT_DAYS))
251250
}
252251

253252
companion object {

0 commit comments

Comments
 (0)