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.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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue