From 79c0734bd2d3e14ca497a9e26c82907da95353b5 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Fri, 5 Sep 2025 15:24:51 +0200 Subject: [PATCH] feat(fullstack): Add server health check with overlay --- legalconsenthub-backend/build.gradle | 1 + .../legalconsenthub/config/SecurityConfig.kt | 27 ++++-- legalconsenthub/app.vue | 2 + .../components/ServerConnectionOverlay.vue | 55 +++++++++++ .../composables/useServerHealth.ts | 92 +++++++++++++++++++ legalconsenthub/i18n/locales/de.json | 8 ++ legalconsenthub/i18n/locales/en.json | 8 ++ legalconsenthub/package.json | 2 +- legalconsenthub/pages/signup.vue | 1 - .../plugins/server-health.client.ts | 13 +++ 10 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 legalconsenthub/components/ServerConnectionOverlay.vue create mode 100644 legalconsenthub/composables/useServerHealth.ts create mode 100644 legalconsenthub/plugins/server-health.client.ts diff --git a/legalconsenthub-backend/build.gradle b/legalconsenthub-backend/build.gradle index 8e153d9..835ecb0 100644 --- a/legalconsenthub-backend/build.gradle +++ b/legalconsenthub-backend/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation "com.openhtmltopdf:openhtmltopdf-core:$openHtmlVersion" implementation "com.openhtmltopdf:openhtmltopdf-pdfbox:$openHtmlVersion" implementation "com.openhtmltopdf:openhtmltopdf-java2d:$openHtmlVersion" diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt index 9cf3f50..7913d03 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt @@ -2,6 +2,7 @@ package com.betriebsratkanzlei.legalconsenthub.config import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.invoke @@ -16,24 +17,38 @@ import org.springframework.http.HttpMethod class SecurityConfig { @Bean - fun securityFilterChain( + @Order(1) + fun publicApiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher("/swagger-ui/**", "/v3/**", "/actuator/**", "/users") + csrf { disable() } + authorizeHttpRequests { + authorize("/swagger-ui/**", permitAll) + authorize("/v3/**", permitAll) + authorize("/actuator/**", permitAll) + // For user registration + authorize(HttpMethod.POST, "/users", permitAll) + authorize(anyRequest, denyAll) + } + } + return http.build() + } + + @Bean + @Order(2) + fun protectedApiSecurityFilterChain( http: HttpSecurity, customJwtAuthenticationConverter: CustomJwtAuthenticationConverter ): SecurityFilterChain { http { csrf { disable() } authorizeHttpRequests { - authorize("/swagger-ui/**", permitAll) - authorize("/v3/**", permitAll) - // For user registration - authorize(HttpMethod.POST, "/users", permitAll) authorize(anyRequest, authenticated) } oauth2ResourceServer { jwt { jwtAuthenticationConverter = customJwtAuthenticationConverter } } } - return http.build() } diff --git a/legalconsenthub/app.vue b/legalconsenthub/app.vue index 88acd7f..57b60a6 100644 --- a/legalconsenthub/app.vue +++ b/legalconsenthub/app.vue @@ -5,6 +5,8 @@ + + diff --git a/legalconsenthub/components/ServerConnectionOverlay.vue b/legalconsenthub/components/ServerConnectionOverlay.vue new file mode 100644 index 0000000..33c253a --- /dev/null +++ b/legalconsenthub/components/ServerConnectionOverlay.vue @@ -0,0 +1,55 @@ + + + diff --git a/legalconsenthub/composables/useServerHealth.ts b/legalconsenthub/composables/useServerHealth.ts new file mode 100644 index 0000000..191a650 --- /dev/null +++ b/legalconsenthub/composables/useServerHealth.ts @@ -0,0 +1,92 @@ +export const isServerAvailable = ref(true) +export const isChecking = ref(false) +export const lastCheckTime = ref(null) + +export function useServerHealth() { + const checkInterval = ref(null) + const healthCheckUrl = 'http://localhost:3001/api/actuator/health' + + async function checkServerHealth(): Promise { + if (isChecking.value) return isServerAvailable.value + + isChecking.value = true + lastCheckTime.value = new Date() + + try { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 5000) + + const response = await fetch(healthCheckUrl, { + method: 'GET', + signal: controller.signal, + headers: { + 'Content-Type': 'application/json' + } + }) + + clearTimeout(timeoutId) + + const wasAvailable = isServerAvailable.value + isServerAvailable.value = response.ok + + if (!wasAvailable && isServerAvailable.value) { + console.info('Server is back online') + } + + if (wasAvailable && !isServerAvailable.value) { + console.warn('Server is no longer available') + } + + return isServerAvailable.value + } catch (error) { + const wasAvailable = isServerAvailable.value + isServerAvailable.value = false + + if (wasAvailable) { + console.warn('Server health check failed:', error) + } + + return false + } finally { + isChecking.value = false + } + } + + async function startPeriodicHealthCheck(intervalMs: number = 60000) { + if (checkInterval.value) { + clearInterval(checkInterval.value) + } + + checkServerHealth() + + checkInterval.value = setInterval(() => { + checkServerHealth() + }, intervalMs) + + onUnmounted(() => { + if (checkInterval.value) { + clearInterval(checkInterval.value) + checkInterval.value = null + } + }) + + return checkInterval.value + } + + const stopHealthCheck = () => { + if (checkInterval.value) { + clearInterval(checkInterval.value) + checkInterval.value = null + } + } + + return { + isServerAvailable, + isChecking, + lastCheckTime, + healthCheckUrl, + checkServerHealth, + startPeriodicHealthCheck, + stopHealthCheck + } +} diff --git a/legalconsenthub/i18n/locales/de.json b/legalconsenthub/i18n/locales/de.json index 5a463de..83e5777 100644 --- a/legalconsenthub/i18n/locales/de.json +++ b/legalconsenthub/i18n/locales/de.json @@ -5,5 +5,13 @@ "employer": "Arbeitgeber", "worksCouncilMember": "Betriebsratsmitglied", "worksCouncilChair": "Betriebsratsvorsitzender" + }, + "serverConnection": { + "title": "Verbindung zum Server unterbrochen", + "message": "Die Verbindung zum Server ist momentan nicht verfügbar. Wir versuchen automatisch, die Verbindung wiederherzustellen.", + "checking": "Verbindung wird geprüft...", + "lastCheck": "Letzte Überprüfung", + "retryInfo": "Automatischer Wiederholungsversuch alle 60 Sekunden", + "retryNow": "Jetzt erneut versuchen" } } diff --git a/legalconsenthub/i18n/locales/en.json b/legalconsenthub/i18n/locales/en.json index 7bc7edc..66e18e7 100644 --- a/legalconsenthub/i18n/locales/en.json +++ b/legalconsenthub/i18n/locales/en.json @@ -5,5 +5,13 @@ "employer": "Employer", "worksCouncilMember": "Works Council Member", "worksCouncilChair": "Works Council Chair" + }, + "serverConnection": { + "title": "Server Connection Lost", + "message": "The connection to the server is currently unavailable. We are automatically trying to restore the connection.", + "checking": "Checking connection...", + "lastCheck": "Last check", + "retryInfo": "Automatic retry every 60 seconds", + "retryNow": "Try again now" } } diff --git a/legalconsenthub/package.json b/legalconsenthub/package.json index 8db7c01..57ad38a 100644 --- a/legalconsenthub/package.json +++ b/legalconsenthub/package.json @@ -17,7 +17,7 @@ "fix:bettersqlite": "cd node_modules/better-sqlite3 && pnpm dlx node-gyp rebuild && cd ../..", "generate:betterauth": "pnpm dlx @better-auth/cli generate --config server/utils/auth.ts --yes", "migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts --yes", - "recreate-db:betterauth": "rm sqlite.db && pnpm run migrate:betterauth && pnpm run migrate:betterauth" + "recreate-db:betterauth": "[ -f sqlite.db ] && rm sqlite.db; pnpm run migrate:betterauth" }, "dependencies": { "@nuxt/ui-pro": "3.1.1", diff --git a/legalconsenthub/pages/signup.vue b/legalconsenthub/pages/signup.vue index 4898f46..a768ba1 100644 --- a/legalconsenthub/pages/signup.vue +++ b/legalconsenthub/pages/signup.vue @@ -118,7 +118,6 @@ function onSubmit(payload: FormSubmitEvent) { }, onError: async (ctx) => { console.log(ctx.error.message) - await deleteUser({ callbackURL: '/signup' }) useToast().add({ title: 'Fehler bei der Registrierung', description: ctx.error.message, diff --git a/legalconsenthub/plugins/server-health.client.ts b/legalconsenthub/plugins/server-health.client.ts new file mode 100644 index 0000000..c27cb14 --- /dev/null +++ b/legalconsenthub/plugins/server-health.client.ts @@ -0,0 +1,13 @@ +export default defineNuxtPlugin(() => { + // This plugin runs only on the client side to avoid issues with server-side rendering + if (import.meta.client) { + // Initialize server health monitoring as soon as the client is ready + const { startPeriodicHealthCheck } = useServerHealth() + + // Start the health check with a 1-minute interval + // This ensures the health check starts even if app.vue's onMounted hasn't fired yet + nextTick(() => { + startPeriodicHealthCheck(60000) + }) + } +})