From ca166eb6611e1374e239f0ef464436cb70213006 Mon Sep 17 00:00:00 2001 From: oskar Date: Fri, 9 Jan 2026 19:01:46 +0100 Subject: [PATCH] cloude review --- .../mosenioring/app/config/SecurityConfig.kt | 12 ++-- .../mosenioring/app/outbox/OutboxPublisher.kt | 31 ++++++---- .../app/src/main/resources/application.yml | 8 +++ .../db/migration/V2__add_foreign_keys.sql | 40 +++++++++++++ .../com/mosenioring/common/BaseEntity.kt | 14 +++++ back001/modules/clinical/build.gradle.kts | 2 - back001/modules/identity/build.gradle.kts | 2 - .../integrations/service/FhirGateway.kt | 1 + .../integrations/service/FileService.kt | 56 ++++++++----------- back001/modules/messaging/build.gradle.kts | 2 - 10 files changed, 112 insertions(+), 56 deletions(-) create mode 100644 back001/app/src/main/resources/db/migration/V2__add_foreign_keys.sql diff --git a/back001/app/src/main/kotlin/com/mosenioring/app/config/SecurityConfig.kt b/back001/app/src/main/kotlin/com/mosenioring/app/config/SecurityConfig.kt index f1584be..b94c103 100644 --- a/back001/app/src/main/kotlin/com/mosenioring/app/config/SecurityConfig.kt +++ b/back001/app/src/main/kotlin/com/mosenioring/app/config/SecurityConfig.kt @@ -4,19 +4,22 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import com.mosenioring.common.security.LocalAuthFilter +import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.config.Customizer.withDefaults import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.web.SecurityFilterChain import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter -import org.springframework.beans.factory.ObjectProvider @Configuration @EnableMethodSecurity class SecurityConfig { @Bean - fun filterChain(http: HttpSecurity, localAuthFilterProvider: ObjectProvider): SecurityFilterChain { + fun filterChain( + http: HttpSecurity, + @Autowired(required = false) localAuthFilter: LocalAuthFilter? + ): SecurityFilterChain { http .csrf { it.disable() } .authorizeHttpRequests { @@ -25,9 +28,8 @@ class SecurityConfig { it.anyRequest().authenticated() } .oauth2ResourceServer { it.jwt(withDefaults()) } - val localAuthFilter = localAuthFilterProvider.ifAvailable - if (localAuthFilter != null) { - http.addFilterBefore(localAuthFilter, BearerTokenAuthenticationFilter::class.java) + localAuthFilter?.let { + http.addFilterBefore(it, BearerTokenAuthenticationFilter::class.java) } return http.build() } diff --git a/back001/app/src/main/kotlin/com/mosenioring/app/outbox/OutboxPublisher.kt b/back001/app/src/main/kotlin/com/mosenioring/app/outbox/OutboxPublisher.kt index 9ae582c..fab6f15 100644 --- a/back001/app/src/main/kotlin/com/mosenioring/app/outbox/OutboxPublisher.kt +++ b/back001/app/src/main/kotlin/com/mosenioring/app/outbox/OutboxPublisher.kt @@ -4,6 +4,7 @@ import com.mosenioring.common.Events import com.mosenioring.common.outbox.OutboxRepository import org.slf4j.LoggerFactory import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.beans.factory.annotation.Value import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -11,30 +12,36 @@ import org.springframework.transaction.annotation.Transactional @Component class OutboxPublisher( private val repository: OutboxRepository, - private val rabbitTemplate: RabbitTemplate + private val rabbitTemplate: RabbitTemplate, + @Value("\${app.outbox.batch-size:50}") private val batchSize: Int ) { private val logger = LoggerFactory.getLogger(javaClass) @Scheduled(fixedDelayString = "\${app.outbox.publish-delay-ms:2000}") @Transactional fun publishPending() { - val pending = repository.findTop50ByStatusOrderByCreatedAtAsc("PENDING") + val pending = repository.findTop50ByStatusOrderByCreatedAtAsc("PENDING").take(batchSize) if (pending.isEmpty()) { return } pending.forEach { event -> - val exchange = when (event.eventType) { - Events.MEDICATION_PLAN_CREATED -> Events.MEDICATION_EXCHANGE - Events.NOTIFICATION_REQUESTED -> Events.NOTIFICATION_EXCHANGE - else -> null - } - if (exchange == null) { - logger.warn("Unknown event type {}", event.eventType) + try { + val exchange = when (event.eventType) { + Events.MEDICATION_PLAN_CREATED -> Events.MEDICATION_EXCHANGE + Events.NOTIFICATION_REQUESTED -> Events.NOTIFICATION_EXCHANGE + else -> null + } + if (exchange == null) { + logger.warn("Unknown event type {}", event.eventType) + event.status = "FAILED" + return@forEach + } + rabbitTemplate.convertAndSend(exchange, event.eventType, event.payload) + event.status = "PUBLISHED" + } catch (e: Exception) { + logger.error("Failed to publish event ${event.id}", e) event.status = "FAILED" - return@forEach } - rabbitTemplate.convertAndSend(exchange, event.eventType, event.payload) - event.status = "PUBLISHED" } repository.saveAll(pending) } diff --git a/back001/app/src/main/resources/application.yml b/back001/app/src/main/resources/application.yml index 359f0cc..b3945b0 100644 --- a/back001/app/src/main/resources/application.yml +++ b/back001/app/src/main/resources/application.yml @@ -41,8 +41,16 @@ storage: app: outbox: publish-delay-ms: 2000 + batch-size: 50 tenant: header: X-Tenant-Id + events: + medication-plan-created: MedicationPlanCreated + notification-requested: NotificationRequested + medication-exchange: medication.events + notification-exchange: notification.events + medication-queue: medication.plan.created + notification-queue: notification.requested management: endpoints: diff --git a/back001/app/src/main/resources/db/migration/V2__add_foreign_keys.sql b/back001/app/src/main/resources/db/migration/V2__add_foreign_keys.sql new file mode 100644 index 0000000..4f60c1a --- /dev/null +++ b/back001/app/src/main/resources/db/migration/V2__add_foreign_keys.sql @@ -0,0 +1,40 @@ +-- Add foreign key constraints for data integrity + +-- Users reference tenants +alter table users add constraint fk_users_tenant + foreign key (tenant_id) references tenants(id); + +-- Patient relationships +alter table patient_caregivers add constraint fk_patient_caregivers_patient + foreign key (patient_id) references patients(id) on delete cascade; + +alter table patient_caregivers add constraint fk_patient_caregivers_user + foreign key (user_id) references users(id) on delete cascade; + +alter table patient_doctors add constraint fk_patient_doctors_patient + foreign key (patient_id) references patients(id) on delete cascade; + +alter table patient_doctors add constraint fk_patient_doctors_user + foreign key (user_id) references users(id) on delete cascade; + +-- Clinical data +alter table medication_plans add constraint fk_medication_plans_patient + foreign key (patient_id) references patients(id) on delete cascade; + +alter table tests add constraint fk_tests_patient + foreign key (patient_id) references patients(id) on delete cascade; + +-- Messages +alter table messages add constraint fk_messages_patient + foreign key (patient_id) references patients(id) on delete cascade; + +alter table messages add constraint fk_messages_sender + foreign key (sender_id) references users(id); + +-- Files +alter table files add constraint fk_files_patient + foreign key (patient_id) references patients(id) on delete set null; + +-- Audit events +alter table audit_events add constraint fk_audit_events_patient + foreign key (patient_id) references patients(id) on delete set null; diff --git a/back001/common/src/main/kotlin/com/mosenioring/common/BaseEntity.kt b/back001/common/src/main/kotlin/com/mosenioring/common/BaseEntity.kt index 03cd468..007d417 100644 --- a/back001/common/src/main/kotlin/com/mosenioring/common/BaseEntity.kt +++ b/back001/common/src/main/kotlin/com/mosenioring/common/BaseEntity.kt @@ -1,5 +1,7 @@ package com.mosenioring.common +import com.mosenioring.common.security.SecurityUtils +import com.mosenioring.common.tenant.TenantContext import jakarta.persistence.Column import jakarta.persistence.MappedSuperclass import jakarta.persistence.PrePersist @@ -29,10 +31,22 @@ abstract class BaseEntity { val now = Instant.now() createdAt = now updatedAt = now + if (!::tenantId.isInitialized) { + tenantId = TenantContext.getTenantId() ?: "unknown" + } + if (createdBy == null) { + createdBy = SecurityUtils.currentUserId() + } + if (updatedBy == null) { + updatedBy = SecurityUtils.currentUserId() + } } @PreUpdate fun onUpdate() { updatedAt = Instant.now() + if (updatedBy == null) { + updatedBy = SecurityUtils.currentUserId() + } } } diff --git a/back001/modules/clinical/build.gradle.kts b/back001/modules/clinical/build.gradle.kts index 22ead93..5db2a38 100644 --- a/back001/modules/clinical/build.gradle.kts +++ b/back001/modules/clinical/build.gradle.kts @@ -9,6 +9,4 @@ plugins { dependencies { api(project(":common")) implementation(project(":modules:audit")) - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-validation") } diff --git a/back001/modules/identity/build.gradle.kts b/back001/modules/identity/build.gradle.kts index 22ead93..5db2a38 100644 --- a/back001/modules/identity/build.gradle.kts +++ b/back001/modules/identity/build.gradle.kts @@ -9,6 +9,4 @@ plugins { dependencies { api(project(":common")) implementation(project(":modules:audit")) - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-validation") } diff --git a/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FhirGateway.kt b/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FhirGateway.kt index c1d97cb..a663582 100644 --- a/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FhirGateway.kt +++ b/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FhirGateway.kt @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service @Service class FhirGateway { + // TODO: Implement actual FHIR integration fun submitObservation(payload: String): String { return "stub-${payload.hashCode()}" } diff --git a/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FileService.kt b/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FileService.kt index daf5138..04f2b19 100644 --- a/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FileService.kt +++ b/back001/modules/integrations/src/main/kotlin/com/mosenioring/integrations/service/FileService.kt @@ -28,9 +28,9 @@ class FileService( @Value("\${storage.s3.secret-key}") private val secretKey: String, @Value("\${storage.s3.bucket}") private val bucket: String ) { - private fun presigner(): S3Presigner { + private val presigner: S3Presigner by lazy { val creds = AwsBasicCredentials.create(accessKey, secretKey) - return S3Presigner.builder() + S3Presigner.builder() .endpointOverride(URI.create(endpoint)) .region(Region.of(region)) .credentialsProvider(StaticCredentialsProvider.create(creds)) @@ -48,42 +48,32 @@ class FileService( metadata.updatedBy = SecurityUtils.currentUserId() repository.save(metadata) - val presigner = presigner() - try { - val request = PutObjectRequest.builder() - .bucket(bucket) - .key(storageKey) - .contentType(contentType) - .build() - val presignRequest = PutObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(15)) - .putObjectRequest(request) - .build() - val presigned = presigner.presignPutObject(presignRequest) - return PresignResponse(fileId, presigned.url().toString()) - } finally { - presigner.close() - } + val request = PutObjectRequest.builder() + .bucket(bucket) + .key(storageKey) + .contentType(contentType) + .build() + val presignRequest = PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(15)) + .putObjectRequest(request) + .build() + val presigned = presigner.presignPutObject(presignRequest) + return PresignResponse(fileId, presigned.url().toString()) } fun presignDownload(fileId: String): PresignResponse { val tenantId = TenantContext.getTenantId() ?: throw IllegalStateException("Missing tenant") val metadata = repository.findByIdAndTenantId(fileId, tenantId) ?: throw IllegalArgumentException("File not found") - val presigner = presigner() - try { - val request = GetObjectRequest.builder() - .bucket(bucket) - .key(metadata.storageKey) - .build() - val presignRequest = GetObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(15)) - .getObjectRequest(request) - .build() - val presigned = presigner.presignGetObject(presignRequest) - return PresignResponse(metadata.id, presigned.url().toString()) - } finally { - presigner.close() - } + val request = GetObjectRequest.builder() + .bucket(bucket) + .key(metadata.storageKey) + .build() + val presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(15)) + .getObjectRequest(request) + .build() + val presigned = presigner.presignGetObject(presignRequest) + return PresignResponse(metadata.id, presigned.url().toString()) } } diff --git a/back001/modules/messaging/build.gradle.kts b/back001/modules/messaging/build.gradle.kts index 22ead93..5db2a38 100644 --- a/back001/modules/messaging/build.gradle.kts +++ b/back001/modules/messaging/build.gradle.kts @@ -9,6 +9,4 @@ plugins { dependencies { api(project(":common")) implementation(project(":modules:audit")) - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-validation") }