From 7f7852a66a7fc80a94fd82f707513d79272bc566 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Wed, 24 Dec 2025 10:26:22 +0100 Subject: [PATCH] feat(#27): Set up consola logger, make use of log levels in backend and frontend --- CLAUDE.md | 4 + deployment/docker-compose-dev.yaml | 102 +++++++++--------- deployment/docker-compose-prod.yaml | 2 + .../ApplicationFormService.kt | 1 - .../security/JwtUserSyncFilter.kt | 1 - .../resources/application-testcontainers.yaml | 12 +-- .../src/main/resources/application.yaml | 11 ++ legalconsenthub/app/components/FormEngine.vue | 3 +- .../applicationForm/useApplicationForm.ts | 18 ++-- .../useApplicationFormTemplate.ts | 22 ++-- .../useApplicationFormVersion.ts | 8 +- .../composables/comment/useCommentTextarea.ts | 15 +-- .../notification/useNotification.ts | 22 ++-- .../useApplicationFormValidator.ts | 5 +- legalconsenthub/app/composables/useLogger.ts | 5 + .../app/composables/useServerHealth.ts | 9 +- legalconsenthub/app/layouts/default.vue | 7 +- .../app/middleware/refreshToken.global.ts | 16 +-- legalconsenthub/app/pages/administration.vue | 3 +- legalconsenthub/app/pages/callback.vue | 3 +- legalconsenthub/app/pages/create.vue | 5 +- legalconsenthub/app/pages/settings.vue | 3 +- legalconsenthub/app/plugins/error-handler.ts | 13 ++- legalconsenthub/app/plugins/logger.ts | 16 +++ legalconsenthub/app/utils/wrappedFetch.ts | 4 +- legalconsenthub/i18n/locales/de.json | 6 +- legalconsenthub/i18n/locales/en.json | 6 +- legalconsenthub/index.d.ts | 14 +++ legalconsenthub/nuxt.config.ts | 3 +- legalconsenthub/package.json | 3 +- legalconsenthub/pnpm-lock.yaml | 5 +- legalconsenthub/server/api/[...].ts | 11 +- .../server/routes/auth/keycloak.get.ts | 18 +++- .../server/routes/auth/logout.get.ts | 13 ++- legalconsenthub/shared/utils/logger.ts | 48 +++++++++ legalconsenthub/stores/useCommentStore.ts | 16 +-- 36 files changed, 312 insertions(+), 141 deletions(-) create mode 100644 legalconsenthub/app/composables/useLogger.ts create mode 100644 legalconsenthub/app/plugins/logger.ts create mode 100644 legalconsenthub/shared/utils/logger.ts diff --git a/CLAUDE.md b/CLAUDE.md index 506fc67..898ea81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -639,6 +639,7 @@ Set in `nuxt.config.ts` → `runtimeConfig.public`: - `serverApiBaseUrl` - Direct backend URL (server-side) - `serverApiBasePath` - Backend API base path - `keycloakTokenUrl` - Keycloak token endpoint +- `logLevel` - Frontend log level (Consola). Controlled via `NUXT_PUBLIC_LOG_LEVEL` (defaults: `debug` in dev, `warn` in prod) OAuth configuration (`runtimeConfig.oauth.keycloak`): - `clientId`, `clientSecret`, `realm`, `serverUrl`, `redirectURL` @@ -652,6 +653,9 @@ Main config in `src/main/resources/application.yaml`: - Database connection (PostgreSQL) - Keycloak JWT validation - Server port (default: 8080) +- Logging levels via env vars: + - `LOGGING_LEVEL_ROOT` + - `LOGGING_LEVEL_APP` for `com.betriebsratkanzlei.legalconsenthub` --- diff --git a/deployment/docker-compose-dev.yaml b/deployment/docker-compose-dev.yaml index 4830b4e..3fe36a5 100644 --- a/deployment/docker-compose-dev.yaml +++ b/deployment/docker-compose-dev.yaml @@ -74,53 +74,55 @@ services: - legalconsenthub-net ############################################### - - backend: - image: gitea.lugnas.de/denis/legalconsenthub-backend:latest - container_name: legalconsenthub-backend-local - restart: on-failure:2 - environment: - LEGALCONSENTHUB_DB_URL: jdbc:postgresql://legalconsenthub-db:5432/${LEGALCONSENTHUB_POSTGRES_DB} - LEGALCONSENTHUB_DB_APP_USER: ${LEGALCONSENTHUB_POSTGRES_USER} - LEGALCONSENTHUB_DB_PASSWORD: ${LEGALCONSENTHUB_POSTGRES_PASSWORD} - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: ${KEYCLOAK_ISSUER_URL}/realms/${KEYCLOAK_REALM} - SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8080/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs - SERVER_PORT: 8080 - LEGALCONSENTHUB_PDF_STORAGE_FILESYSTEM_BASE_DIR: /var/lib/legalconsenthub/pdfs - ports: - - "8081:8080" - volumes: - - legalconsenthub_pdf_cache:/var/lib/legalconsenthub/pdfs - depends_on: - legalconsenthub-db: - condition: service_started - networks: - - legalconsenthub-net - env_file: - - .env.dev - - frontend: - image: gitea.lugnas.de/denis/legalconsenthub:latest - container_name: legalconsenthub-frontend-local - ports: - - "3211:3000" - networks: - - legalconsenthub-net - env_file: - - .env.dev - - legalconsenthub-db: - image: postgres:latest - container_name: legalconsenthub-postgres-local - environment: - POSTGRES_USER: ${LEGALCONSENTHUB_POSTGRES_USER} - POSTGRES_PASSWORD: ${LEGALCONSENTHUB_POSTGRES_PASSWORD} - POSTGRES_DB: ${LEGALCONSENTHUB_POSTGRES_DB} - ports: - - "5446:5432" - networks: - - legalconsenthub-net - volumes: - - legalconsenthub_postgres_data:/var/lib/postgresql - env_file: - - .env.dev +# +# backend: +# image: gitea.lugnas.de/denis/legalconsenthub-backend:latest +# container_name: legalconsenthub-backend-local +# restart: on-failure:2 +# environment: +# LOGGING_LEVEL_ROOT: ${LOGGING_LEVEL_ROOT:-DEBUG} +# LOGGING_LEVEL_APP: ${LOGGING_LEVEL_APP:-DEBUG} +# LEGALCONSENTHUB_DB_URL: jdbc:postgresql://legalconsenthub-db:5432/${LEGALCONSENTHUB_POSTGRES_DB} +# LEGALCONSENTHUB_DB_APP_USER: ${LEGALCONSENTHUB_POSTGRES_USER} +# LEGALCONSENTHUB_DB_PASSWORD: ${LEGALCONSENTHUB_POSTGRES_PASSWORD} +# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: ${KEYCLOAK_ISSUER_URL}/realms/${KEYCLOAK_REALM} +# SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8080/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs +# SERVER_PORT: 8080 +# LEGALCONSENTHUB_PDF_STORAGE_FILESYSTEM_BASE_DIR: /var/lib/legalconsenthub/pdfs +# ports: +# - "8081:8080" +# volumes: +# - legalconsenthub_pdf_cache:/var/lib/legalconsenthub/pdfs +# depends_on: +# legalconsenthub-db: +# condition: service_started +# networks: +# - legalconsenthub-net +# env_file: +# - .env.dev +# +# frontend: +# image: gitea.lugnas.de/denis/legalconsenthub:latest +# container_name: legalconsenthub-frontend-local +# ports: +# - "3211:3000" +# networks: +# - legalconsenthub-net +# env_file: +# - .env.dev +# +# legalconsenthub-db: +# image: postgres:latest +# container_name: legalconsenthub-postgres-local +# environment: +# POSTGRES_USER: ${LEGALCONSENTHUB_POSTGRES_USER} +# POSTGRES_PASSWORD: ${LEGALCONSENTHUB_POSTGRES_PASSWORD} +# POSTGRES_DB: ${LEGALCONSENTHUB_POSTGRES_DB} +# ports: +# - "5446:5432" +# networks: +# - legalconsenthub-net +# volumes: +# - legalconsenthub_postgres_data:/var/lib/postgresql +# env_file: +# - .env.dev diff --git a/deployment/docker-compose-prod.yaml b/deployment/docker-compose-prod.yaml index c007a82..6192f7a 100755 --- a/deployment/docker-compose-prod.yaml +++ b/deployment/docker-compose-prod.yaml @@ -13,6 +13,8 @@ services: container_name: legalconsenthub-backend restart: on-failure:2 environment: + LOGGING_LEVEL_ROOT: ${LOGGING_LEVEL_ROOT:-WARN} + LOGGING_LEVEL_APP: ${LOGGING_LEVEL_APP:-WARN} LEGALCONSENTHUB_DB_URL: jdbc:postgresql://legalconsenthub-db:5432/${LEGALCONSENTHUB_POSTGRES_DB} LEGALCONSENTHUB_DB_APP_USER: ${LEGALCONSENTHUB_POSTGRES_USER} LEGALCONSENTHUB_DB_PASSWORD: ${LEGALCONSENTHUB_POSTGRES_PASSWORD} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt index f5b9a76..0409779 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt @@ -70,7 +70,6 @@ class ApplicationFormService( id: UUID, applicationFormDto: ApplicationFormDto, ): ApplicationForm { - println("Updating ApplicationForm: $applicationFormDto") val existingApplicationForm = getApplicationFormById(id) val existingSnapshot = versionService.createSnapshot(existingApplicationForm) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt index 895a4dd..af90428 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt @@ -28,7 +28,6 @@ class JwtUserSyncFilter( val logger = LoggerFactory.getLogger(JwtUserSyncFilter::class.java) val auth: Authentication? = SecurityContextHolder.getContext().authentication - println("JwtUserSyncFilter invoked for request: ${request.requestURI}") try { if (auth is JwtAuthenticationToken && auth.isAuthenticated) { val jwt: Jwt = auth.token diff --git a/legalconsenthub-backend/src/main/resources/application-testcontainers.yaml b/legalconsenthub-backend/src/main/resources/application-testcontainers.yaml index 2c80384..a8929d4 100644 --- a/legalconsenthub-backend/src/main/resources/application-testcontainers.yaml +++ b/legalconsenthub-backend/src/main/resources/application-testcontainers.yaml @@ -13,7 +13,7 @@ spring: database-platform: org.hibernate.dialect.PostgreSQLDialect hibernate: ddl-auto: create - show-sql: true + show-sql: false properties: hibernate: format_sql: true @@ -39,16 +39,6 @@ spring: issuer-uri: http://localhost:7080/realms/legalconsenthub jwk-set-uri: http://localhost:7080/realms/legalconsenthub/protocol/openid-connect/certs -logging: - level: - org: - springframework: - security: TRACE - oauth2: TRACE - web: TRACE - org.testcontainers: INFO - com.github.dockerjava: WARN - management: health: mail: diff --git a/legalconsenthub-backend/src/main/resources/application.yaml b/legalconsenthub-backend/src/main/resources/application.yaml index 5e245f3..f27d294 100644 --- a/legalconsenthub-backend/src/main/resources/application.yaml +++ b/legalconsenthub-backend/src/main/resources/application.yaml @@ -61,3 +61,14 @@ management: health: mail: enabled: false + +# Log levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF +logging: + level: + com.betriebsratkanzlei.legalconsenthub: ${LOGGING_LEVEL_APP:INFO} + org: + springframework: + security: ${LOGGING_LEVEL_ROOT:INFO} + oauth2: ${LOGGING_LEVEL_ROOT:INFO} + web: ${LOGGING_LEVEL_ROOT:INFO} + testcontainers: ${LOGGING_LEVEL_ROOT:INFO} diff --git a/legalconsenthub/app/components/FormEngine.vue b/legalconsenthub/app/components/FormEngine.vue index 90b1aab..afaee12 100644 --- a/legalconsenthub/app/components/FormEngine.vue +++ b/legalconsenthub/app/components/FormEngine.vue @@ -91,9 +91,10 @@ const emit = defineEmits<{ const commentStore = useCommentStore() const { load: loadComments } = commentStore const { comments } = storeToRefs(commentStore) +const logger = useLogger().withTag('FormEngine') if (props.applicationFormId) { - console.log('Loading comments for application form:', props.applicationFormId) + logger.debug('Loading comments for application form:', props.applicationFormId) await loadComments(props.applicationFormId) } diff --git a/legalconsenthub/app/composables/applicationForm/useApplicationForm.ts b/legalconsenthub/app/composables/applicationForm/useApplicationForm.ts index c9154f7..7a00010 100644 --- a/legalconsenthub/app/composables/applicationForm/useApplicationForm.ts +++ b/legalconsenthub/app/composables/applicationForm/useApplicationForm.ts @@ -1,14 +1,16 @@ import type { ApplicationFormDto, PagedApplicationFormDto, FormElementDto } from '~~/.api-client' import { useApplicationFormApi } from './useApplicationFormApi' +import { useLogger } from '../useLogger' export function useApplicationForm() { const applicationFormApi = useApplicationFormApi() + const logger = useLogger().withTag('applicationForm') async function createApplicationForm(applicationFormDto: ApplicationFormDto): Promise { try { return await applicationFormApi.createApplicationForm(applicationFormDto) } catch (e: unknown) { - console.error('Failed creating application form:', e) + logger.error('Failed creating application form:', e) return Promise.reject(e) } } @@ -17,7 +19,7 @@ export function useApplicationForm() { try { return await applicationFormApi.getAllApplicationForms(organizationId) } catch (e: unknown) { - console.error('Failed retrieving application forms:', e, JSON.stringify(e)) + logger.error('Failed retrieving application forms:', e) return Promise.reject(e) } } @@ -26,7 +28,7 @@ export function useApplicationForm() { try { return await applicationFormApi.getApplicationFormById(id) } catch (e: unknown) { - console.error(`Failed retrieving application form with ID ${id}:`, e) + logger.error(`Failed retrieving application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -39,11 +41,11 @@ export function useApplicationForm() { return Promise.reject(new Error('ID or application form DTO missing')) } - console.log('Updating application form with ID:', id, applicationFormDto) + logger.debug('Updating application form with ID:', id, applicationFormDto) try { return await applicationFormApi.updateApplicationForm(id, applicationFormDto) } catch (e: unknown) { - console.error(`Failed updating application form with ID ${id}:`, e) + logger.error(`Failed updating application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -52,7 +54,7 @@ export function useApplicationForm() { try { return await applicationFormApi.deleteApplicationFormById(id) } catch (e: unknown) { - console.error(`Failed deleting application form with ID ${id}:`, e) + logger.error(`Failed deleting application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -65,7 +67,7 @@ export function useApplicationForm() { try { return await applicationFormApi.submitApplicationForm(id) } catch (e: unknown) { - console.error(`Failed submitting application form with ID ${id}:`, e) + logger.error(`Failed submitting application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -88,7 +90,7 @@ export function useApplicationForm() { position ) } catch (e: unknown) { - console.error(`Failed adding form element to subsection ${subsectionId}:`, e) + logger.error(`Failed adding form element to subsection ${subsectionId}:`, e) return Promise.reject(e) } } diff --git a/legalconsenthub/app/composables/applicationFormTemplate/useApplicationFormTemplate.ts b/legalconsenthub/app/composables/applicationFormTemplate/useApplicationFormTemplate.ts index 3cb7e0b..be084aa 100644 --- a/legalconsenthub/app/composables/applicationFormTemplate/useApplicationFormTemplate.ts +++ b/legalconsenthub/app/composables/applicationFormTemplate/useApplicationFormTemplate.ts @@ -1,10 +1,12 @@ import { type ApplicationFormDto, type PagedApplicationFormDto, ResponseError } from '~~/.api-client' import { useApplicationFormTemplateApi } from './useApplicationFormTemplateApi' +import { useLogger } from '../useLogger' const currentApplicationForm: Ref = ref() export async function useApplicationFormTemplate() { const applicationFormApi = await useApplicationFormTemplateApi() + const logger = useLogger().withTag('applicationFormTemplate') async function createApplicationFormTemplate(applicationFormDto: ApplicationFormDto): Promise { try { @@ -12,9 +14,9 @@ export async function useApplicationFormTemplate() { return currentApplicationForm.value } catch (e: unknown) { if (e instanceof ResponseError) { - console.error('Failed creating application form:', e.response) + logger.error('Failed creating application form:', e.response) } else { - console.error('Failed creating application form:', e) + logger.error('Failed creating application form:', e) } return Promise.reject(e) } @@ -25,9 +27,9 @@ export async function useApplicationFormTemplate() { return await applicationFormApi.getAllApplicationFormTemplates() } catch (e: unknown) { if (e instanceof ResponseError) { - console.error('Failed retrieving application forms:', e.response) + logger.error('Failed retrieving application forms:', e.response) } else { - console.error('Failed retrieving application forms:', e) + logger.error('Failed retrieving application forms:', e) } return Promise.reject(e) } @@ -38,9 +40,9 @@ export async function useApplicationFormTemplate() { return await applicationFormApi.getApplicationFormTemplateById(id) } catch (e: unknown) { if (e instanceof ResponseError) { - console.error(`Failed retrieving application form with ID ${id}:`, e.response) + logger.error(`Failed retrieving application form with ID ${id}:`, e.response) } else { - console.error(`Failed retrieving application form with ID ${id}:`, e) + logger.error(`Failed retrieving application form with ID ${id}:`, e) } return Promise.reject(e) } @@ -59,9 +61,9 @@ export async function useApplicationFormTemplate() { return currentApplicationForm.value } catch (e: unknown) { if (e instanceof ResponseError) { - console.error(`Failed updating application form with ID ${id}:`, e.response) + logger.error(`Failed updating application form with ID ${id}:`, e.response) } else { - console.error(`Failed updating application form with ID ${id}:`, e) + logger.error(`Failed updating application form with ID ${id}:`, e) } return Promise.reject(e) } @@ -72,9 +74,9 @@ export async function useApplicationFormTemplate() { return await applicationFormApi.deleteApplicationFormTemplateById(id) } catch (e: unknown) { if (e instanceof ResponseError) { - console.error(`Failed deleting application form with ID ${id}:`, e.response) + logger.error(`Failed deleting application form with ID ${id}:`, e.response) } else { - console.error(`Failed deleting application form with ID ${id}:`, e) + logger.error(`Failed deleting application form with ID ${id}:`, e) } return Promise.reject(e) } diff --git a/legalconsenthub/app/composables/applicationFormVersion/useApplicationFormVersion.ts b/legalconsenthub/app/composables/applicationFormVersion/useApplicationFormVersion.ts index b996003..e1ecfeb 100644 --- a/legalconsenthub/app/composables/applicationFormVersion/useApplicationFormVersion.ts +++ b/legalconsenthub/app/composables/applicationFormVersion/useApplicationFormVersion.ts @@ -1,14 +1,16 @@ import type { ApplicationFormVersionDto, ApplicationFormVersionListItemDto, ApplicationFormDto } from '~~/.api-client' import { useApplicationFormVersionApi } from '~/composables' +import { useLogger } from '../useLogger' export function useApplicationFormVersion() { const versionApi = useApplicationFormVersionApi() + const logger = useLogger().withTag('applicationFormVersion') async function getVersions(applicationFormId: string): Promise { try { return await versionApi.getVersions(applicationFormId) } catch (e: unknown) { - console.error(`Failed retrieving versions for application form ${applicationFormId}:`, e) + logger.error(`Failed retrieving versions for application form ${applicationFormId}:`, e) return Promise.reject(e) } } @@ -17,7 +19,7 @@ export function useApplicationFormVersion() { try { return await versionApi.getVersion(applicationFormId, versionNumber) } catch (e: unknown) { - console.error(`Failed retrieving version ${versionNumber} for application form ${applicationFormId}:`, e) + logger.error(`Failed retrieving version ${versionNumber} for application form ${applicationFormId}:`, e) return Promise.reject(e) } } @@ -30,7 +32,7 @@ export function useApplicationFormVersion() { try { return await versionApi.restoreVersion(applicationFormId, versionNumber) } catch (e: unknown) { - console.error(`Failed restoring version ${versionNumber} for application form ${applicationFormId}:`, e) + logger.error(`Failed restoring version ${versionNumber} for application form ${applicationFormId}:`, e) return Promise.reject(e) } } diff --git a/legalconsenthub/app/composables/comment/useCommentTextarea.ts b/legalconsenthub/app/composables/comment/useCommentTextarea.ts index e5f6998..39d343d 100644 --- a/legalconsenthub/app/composables/comment/useCommentTextarea.ts +++ b/legalconsenthub/app/composables/comment/useCommentTextarea.ts @@ -1,6 +1,7 @@ import type { CreateCommentDto, CommentDto } from '~~/.api-client' import { useCommentStore } from '~~/stores/useCommentStore' import { useUserStore } from '~~/stores/useUserStore' +import { useLogger } from '../useLogger' export function useCommentTextarea(applicationFormId: string) { const commentStore = useCommentStore() @@ -11,6 +12,8 @@ export function useCommentTextarea(applicationFormId: string) { const currentEditedComment = ref(null) const commentTextAreaValue = ref('') const toast = useToast() + const { t: $t } = useI18n() + const logger = useLogger().withTag('commentTextarea') async function submitComment(formElementId: string) { const newCommentDto: CreateCommentDto = { @@ -19,10 +22,10 @@ export function useCommentTextarea(applicationFormId: string) { try { await createComment(applicationFormId, formElementId, newCommentDto) commentTextAreaValue.value = '' - toast.add({ title: 'Comment created successfully', color: 'success' }) + toast.add({ title: $t('comments.created'), color: 'success' }) } catch (e) { - toast.add({ title: 'Error creating comment', color: 'error' }) - console.error('Error creating comment:', e) + toast.add({ title: $t('comments.createError'), color: 'error' }) + logger.error('Error creating comment:', e) } } @@ -34,10 +37,10 @@ export function useCommentTextarea(applicationFormId: string) { commentTextAreaValue.value = '' currentEditedComment.value = null isEditingComment.value = false - toast.add({ title: 'Comment updated successfully', color: 'success' }) + toast.add({ title: $t('comments.updated'), color: 'success' }) } catch (e) { - toast.add({ title: 'Error updating comment', color: 'error' }) - console.error('Error updating comment:', e) + toast.add({ title: $t('comments.updateError'), color: 'error' }) + logger.error('Error updating comment:', e) } } diff --git a/legalconsenthub/app/composables/notification/useNotification.ts b/legalconsenthub/app/composables/notification/useNotification.ts index 68e9c05..83467a5 100644 --- a/legalconsenthub/app/composables/notification/useNotification.ts +++ b/legalconsenthub/app/composables/notification/useNotification.ts @@ -2,6 +2,8 @@ import type { NotificationDto } from '~~/.api-client' import { useUserStore } from '~~/stores/useUserStore' export const useNotification = () => { + const logger = useLogger().withTag('notification') + const { getNotifications, getUnreadNotifications, @@ -22,7 +24,7 @@ export const useNotification = () => { const fetchNotifications = async (page: number = 0, size: number = 20) => { if (!organizationId.value) { - console.warn('No organization selected') + logger.warn('No organization selected') return } isLoading.value = true @@ -31,7 +33,7 @@ export const useNotification = () => { notifications.value = response.content || [] return response } catch (error) { - console.error('Failed to fetch notifications:', error) + logger.error('Failed to fetch notifications:', error) throw error } finally { isLoading.value = false @@ -40,7 +42,7 @@ export const useNotification = () => { const fetchUnreadNotifications = async () => { if (!organizationId.value) { - console.warn('No organization selected') + logger.warn('No organization selected') return } try { @@ -48,14 +50,14 @@ export const useNotification = () => { unreadNotifications.value = response || [] return response } catch (error) { - console.error('Failed to fetch unread notifications:', error) + logger.error('Failed to fetch unread notifications:', error) throw error } } const fetchUnreadCount = async () => { if (!userId.value || !organizationId.value) { - console.warn('No user or organization selected') + logger.warn('No user or organization selected') return } try { @@ -63,14 +65,14 @@ export const useNotification = () => { unreadCount.value = count || 0 return count } catch (error) { - console.error('Failed to fetch unread count:', error) + logger.error('Failed to fetch unread count:', error) throw error } } const markAllAsRead = async () => { if (!organizationId.value) { - console.warn('No organization selected') + logger.warn('No organization selected') return } try { @@ -79,14 +81,14 @@ export const useNotification = () => { unreadNotifications.value = [] notifications.value = notifications.value.map((n) => ({ ...n, isRead: true })) } catch (error) { - console.error('Failed to mark all as read:', error) + logger.error('Failed to mark all as read:', error) throw error } } const markAsRead = async (notificationId: string) => { if (!organizationId.value) { - console.warn('No organization selected') + logger.warn('No organization selected') return } try { @@ -102,7 +104,7 @@ export const useNotification = () => { unreadCount.value-- } } catch (error) { - console.error('Failed to mark notification as read:', error) + logger.error('Failed to mark notification as read:', error) throw error } } diff --git a/legalconsenthub/app/composables/useApplicationFormValidator.ts b/legalconsenthub/app/composables/useApplicationFormValidator.ts index 5d7a654..bfe7004 100644 --- a/legalconsenthub/app/composables/useApplicationFormValidator.ts +++ b/legalconsenthub/app/composables/useApplicationFormValidator.ts @@ -1,10 +1,13 @@ import { ComplianceStatus, type FormElementDto } from '~~/.api-client' import { complianceCheckableElementTypes, complianceMap } from './complianceMap' import type { FormElementId } from '~~/types/formElement' +import { useLogger } from './useLogger' const formElementComplianceMap = ref(new Map()) export function useApplicationFormValidator() { + const logger = useLogger().withTag('validator') + function getHighestComplianceStatus(): ComplianceStatus { const complianceStatusValues = Array.from(formElementComplianceMap.value.values()) const highestComplianceNumber = Math.max( @@ -37,7 +40,7 @@ export function useApplicationFormValidator() { formElement.options.forEach((option) => { if (!option.value) { - console.log(`Value missing for ${formElement.type}`) + logger.debug(`Value missing for ${formElement.type}`) return } diff --git a/legalconsenthub/app/composables/useLogger.ts b/legalconsenthub/app/composables/useLogger.ts new file mode 100644 index 0000000..381fbf9 --- /dev/null +++ b/legalconsenthub/app/composables/useLogger.ts @@ -0,0 +1,5 @@ +import type { ConsolaInstance } from 'consola' + +export function useLogger(): ConsolaInstance { + return useNuxtApp().$logger +} diff --git a/legalconsenthub/app/composables/useServerHealth.ts b/legalconsenthub/app/composables/useServerHealth.ts index a136a7c..6520643 100644 --- a/legalconsenthub/app/composables/useServerHealth.ts +++ b/legalconsenthub/app/composables/useServerHealth.ts @@ -1,8 +1,11 @@ +import { useLogger } from './useLogger' + export const isServerAvailable = ref(true) export const isChecking = ref(false) export const lastCheckTime = ref(null) export function useServerHealth() { + const logger = useLogger().withTag('serverHealth') const checkInterval = ref | null>(null) const healthCheckUrl = '/api/actuator/health' @@ -30,11 +33,11 @@ export function useServerHealth() { isServerAvailable.value = response.ok if (!wasAvailable && isServerAvailable.value) { - console.info('Server is back online') + logger.info('Server is back online') } if (wasAvailable && !isServerAvailable.value) { - console.warn('Server is no longer available') + logger.warn('Server is no longer available') } return isServerAvailable.value @@ -43,7 +46,7 @@ export function useServerHealth() { isServerAvailable.value = false if (wasAvailable) { - console.warn('Server health check failed:', error) + logger.warn('Server health check failed:', error) } return false diff --git a/legalconsenthub/app/layouts/default.vue b/legalconsenthub/app/layouts/default.vue index 5ab87c2..aa48773 100644 --- a/legalconsenthub/app/layouts/default.vue +++ b/legalconsenthub/app/layouts/default.vue @@ -57,6 +57,7 @@ const links = [ ] ] const open = ref(false) +const logger = useLogger().withTag('layout') const isNotificationsSlideoverOpen = ref(false) const { unreadCount, fetchUnreadCount, startPeriodicRefresh } = useNotification() @@ -73,13 +74,13 @@ provide('notificationState', { async function copyAccessTokenToClipboard() { const { session } = useUserSession() - console.log('Access Token :', session.value?.jwt?.accessToken) + logger.debug('Access Token :', session.value?.jwt?.accessToken) const accessToken = session.value?.jwt?.accessToken if (accessToken) { navigator.clipboard.writeText(accessToken) - console.log('Access token copied to clipboard') + logger.info('Access token copied to clipboard') } else { - console.warn('No access token found in session') + logger.warn('No access token found in session') } } diff --git a/legalconsenthub/app/middleware/refreshToken.global.ts b/legalconsenthub/app/middleware/refreshToken.global.ts index 9c32318..abdf597 100644 --- a/legalconsenthub/app/middleware/refreshToken.global.ts +++ b/legalconsenthub/app/middleware/refreshToken.global.ts @@ -3,14 +3,16 @@ import { appendResponseHeader } from 'h3' import { parse, parseSetCookie, serialize } from 'cookie-es' import { jwtDecode, type JwtPayload } from 'jwt-decode' +import { useLogger } from '../composables/useLogger' export default defineNuxtRouteMiddleware(async (to, from) => { const nuxtApp = useNuxtApp() // Don't run on client hydration when server rendered if (import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return - console.log('🔍 Middleware: refreshToken.global.ts') - console.log(` from: ${from.fullPath} to: ${to.fullPath}`) + const logger = useLogger().withTag('refreshToken') + logger.debug('🔍 Middleware: refreshToken.global.ts') + logger.debug(`from: ${from.fullPath} to: ${to.fullPath}`) const { session, clear: clearSession, fetch: fetchSession } = useUserSession() // Ignore if no tokens @@ -25,11 +27,11 @@ export default defineNuxtRouteMiddleware(async (to, from) => { // Both tokens expired, clearing session if (isExpired(accessPayload) && isExpired(refreshPayload)) { - console.info('both tokens expired, clearing session') + logger.info('both tokens expired, clearing session') await clearSession() return navigateTo('/login') } else if (isExpired(accessPayload)) { - console.info('access token expired, refreshing') + logger.info('access token expired, refreshing') await useRequestFetch()('/api/jwt/refresh', { method: 'POST', onResponse({ response: { headers } }: { response: { headers: Headers } }) { @@ -41,7 +43,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => { const { name, value } = parseSetCookie(setCookie) if (name === runtimeConfig.session.name) { - console.log('updating headers.cookie to', value) + logger.debug('updating headers.cookie to', value) const cookies = parse(serverEvent.headers.get('cookie') || '') // set or overwrite existing cookie @@ -64,10 +66,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => { } }, onError() { - console.error('🔍 Middleware: Token refresh failed') + logger.error('🔍 Middleware: Token refresh failed') const { loggedIn } = useUserSession() if (!loggedIn.value) { - console.log('🔍 Middleware: User not logged in, redirecting to /login') + logger.debug('🔍 Middleware: User not logged in, redirecting to /login') return navigateTo('/login') } } diff --git a/legalconsenthub/app/pages/administration.vue b/legalconsenthub/app/pages/administration.vue index 6750178..b91cbf1 100644 --- a/legalconsenthub/app/pages/administration.vue +++ b/legalconsenthub/app/pages/administration.vue @@ -94,6 +94,7 @@ const { hasRole } = usePermissions() const { getAllApplicationFormTemplates, updateApplicationFormTemplate, createApplicationFormTemplate } = await useApplicationFormTemplate() const { t: $t } = useI18n() +const logger = useLogger().withTag('administration') if (!hasRole('CHIEF_EXECUTIVE_OFFICER') && !hasRole('IT_DEPARTMENT')) { await navigateTo('/') @@ -239,7 +240,7 @@ async function saveTemplate() { description: $t('templates.saveError'), color: 'error' }) - console.error('Error saving template:', error) + logger.error('Error saving template:', error) } finally { isSaving.value = false } diff --git a/legalconsenthub/app/pages/callback.vue b/legalconsenthub/app/pages/callback.vue index a7a096e..28ae9c0 100644 --- a/legalconsenthub/app/pages/callback.vue +++ b/legalconsenthub/app/pages/callback.vue @@ -4,10 +4,11 @@ diff --git a/legalconsenthub/app/pages/create.vue b/legalconsenthub/app/pages/create.vue index 23becf8..5147f6a 100644 --- a/legalconsenthub/app/pages/create.vue +++ b/legalconsenthub/app/pages/create.vue @@ -60,6 +60,7 @@ const userStore = useUserStore() const { selectedOrganization } = storeToRefs(userStore) const toast = useToast() const { t: $t } = useI18n() +const logger = useLogger().withTag('create') const { data, error } = await useAsyncData( 'create-application-form', @@ -132,11 +133,11 @@ function handleFormElementSectionsUpdate(sections: FormElementSectionDto[]) { async function prepareAndCreateApplicationForm() { if (!applicationFormTemplate.value) { - console.error('Application form data is undefined') + logger.error('Application form data is undefined') return null } - console.log('selectedOrganization', selectedOrganization.value) + logger.debug('selectedOrganization', selectedOrganization.value) applicationFormTemplate.value.organizationId = selectedOrganization.value?.id ?? '' return await createApplicationForm(applicationFormTemplate.value) diff --git a/legalconsenthub/app/pages/settings.vue b/legalconsenthub/app/pages/settings.vue index 36ede73..befa1a8 100644 --- a/legalconsenthub/app/pages/settings.vue +++ b/legalconsenthub/app/pages/settings.vue @@ -119,6 +119,7 @@ const appConfig = useAppConfig() const toast = useToast() const userStore = useUserStore() const { getUserById, updateEmailPreferences } = useUser() +const logger = useLogger().withTag('settings') const colors = [ 'red', @@ -152,7 +153,7 @@ onMounted(async () => { emailOnFormCreated.value = userData.emailOnFormCreated ?? true emailOnFormSubmitted.value = userData.emailOnFormSubmitted ?? true } catch (error) { - console.error('Failed to load user email preferences:', error) + logger.error('Failed to load user email preferences:', error) } } }) diff --git a/legalconsenthub/app/plugins/error-handler.ts b/legalconsenthub/app/plugins/error-handler.ts index 4f24943..1b99a5c 100644 --- a/legalconsenthub/app/plugins/error-handler.ts +++ b/legalconsenthub/app/plugins/error-handler.ts @@ -1,9 +1,18 @@ +import { createLogger } from '~~/shared/utils/logger' + export default defineNuxtPlugin((nuxtApp) => { + const config = useRuntimeConfig() + const logger = createLogger({ + level: config.public.logLevel, + tag: 'app error-handler', + fancy: import.meta.env.MODE !== 'production' + }) + nuxtApp.hook('vue:error', (error, instance, info) => { - console.error('Vue error:', error, 'Instance:', instance, 'Info:', info) + logger.error('Vue error:', error, 'Instance:', instance, 'Info:', info) }) nuxtApp.hook('app:error', (error) => { - console.error('App error:', error) + logger.error('App error:', error) }) }) diff --git a/legalconsenthub/app/plugins/logger.ts b/legalconsenthub/app/plugins/logger.ts new file mode 100644 index 0000000..d88a819 --- /dev/null +++ b/legalconsenthub/app/plugins/logger.ts @@ -0,0 +1,16 @@ +import { createLogger } from '~~/shared/utils/logger' + +export default defineNuxtPlugin(() => { + const config = useRuntimeConfig() + const logger = createLogger({ + level: config.public.logLevel, + tag: 'legalconsenthub', + fancy: import.meta.env.MODE !== 'production' + }) + + return { + provide: { + logger + } + } +}) diff --git a/legalconsenthub/app/utils/wrappedFetch.ts b/legalconsenthub/app/utils/wrappedFetch.ts index 1aa0e47..7321ece 100644 --- a/legalconsenthub/app/utils/wrappedFetch.ts +++ b/legalconsenthub/app/utils/wrappedFetch.ts @@ -1,4 +1,5 @@ import type { HTTPMethod } from 'h3' +import { useLogger } from '../composables/useLogger' // Custom OpenAPI fetch client that wraps useRequestFetch. This ensures that authentication headers // are forwarded correctly during SSR. Unlike fetch, useRequestFetch returns data directly, @@ -28,7 +29,8 @@ export const wrappedFetchWrap = (requestFetch: ReturnType { const { serverApiBaseUrl, clientProxyBasePath } = useRuntimeConfig().public + const logger = createLogger({ + level: useRuntimeConfig().public.logLevel, + tag: '🔀 proxy' + }) const escapedClientProxyBasePath = clientProxyBasePath.replace(/^\//, '\\/') // Use the escaped value in the regex const path = event.path.replace(new RegExp(`^${escapedClientProxyBasePath}`), '') @@ -19,8 +24,10 @@ export default defineEventHandler(async (event: H3Event) => { }) } - if (accessToken) console.log('🔀 [PROXY] Expiration:', new Date(jwtDecode(accessToken).exp! * 1000).toISOString()) - console.log('🔀 [PROXY] Proxying request to:', target) + if (accessToken) { + logger.debug('Expiration:', new Date(jwtDecode(accessToken).exp! * 1000).toISOString()) + } + logger.debug('Proxying request to:', target) return proxyRequest(event, target, { headers: { diff --git a/legalconsenthub/server/routes/auth/keycloak.get.ts b/legalconsenthub/server/routes/auth/keycloak.get.ts index 299a917..4f9f304 100644 --- a/legalconsenthub/server/routes/auth/keycloak.get.ts +++ b/legalconsenthub/server/routes/auth/keycloak.get.ts @@ -1,15 +1,23 @@ import { jwtDecode } from 'jwt-decode' import type { KeycloakTokenPayload, Organization } from '~~/types/keycloak' +import { createLogger } from '~~/shared/utils/logger' export default defineOAuthKeycloakEventHandler({ async onSuccess(event, { user, tokens }) { + const config = useRuntimeConfig() + const logger = createLogger({ + level: config.public.logLevel, + tag: 'auth', + fancy: import.meta.env.MODE !== 'production' + }) + const rawAccessToken = tokens?.access_token let decodedJwt: KeycloakTokenPayload | null = null try { decodedJwt = jwtDecode(rawAccessToken!) } catch (err) { - console.warn('[auth] Failed to decode access token:', err) + logger.warn('Failed to decode access token:', err) } const organizations = decodedJwt ? extractOrganizations(decodedJwt) : [] @@ -34,7 +42,13 @@ export default defineOAuthKeycloakEventHandler({ }, onError(event) { - console.log('error during keycloak authentication', event) + const config = useRuntimeConfig() + const logger = createLogger({ + level: config.public.logLevel, + tag: 'auth' + }) + + logger.error('Error during keycloak authentication') return sendRedirect(event, '/login') } }) diff --git a/legalconsenthub/server/routes/auth/logout.get.ts b/legalconsenthub/server/routes/auth/logout.get.ts index d828273..73e35ca 100644 --- a/legalconsenthub/server/routes/auth/logout.get.ts +++ b/legalconsenthub/server/routes/auth/logout.get.ts @@ -1,11 +1,20 @@ +import { createLogger } from '~~/shared/utils/logger' + export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const logger = createLogger({ + level: config.public.logLevel, + tag: 'auth', + fancy: import.meta.env.MODE !== 'production' + }) + try { const cleared = await clearUserSession(event) if (!cleared) { - console.warn('Failed to clear user session') + logger.warn('Failed to clear user session') } } catch (error) { - console.error('Error clearing user session:', error) + logger.error('Error clearing user session:', error) } return sendRedirect(event, '/login', 200) diff --git a/legalconsenthub/shared/utils/logger.ts b/legalconsenthub/shared/utils/logger.ts new file mode 100644 index 0000000..b98aee2 --- /dev/null +++ b/legalconsenthub/shared/utils/logger.ts @@ -0,0 +1,48 @@ +import { createConsola } from 'consola' +import type { ConsolaInstance } from 'consola' + +export type AppLogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'verbose' | 'log' + +export function createLogger(options: { level?: string; tag?: string; fancy?: boolean } = {}): ConsolaInstance { + const level = resolveConsolaLevel(options.level) + + const logger = createConsola({ + level + }) + + return options.tag ? logger.withTag(options.tag) : logger +} + +export function resolveConsolaLevel(level?: string): number { + const normalized = (level ?? '').toString().trim().toLowerCase() + + // Consola levels (from docs): + // 0: Fatal and Error + // 1: Warnings + // 2: Normal logs + // 3: Informational logs + // 4: Debug logs + // 5: Trace logs + // -999: Silent + // +999: Verbose logs + switch (normalized as AppLogLevel) { + case 'silent': + return -999 + case 'error': + return 0 + case 'warn': + return 1 + case 'log': + return 2 + case 'info': + return 3 + case 'debug': + return 4 + case 'trace': + return 5 + case 'verbose': + return 999 + default: + return 3 + } +} diff --git a/legalconsenthub/stores/useCommentStore.ts b/legalconsenthub/stores/useCommentStore.ts index 65fdbbb..0ce6763 100644 --- a/legalconsenthub/stores/useCommentStore.ts +++ b/legalconsenthub/stores/useCommentStore.ts @@ -1,12 +1,14 @@ import { defineStore } from 'pinia' import { type CreateCommentDto, type CommentDto, ResponseError } from '~~/.api-client' import { useCommentApi } from '~/composables/comment/useCommentApi' +import { useLogger } from '../app/composables/useLogger' export const useCommentStore = defineStore('Comment', () => { type FormElementId = string const commentApi = useCommentApi() const comments = ref>({}) const loadedForms = ref(new Set()) + const logger = useLogger().withTag('commentStore') async function load(applicationFormId: string) { if (loadedForms.value.has(applicationFormId)) return @@ -14,7 +16,7 @@ export const useCommentStore = defineStore('Comment', () => { commentApi.getCommentsByApplicationFormId(applicationFormId) ) if (error.value) { - console.error('Failed loading comments:', error.value) + logger.error('Failed loading comments:', error.value) return } comments.value = @@ -43,9 +45,9 @@ export const useCommentStore = defineStore('Comment', () => { return newComment } catch (e: unknown) { if (e instanceof ResponseError) { - console.error('Failed creating comment:', e.response) + logger.error('Failed creating comment:', e.response) } else { - console.error('Failed creating comment:', e) + logger.error('Failed creating comment:', e) } return Promise.reject(e) } @@ -69,9 +71,9 @@ export const useCommentStore = defineStore('Comment', () => { return updatedComment } catch (e: unknown) { if (e instanceof ResponseError) { - console.error(`Failed updating comment with ID ${id}:`, e.response) + logger.error(`Failed updating comment with ID ${id}:`, e.response) } else { - console.error(`Failed updating comment with ID ${id}:`, e) + logger.error(`Failed updating comment with ID ${id}:`, e) } return Promise.reject(e) } @@ -92,9 +94,9 @@ export const useCommentStore = defineStore('Comment', () => { } } catch (e: unknown) { if (e instanceof ResponseError) { - console.error(`Failed deleting comment with ID ${id}:`, e.response) + logger.error(`Failed deleting comment with ID ${id}:`, e.response) } else { - console.error(`Failed deleting comment with ID ${id}:`, e) + logger.error(`Failed deleting comment with ID ${id}:`, e) } return Promise.reject(e) }