zino_orm/
helper.rs

1use super::Schema;
2use std::fmt::Display;
3use zino_core::{
4    LazyLock, Map, crypto,
5    encoding::base64,
6    error::Error,
7    extension::{JsonObjectExt, TomlTableExt},
8    state::State,
9    warn,
10};
11
12/// Helper utilities for models.
13pub trait ModelHelper<K>: Schema<PrimaryKey = K>
14where
15    K: Default + Display + PartialEq,
16{
17    /// Returns the secret key for the model.
18    /// It should have at least 64 bytes.
19    ///
20    /// # Note
21    ///
22    /// This should only be used for internal services. Do not expose it to external users.
23    #[inline]
24    fn secret_key() -> &'static [u8] {
25        SECRET_KEY.as_slice()
26    }
27
28    /// Encrypts the password for the model.
29    fn encrypt_password(password: &str) -> Result<String, Error> {
30        let key = Self::secret_key();
31        let password = password.as_bytes();
32        if base64::decode(password).is_ok_and(|bytes| bytes.len() == 256) {
33            crypto::encrypt_hashed_password(password, key)
34                .map_err(|err| warn!("fail to encrypt hashed password: {}", err.message()))
35        } else {
36            crypto::encrypt_raw_password(password, key)
37                .map_err(|err| warn!("fail to encrypt raw password: {}", err.message()))
38        }
39    }
40
41    /// Verifies the password for the model.
42    fn verify_password(password: &str, encrypted_password: &str) -> Result<bool, Error> {
43        let key = Self::secret_key();
44        let password = password.as_bytes();
45        let encrypted_password = encrypted_password.as_bytes();
46        if base64::decode(password).is_ok_and(|bytes| bytes.len() == 256) {
47            crypto::verify_hashed_password(password, encrypted_password, key)
48                .map_err(|err| warn!("fail to verify hashed password: {}", err.message()))
49        } else {
50            crypto::verify_raw_password(password, encrypted_password, key)
51                .map_err(|err| warn!("fail to verify raw password: {}", err.message()))
52        }
53    }
54
55    /// Translates the model data.
56    fn translate_model(model: &mut Map) {
57        #[cfg(feature = "openapi")]
58        zino_openapi::translate_model_entry(model, Self::model_name());
59        for col in Self::columns() {
60            if let Some(translated_field) = col.extra().get_str("translate_as") {
61                let field = [col.name(), "_translated"].concat();
62                if let Some(value) = model.remove(&field) {
63                    model.upsert(translated_field, value);
64                }
65            }
66        }
67    }
68}
69
70impl<M, K> ModelHelper<K> for M
71where
72    M: Schema<PrimaryKey = K>,
73    K: Default + Display + PartialEq,
74{
75}
76
77/// Secret key.
78static SECRET_KEY: LazyLock<[u8; 64]> = LazyLock::new(|| {
79    let app_config = State::shared().config();
80    let config = app_config.get_table("database").unwrap_or(app_config);
81    let checksum: [u8; 32] = config
82        .get_str("checksum")
83        .and_then(|checksum| checksum.as_bytes().try_into().ok())
84        .unwrap_or_else(|| {
85            let secret = config
86                .get_str("secret")
87                .map(|s| s.to_owned())
88                .unwrap_or_else(|| {
89                    tracing::warn!("auto-generated `secret` is used for deriving a secret key");
90                    format!("{}{}", *super::TABLE_PREFIX, super::DRIVER_NAME)
91                });
92            crypto::digest(secret.as_bytes())
93        });
94    let info = config.get_str("info").unwrap_or("ZINO:ORM");
95    crypto::derive_key(info, &checksum)
96});