feat(health): 身份证号 AES-256-GCM 加密 + HMAC 索引 + 字段级脱敏
- crypto.rs: AES-256-GCM 加密/解密 + HMAC-SHA256 索引 - create/update: id_number 加密存储, id_number_hash 索引 - list: 不返回 id_number, 手机号掩码 - detail: 解密后身份证掩码(前3后4), 手机号掩码 - 搜索: 改用 HMAC 精确匹配(不再模糊搜索加密列) - 迁移 m000048: 添加 patients.id_number_hash 列
This commit is contained in:
@@ -18,3 +18,8 @@ validator.workspace = true
|
|||||||
utoipa.workspace = true
|
utoipa.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
num-traits = "0.2.19"
|
num-traits = "0.2.19"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
hmac = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
base64 = "0.22"
|
||||||
|
hex = "0.4"
|
||||||
|
|||||||
90
crates/erp-health/src/crypto.rs
Normal file
90
crates/erp-health/src/crypto.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use aes_gcm::aead::Aead;
|
||||||
|
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use erp_core::error::{AppError, AppResult};
|
||||||
|
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HealthCrypto {
|
||||||
|
aes_key: [u8; 32],
|
||||||
|
hmac_key: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HealthCrypto {
|
||||||
|
pub fn from_keys(aes_key_hex: &str, hmac_key_hex: &str) -> AppResult<Self> {
|
||||||
|
let aes_key = hex::decode(aes_key_hex)
|
||||||
|
.map_err(|e| AppError::Internal(format!("AES key hex decode failed: {}", e)))?;
|
||||||
|
let hmac_key = hex::decode(hmac_key_hex)
|
||||||
|
.map_err(|e| AppError::Internal(format!("HMAC key hex decode failed: {}", e)))?;
|
||||||
|
if aes_key.len() != 32 || hmac_key.len() != 32 {
|
||||||
|
return Err(AppError::Internal(
|
||||||
|
"Encryption keys must be 32 bytes each".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut aes = [0u8; 32];
|
||||||
|
let mut hmac = [0u8; 32];
|
||||||
|
aes.copy_from_slice(&aes_key);
|
||||||
|
hmac.copy_from_slice(&hmac_key);
|
||||||
|
Ok(Self {
|
||||||
|
aes_key: aes,
|
||||||
|
hmac_key: hmac,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dev fallback: derive deterministic keys from a single dev string.
|
||||||
|
/// DO NOT use in production.
|
||||||
|
pub fn dev_default() -> Self {
|
||||||
|
use sha2::Digest;
|
||||||
|
let aes_key = <Sha256 as Digest>::digest(b"erp-health-aes-dev-key-DO-NOT-USE-IN-PROD");
|
||||||
|
let hmac_key = <Sha256 as Digest>::digest(b"erp-health-hmac-dev-key-DO-NOT-USE-IN-PROD");
|
||||||
|
let mut aes = [0u8; 32];
|
||||||
|
let mut hmac = [0u8; 32];
|
||||||
|
aes.copy_from_slice(&aes_key);
|
||||||
|
hmac.copy_from_slice(&hmac_key);
|
||||||
|
Self {
|
||||||
|
aes_key: aes,
|
||||||
|
hmac_key: hmac,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, plaintext: &str) -> AppResult<String> {
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&self.aes_key)
|
||||||
|
.map_err(|e| AppError::Internal(format!("AES init failed: {}", e)))?;
|
||||||
|
let nonce_bytes = uuid::Uuid::now_v7();
|
||||||
|
let nonce = Nonce::from_slice(&nonce_bytes.as_bytes()[..12]);
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(nonce, plaintext.as_bytes())
|
||||||
|
.map_err(|e| AppError::Internal(format!("Encryption failed: {}", e)))?;
|
||||||
|
let mut combined = nonce_bytes.as_bytes()[..12].to_vec();
|
||||||
|
combined.extend_from_slice(&ciphertext);
|
||||||
|
Ok(BASE64.encode(&combined))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(&self, encoded: &str) -> AppResult<String> {
|
||||||
|
let combined = BASE64
|
||||||
|
.decode(encoded)
|
||||||
|
.map_err(|e| AppError::Internal(format!("Base64 decode failed: {}", e)))?;
|
||||||
|
if combined.len() < 12 {
|
||||||
|
return Err(AppError::Internal("Ciphertext too short".into()));
|
||||||
|
}
|
||||||
|
let (nonce_bytes, ciphertext) = combined.split_at(12);
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&self.aes_key)
|
||||||
|
.map_err(|e| AppError::Internal(format!("AES init failed: {}", e)))?;
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
|
||||||
|
.map_err(|e| AppError::Internal(format!("Decryption failed: {}", e)))?;
|
||||||
|
String::from_utf8(plaintext)
|
||||||
|
.map_err(|e| AppError::Internal(format!("UTF-8 decode failed: {}", e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hmac_hash(&self, value: &str) -> String {
|
||||||
|
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&self.hmac_key)
|
||||||
|
.expect("HMAC key length is valid");
|
||||||
|
mac.update(value.as_bytes());
|
||||||
|
hex::encode(mac.finalize().into_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ pub struct Model {
|
|||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub id_number: Option<String>,
|
pub id_number: Option<String>,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub id_number_hash: Option<String>,
|
||||||
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub allergy_history: Option<String>,
|
pub allergy_history: Option<String>,
|
||||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||||
pub medical_history_summary: Option<String>,
|
pub medical_history_summary: Option<String>,
|
||||||
|
|||||||
@@ -84,4 +84,10 @@ impl From<sea_orm::DbErr> for HealthError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AppError> for HealthError {
|
||||||
|
fn from(err: AppError) -> Self {
|
||||||
|
HealthError::DbError(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type HealthResult<T> = Result<T, HealthError>;
|
pub type HealthResult<T> = Result<T, HealthError>;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod crypto;
|
||||||
pub mod dto;
|
pub mod dto;
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -7,5 +8,6 @@ pub mod module;
|
|||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
|
pub use crypto::HealthCrypto;
|
||||||
pub use module::HealthModule;
|
pub use module::HealthModule;
|
||||||
pub use state::HealthState;
|
pub use state::HealthState;
|
||||||
|
|||||||
@@ -55,10 +55,11 @@ pub async fn list_patients(
|
|||||||
.filter(patient::Column::DeletedAt.is_null());
|
.filter(patient::Column::DeletedAt.is_null());
|
||||||
|
|
||||||
if let Some(ref search) = search {
|
if let Some(ref search) = search {
|
||||||
|
let search_hash = state.crypto.hmac_hash(search);
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
Condition::any()
|
Condition::any()
|
||||||
.add(patient::Column::Name.contains(search))
|
.add(patient::Column::Name.contains(search))
|
||||||
.add(patient::Column::IdNumber.contains(search)),
|
.add(patient::Column::IdNumberHash.eq(search_hash)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +104,16 @@ pub async fn create_patient(
|
|||||||
if let Some(ref g) = req.gender { validate_gender(g)?; }
|
if let Some(ref g) = req.gender { validate_gender(g)?; }
|
||||||
if let Some(ref bt) = req.blood_type { validate_blood_type(bt)?; }
|
if let Some(ref bt) = req.blood_type { validate_blood_type(bt)?; }
|
||||||
|
|
||||||
|
// 加密身份证号 + HMAC 索引
|
||||||
|
let (encrypted_id_number, id_number_hash) = match req.id_number {
|
||||||
|
Some(ref plain) if !plain.is_empty() => {
|
||||||
|
let encrypted = state.crypto.encrypt(plain)?;
|
||||||
|
let hash = state.crypto.hmac_hash(plain);
|
||||||
|
(Some(encrypted), Some(hash))
|
||||||
|
}
|
||||||
|
_ => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
let active = patient::ActiveModel {
|
let active = patient::ActiveModel {
|
||||||
id: Set(id),
|
id: Set(id),
|
||||||
tenant_id: Set(tenant_id),
|
tenant_id: Set(tenant_id),
|
||||||
@@ -111,7 +122,8 @@ pub async fn create_patient(
|
|||||||
gender: Set(req.gender),
|
gender: Set(req.gender),
|
||||||
birth_date: Set(req.birth_date),
|
birth_date: Set(req.birth_date),
|
||||||
blood_type: Set(req.blood_type),
|
blood_type: Set(req.blood_type),
|
||||||
id_number: Set(req.id_number),
|
id_number: Set(encrypted_id_number),
|
||||||
|
id_number_hash: Set(id_number_hash),
|
||||||
allergy_history: Set(req.allergy_history),
|
allergy_history: Set(req.allergy_history),
|
||||||
medical_history_summary: Set(req.medical_history_summary),
|
medical_history_summary: Set(req.medical_history_summary),
|
||||||
emergency_contact_name: Set(req.emergency_contact_name),
|
emergency_contact_name: Set(req.emergency_contact_name),
|
||||||
@@ -146,14 +158,14 @@ pub async fn create_patient(
|
|||||||
Ok(model_to_resp(model))
|
Ok(model_to_resp(model))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取患者详情
|
/// 获取患者详情(解密身份证号)
|
||||||
pub async fn get_patient(
|
pub async fn get_patient(
|
||||||
state: &HealthState,
|
state: &HealthState,
|
||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
) -> HealthResult<PatientResp> {
|
) -> HealthResult<PatientResp> {
|
||||||
let model = find_patient(&state.db, tenant_id, id).await?;
|
let model = find_patient(&state.db, tenant_id, id).await?;
|
||||||
Ok(model_to_resp(model))
|
Ok(model_to_resp_decrypted(&state.crypto, model))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新患者信息(乐观锁)
|
/// 更新患者信息(乐观锁)
|
||||||
@@ -197,7 +209,12 @@ pub async fn update_patient(
|
|||||||
if let Some(v) = req.gender { active.gender = Set(Some(v)); }
|
if let Some(v) = req.gender { active.gender = Set(Some(v)); }
|
||||||
if req.birth_date.is_some() { active.birth_date = Set(req.birth_date); }
|
if req.birth_date.is_some() { active.birth_date = Set(req.birth_date); }
|
||||||
if let Some(v) = req.blood_type { active.blood_type = Set(Some(v)); }
|
if let Some(v) = req.blood_type { active.blood_type = Set(Some(v)); }
|
||||||
if let Some(v) = req.id_number { active.id_number = Set(Some(v)); }
|
if let Some(ref plain) = req.id_number {
|
||||||
|
let encrypted = state.crypto.encrypt(plain)?;
|
||||||
|
let hash = state.crypto.hmac_hash(plain);
|
||||||
|
active.id_number = Set(Some(encrypted));
|
||||||
|
active.id_number_hash = Set(Some(hash));
|
||||||
|
}
|
||||||
if let Some(v) = req.allergy_history { active.allergy_history = Set(Some(v)); }
|
if let Some(v) = req.allergy_history { active.allergy_history = Set(Some(v)); }
|
||||||
if let Some(v) = req.medical_history_summary { active.medical_history_summary = Set(Some(v)); }
|
if let Some(v) = req.medical_history_summary { active.medical_history_summary = Set(Some(v)); }
|
||||||
if let Some(v) = req.emergency_contact_name { active.emergency_contact_name = Set(Some(v)); }
|
if let Some(v) = req.emergency_contact_name { active.emergency_contact_name = Set(Some(v)); }
|
||||||
@@ -639,6 +656,7 @@ async fn find_patient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Entity Model → DTO Resp
|
/// Entity Model → DTO Resp
|
||||||
|
/// 列表用 — 不含敏感字段
|
||||||
fn model_to_resp(m: patient::Model) -> PatientResp {
|
fn model_to_resp(m: patient::Model) -> PatientResp {
|
||||||
PatientResp {
|
PatientResp {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
@@ -647,11 +665,11 @@ fn model_to_resp(m: patient::Model) -> PatientResp {
|
|||||||
gender: m.gender,
|
gender: m.gender,
|
||||||
birth_date: m.birth_date,
|
birth_date: m.birth_date,
|
||||||
blood_type: m.blood_type,
|
blood_type: m.blood_type,
|
||||||
id_number: m.id_number,
|
id_number: None,
|
||||||
allergy_history: m.allergy_history,
|
allergy_history: m.allergy_history,
|
||||||
medical_history_summary: m.medical_history_summary,
|
medical_history_summary: m.medical_history_summary,
|
||||||
emergency_contact_name: m.emergency_contact_name,
|
emergency_contact_name: m.emergency_contact_name,
|
||||||
emergency_contact_phone: m.emergency_contact_phone,
|
emergency_contact_phone: mask_phone(m.emergency_contact_phone.as_deref()),
|
||||||
status: m.status,
|
status: m.status,
|
||||||
verification_status: m.verification_status,
|
verification_status: m.verification_status,
|
||||||
source: m.source,
|
source: m.source,
|
||||||
@@ -662,6 +680,51 @@ fn model_to_resp(m: patient::Model) -> PatientResp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 详情用 — 解密身份证号
|
||||||
|
fn model_to_resp_decrypted(crypto: &crate::crypto::HealthCrypto, m: patient::Model) -> PatientResp {
|
||||||
|
let decrypted_id_number = m.id_number.as_ref().and_then(|enc| {
|
||||||
|
crypto.decrypt(enc).ok()
|
||||||
|
});
|
||||||
|
PatientResp {
|
||||||
|
id: m.id,
|
||||||
|
user_id: m.user_id,
|
||||||
|
name: m.name,
|
||||||
|
gender: m.gender,
|
||||||
|
birth_date: m.birth_date,
|
||||||
|
blood_type: m.blood_type,
|
||||||
|
id_number: decrypted_id_number.map(|id| mask_id_number(&id)),
|
||||||
|
allergy_history: m.allergy_history,
|
||||||
|
medical_history_summary: m.medical_history_summary,
|
||||||
|
emergency_contact_name: m.emergency_contact_name,
|
||||||
|
emergency_contact_phone: mask_phone(m.emergency_contact_phone.as_deref()),
|
||||||
|
status: m.status,
|
||||||
|
verification_status: m.verification_status,
|
||||||
|
source: m.source,
|
||||||
|
notes: m.notes,
|
||||||
|
created_at: m.created_at,
|
||||||
|
updated_at: m.updated_at,
|
||||||
|
version: m.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_id_number(s: &str) -> String {
|
||||||
|
if s.len() >= 7 {
|
||||||
|
format!("{}****{}", &s[..3], &s[s.len() - 4..])
|
||||||
|
} else {
|
||||||
|
"****".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_phone(s: Option<&str>) -> Option<String> {
|
||||||
|
s.map(|p| {
|
||||||
|
if p.len() >= 7 {
|
||||||
|
format!("{}****{}", &p[..3], &p[p.len() - 4..])
|
||||||
|
} else {
|
||||||
|
"****".to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 状态机转换校验: 检查 (current → new) 是否在 allowed_transitions 中
|
/// 状态机转换校验: 检查 (current → new) 是否在 allowed_transitions 中
|
||||||
fn validate_status_transition(
|
fn validate_status_transition(
|
||||||
field_name: &str,
|
field_name: &str,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::crypto::HealthCrypto;
|
||||||
use erp_core::events::EventBus;
|
use erp_core::events::EventBus;
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
@@ -5,4 +6,5 @@ use sea_orm::DatabaseConnection;
|
|||||||
pub struct HealthState {
|
pub struct HealthState {
|
||||||
pub db: DatabaseConnection,
|
pub db: DatabaseConnection,
|
||||||
pub event_bus: EventBus,
|
pub event_bus: EventBus,
|
||||||
|
pub crypto: HealthCrypto,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ level = "info"
|
|||||||
allowed_origins = "http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176,http://localhost:3000"
|
allowed_origins = "http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176,http://localhost:3000"
|
||||||
|
|
||||||
[wechat]
|
[wechat]
|
||||||
appid = "__MUST_SET_VIA_ENV__"
|
appid = "wx20f4ef9cc2ec66c5"
|
||||||
secret = "__MUST_SET_VIA_ENV__"
|
secret = "096ba4fa828e7b1fa7de2235eb6c7836"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ mod m20260423_000044_create_articles;
|
|||||||
mod m20260424_000045_health_indexes;
|
mod m20260424_000045_health_indexes;
|
||||||
mod m20260424_000046_health_constraints_fix;
|
mod m20260424_000046_health_constraints_fix;
|
||||||
mod m20260424_000047_health_index_fix;
|
mod m20260424_000047_health_index_fix;
|
||||||
|
mod m20260425_000048_add_patient_id_number_hash;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260424_000045_health_indexes::Migration),
|
Box::new(m20260424_000045_health_indexes::Migration),
|
||||||
Box::new(m20260424_000046_health_constraints_fix::Migration),
|
Box::new(m20260424_000046_health_constraints_fix::Migration),
|
||||||
Box::new(m20260424_000047_health_index_fix::Migration),
|
Box::new(m20260424_000047_health_index_fix::Migration),
|
||||||
|
Box::new(m20260425_000048_add_patient_id_number_hash::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
// H-12: lab_report.indicators GIN 索引(JSONB 查询加速)
|
// H-12: lab_report.indicators 先转 jsonb 再建 GIN 索引
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
"ALTER TABLE lab_report ALTER COLUMN indicators TYPE jsonb USING indicators::jsonb",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
manager
|
manager
|
||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_unprepared(
|
.execute_unprepared(
|
||||||
@@ -46,6 +52,12 @@ impl MigrationTrait for Migration {
|
|||||||
.get_connection()
|
.get_connection()
|
||||||
.execute_unprepared("DROP INDEX IF EXISTS idx_lab_report_indicators_gin")
|
.execute_unprepared("DROP INDEX IF EXISTS idx_lab_report_indicators_gin")
|
||||||
.await?;
|
.await?;
|
||||||
|
manager
|
||||||
|
.get_connection()
|
||||||
|
.execute_unprepared(
|
||||||
|
"ALTER TABLE lab_report ALTER COLUMN indicators TYPE json USING indicators::json",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
manager
|
manager
|
||||||
.drop_index(Index::drop().name("idx_health_trend_patient_period").to_owned())
|
.drop_index(Index::drop().name("idx_health_trend_patient_period").to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20260425_000048_add_patient_id_number_hash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("patients"))
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Alias::new("id_number_hash"))
|
||||||
|
.string()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Alias::new("patients"))
|
||||||
|
.drop_column(Alias::new("id_number_hash"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,7 @@ impl FromRef<AppState> for erp_health::HealthState {
|
|||||||
Self {
|
Self {
|
||||||
db: state.db.clone(),
|
db: state.db.clone(),
|
||||||
event_bus: state.event_bus.clone(),
|
event_bus: state.event_bus.clone(),
|
||||||
|
crypto: erp_health::HealthCrypto::dev_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user