cloude review

This commit is contained in:
oskar 2026-01-09 19:01:46 +01:00
parent 195ca7d961
commit ca166eb661
10 changed files with 112 additions and 56 deletions

View file

@ -4,19 +4,22 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import com.mosenioring.common.security.LocalAuthFilter 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.Customizer.withDefaults
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter
import org.springframework.beans.factory.ObjectProvider
@Configuration @Configuration
@EnableMethodSecurity @EnableMethodSecurity
class SecurityConfig { class SecurityConfig {
@Bean @Bean
fun filterChain(http: HttpSecurity, localAuthFilterProvider: ObjectProvider<LocalAuthFilter>): SecurityFilterChain { fun filterChain(
http: HttpSecurity,
@Autowired(required = false) localAuthFilter: LocalAuthFilter?
): SecurityFilterChain {
http http
.csrf { it.disable() } .csrf { it.disable() }
.authorizeHttpRequests { .authorizeHttpRequests {
@ -25,9 +28,8 @@ class SecurityConfig {
it.anyRequest().authenticated() it.anyRequest().authenticated()
} }
.oauth2ResourceServer { it.jwt(withDefaults()) } .oauth2ResourceServer { it.jwt(withDefaults()) }
val localAuthFilter = localAuthFilterProvider.ifAvailable localAuthFilter?.let {
if (localAuthFilter != null) { http.addFilterBefore(it, BearerTokenAuthenticationFilter::class.java)
http.addFilterBefore(localAuthFilter, BearerTokenAuthenticationFilter::class.java)
} }
return http.build() return http.build()
} }

View file

@ -4,6 +4,7 @@ import com.mosenioring.common.Events
import com.mosenioring.common.outbox.OutboxRepository import com.mosenioring.common.outbox.OutboxRepository
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -11,30 +12,36 @@ import org.springframework.transaction.annotation.Transactional
@Component @Component
class OutboxPublisher( class OutboxPublisher(
private val repository: OutboxRepository, 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) private val logger = LoggerFactory.getLogger(javaClass)
@Scheduled(fixedDelayString = "\${app.outbox.publish-delay-ms:2000}") @Scheduled(fixedDelayString = "\${app.outbox.publish-delay-ms:2000}")
@Transactional @Transactional
fun publishPending() { fun publishPending() {
val pending = repository.findTop50ByStatusOrderByCreatedAtAsc("PENDING") val pending = repository.findTop50ByStatusOrderByCreatedAtAsc("PENDING").take(batchSize)
if (pending.isEmpty()) { if (pending.isEmpty()) {
return return
} }
pending.forEach { event -> pending.forEach { event ->
val exchange = when (event.eventType) { try {
Events.MEDICATION_PLAN_CREATED -> Events.MEDICATION_EXCHANGE val exchange = when (event.eventType) {
Events.NOTIFICATION_REQUESTED -> Events.NOTIFICATION_EXCHANGE Events.MEDICATION_PLAN_CREATED -> Events.MEDICATION_EXCHANGE
else -> null Events.NOTIFICATION_REQUESTED -> Events.NOTIFICATION_EXCHANGE
} else -> null
if (exchange == null) { }
logger.warn("Unknown event type {}", event.eventType) 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" event.status = "FAILED"
return@forEach
} }
rabbitTemplate.convertAndSend(exchange, event.eventType, event.payload)
event.status = "PUBLISHED"
} }
repository.saveAll(pending) repository.saveAll(pending)
} }

View file

@ -41,8 +41,16 @@ storage:
app: app:
outbox: outbox:
publish-delay-ms: 2000 publish-delay-ms: 2000
batch-size: 50
tenant: tenant:
header: X-Tenant-Id 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: management:
endpoints: endpoints:

View file

@ -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;

View file

@ -1,5 +1,7 @@
package com.mosenioring.common package com.mosenioring.common
import com.mosenioring.common.security.SecurityUtils
import com.mosenioring.common.tenant.TenantContext
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.MappedSuperclass import jakarta.persistence.MappedSuperclass
import jakarta.persistence.PrePersist import jakarta.persistence.PrePersist
@ -29,10 +31,22 @@ abstract class BaseEntity {
val now = Instant.now() val now = Instant.now()
createdAt = now createdAt = now
updatedAt = now updatedAt = now
if (!::tenantId.isInitialized) {
tenantId = TenantContext.getTenantId() ?: "unknown"
}
if (createdBy == null) {
createdBy = SecurityUtils.currentUserId()
}
if (updatedBy == null) {
updatedBy = SecurityUtils.currentUserId()
}
} }
@PreUpdate @PreUpdate
fun onUpdate() { fun onUpdate() {
updatedAt = Instant.now() updatedAt = Instant.now()
if (updatedBy == null) {
updatedBy = SecurityUtils.currentUserId()
}
} }
} }

View file

@ -9,6 +9,4 @@ plugins {
dependencies { dependencies {
api(project(":common")) api(project(":common"))
implementation(project(":modules:audit")) implementation(project(":modules:audit"))
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
} }

View file

@ -9,6 +9,4 @@ plugins {
dependencies { dependencies {
api(project(":common")) api(project(":common"))
implementation(project(":modules:audit")) implementation(project(":modules:audit"))
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
} }

View file

@ -4,6 +4,7 @@ import org.springframework.stereotype.Service
@Service @Service
class FhirGateway { class FhirGateway {
// TODO: Implement actual FHIR integration
fun submitObservation(payload: String): String { fun submitObservation(payload: String): String {
return "stub-${payload.hashCode()}" return "stub-${payload.hashCode()}"
} }

View file

@ -28,9 +28,9 @@ class FileService(
@Value("\${storage.s3.secret-key}") private val secretKey: String, @Value("\${storage.s3.secret-key}") private val secretKey: String,
@Value("\${storage.s3.bucket}") private val bucket: 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) val creds = AwsBasicCredentials.create(accessKey, secretKey)
return S3Presigner.builder() S3Presigner.builder()
.endpointOverride(URI.create(endpoint)) .endpointOverride(URI.create(endpoint))
.region(Region.of(region)) .region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(creds)) .credentialsProvider(StaticCredentialsProvider.create(creds))
@ -48,42 +48,32 @@ class FileService(
metadata.updatedBy = SecurityUtils.currentUserId() metadata.updatedBy = SecurityUtils.currentUserId()
repository.save(metadata) repository.save(metadata)
val presigner = presigner() val request = PutObjectRequest.builder()
try { .bucket(bucket)
val request = PutObjectRequest.builder() .key(storageKey)
.bucket(bucket) .contentType(contentType)
.key(storageKey) .build()
.contentType(contentType) val presignRequest = PutObjectPresignRequest.builder()
.build() .signatureDuration(Duration.ofMinutes(15))
val presignRequest = PutObjectPresignRequest.builder() .putObjectRequest(request)
.signatureDuration(Duration.ofMinutes(15)) .build()
.putObjectRequest(request) val presigned = presigner.presignPutObject(presignRequest)
.build() return PresignResponse(fileId, presigned.url().toString())
val presigned = presigner.presignPutObject(presignRequest)
return PresignResponse(fileId, presigned.url().toString())
} finally {
presigner.close()
}
} }
fun presignDownload(fileId: String): PresignResponse { fun presignDownload(fileId: String): PresignResponse {
val tenantId = TenantContext.getTenantId() ?: throw IllegalStateException("Missing tenant") val tenantId = TenantContext.getTenantId() ?: throw IllegalStateException("Missing tenant")
val metadata = repository.findByIdAndTenantId(fileId, tenantId) ?: throw IllegalArgumentException("File not found") val metadata = repository.findByIdAndTenantId(fileId, tenantId) ?: throw IllegalArgumentException("File not found")
val presigner = presigner() val request = GetObjectRequest.builder()
try { .bucket(bucket)
val request = GetObjectRequest.builder() .key(metadata.storageKey)
.bucket(bucket) .build()
.key(metadata.storageKey) val presignRequest = GetObjectPresignRequest.builder()
.build() .signatureDuration(Duration.ofMinutes(15))
val presignRequest = GetObjectPresignRequest.builder() .getObjectRequest(request)
.signatureDuration(Duration.ofMinutes(15)) .build()
.getObjectRequest(request) val presigned = presigner.presignGetObject(presignRequest)
.build() return PresignResponse(metadata.id, presigned.url().toString())
val presigned = presigner.presignGetObject(presignRequest)
return PresignResponse(metadata.id, presigned.url().toString())
} finally {
presigner.close()
}
} }
} }

View file

@ -9,6 +9,4 @@ plugins {
dependencies { dependencies {
api(project(":common")) api(project(":common"))
implementation(project(":modules:audit")) implementation(project(":modules:audit"))
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
} }