cloude review
This commit is contained in:
parent
195ca7d961
commit
ca166eb661
|
|
@ -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<LocalAuthFilter>): 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue