Skip to main content

platform/realm/
model.rs

1//! Realm 核心数据结构与数据库操作
2//!
3//! 定义 Realm 实体的核心数据结构、数据库 CRUD 操作
4
5use chrono::Utc;
6use serde::{Deserialize, Serialize};
7use std::str::FromStr;
8use strum::{Display, EnumString};
9
10use super::error::RealmError;
11use crate::storage::db::get_database;
12
13/// Realm 状态枚举
14#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Display, EnumString)]
15pub enum RealmStatus {
16    #[default]
17    Active,
18    Inactive,
19    Suspended,
20}
21
22/// Realm 是用于分离不同应用程序资源的虚拟概念。
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Realm {
25    /// DB 自增主键,起点 2^25 = 33554432
26    pub id: u32,
27    pub name: String,
28    pub status: RealmStatus,
29    pub enabled: bool,
30    pub expires_at: Option<u64>,
31    pub created_at: u64,
32    pub updated_at: Option<u64>,
33    /// SHA256 hash of current secret (必填)
34    pub secret_current: String,
35    /// (hash, valid_until) for previous secret during rotation grace window
36    pub secret_previous: Option<(String, u64)>,
37}
38
39impl Default for Realm {
40    fn default() -> Self {
41        let now = Utc::now().timestamp() as u64;
42        Self {
43            id: 0,
44            name: String::new(),
45            status: RealmStatus::Active,
46            enabled: true,
47            expires_at: None,
48            created_at: now,
49            updated_at: None,
50            secret_current: String::new(),
51            secret_previous: None,
52        }
53    }
54}
55
56impl<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow> for Realm {
57    fn from_row(row: &'r sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
58        use sqlx::Row;
59        let id: i64 = row.try_get("id")?;
60        let name: String = row.try_get("name")?;
61        let status_str: String = row.try_get("status")?;
62        let enabled: bool = row.try_get::<i32, _>("enabled")? != 0;
63        let expires_at: Option<i64> = row.try_get("expires_at")?;
64        let created_at: i64 = row.try_get("created_at")?;
65        let updated_at: Option<i64> = row.try_get("updated_at")?;
66        let secret_current: String = row.try_get("secret_current")?;
67        let secret_previous_hash: Option<String> = row.try_get("secret_previous_hash")?;
68        let secret_previous_valid_until: Option<i64> =
69            row.try_get("secret_previous_valid_until")?;
70
71        let secret_previous = match (secret_previous_hash, secret_previous_valid_until) {
72            (Some(hash), Some(valid_until)) if !hash.is_empty() => Some((hash, valid_until as u64)),
73            _ => None,
74        };
75
76        Ok(Self {
77            id: id as u32,
78            name,
79            status: RealmStatus::from_str(&status_str).unwrap_or_default(),
80            enabled,
81            expires_at: expires_at.map(|v| v as u64),
82            created_at: created_at as u64,
83            updated_at: updated_at.map(|v| v as u64),
84            secret_current,
85            secret_previous,
86        })
87    }
88}
89
90impl Realm {
91    /// Create a new realm in the database.
92    ///
93    /// `secret_hash` is the SHA256 hash of the realm secret (must be non-empty).
94    /// Returns the Realm with its auto-generated `id`.
95    pub async fn create(name: String, secret_hash: String) -> Result<Self, RealmError> {
96        let db = get_database();
97        let pool = db.get_pool();
98        let now = Utc::now().timestamp();
99
100        let result = sqlx::query(
101            "INSERT INTO realm (name, status, enabled, created_at, secret_current)
102             VALUES (?, 'Active', 1, ?, ?)",
103        )
104        .bind(&name)
105        .bind(now)
106        .bind(&secret_hash)
107        .execute(pool)
108        .await?;
109
110        let id = result.last_insert_rowid() as u32;
111
112        Ok(Self {
113            id,
114            name,
115            status: RealmStatus::Active,
116            enabled: true,
117            expires_at: None,
118            created_at: now as u64,
119            updated_at: None,
120            secret_current: secret_hash,
121            secret_previous: None,
122        })
123    }
124
125    /// Upsert a superv-managed realm with an externally assigned id.
126    pub async fn upsert_managed(
127        id: u32,
128        name: String,
129        status: RealmStatus,
130        enabled: bool,
131        expires_at: Option<u64>,
132        secret_current: String,
133        secret_previous: Option<(String, u64)>,
134    ) -> Result<Self, RealmError> {
135        if id == 0 {
136            return Err(RealmError::ValidationError(
137                "managed realm id must be greater than 0".to_string(),
138            ));
139        }
140        if secret_current.trim().is_empty() {
141            return Err(RealmError::ValidationError(
142                "managed realm secret_current must not be empty".to_string(),
143            ));
144        }
145
146        let db = get_database();
147        let pool = db.get_pool();
148        let now = Utc::now().timestamp();
149        let (prev_hash, prev_valid_until) = match &secret_previous {
150            Some((hash, valid_until)) if !hash.trim().is_empty() => {
151                (Some(hash.as_str()), Some(*valid_until as i64))
152            }
153            _ => (None, None),
154        };
155
156        sqlx::query(
157            "INSERT INTO realm (
158                id, name, status, enabled, expires_at, created_at, updated_at,
159                secret_current, secret_previous_hash, secret_previous_valid_until
160             )
161             VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
162             ON CONFLICT(id) DO UPDATE SET
163                name = excluded.name,
164                status = excluded.status,
165                enabled = excluded.enabled,
166                expires_at = excluded.expires_at,
167                updated_at = ?,
168                secret_current = excluded.secret_current,
169                secret_previous_hash = excluded.secret_previous_hash,
170                secret_previous_valid_until = excluded.secret_previous_valid_until",
171        )
172        .bind(id as i64)
173        .bind(&name)
174        .bind(status.to_string())
175        .bind(enabled as i32)
176        .bind(expires_at.map(|v| v as i64))
177        .bind(now)
178        .bind(&secret_current)
179        .bind(prev_hash)
180        .bind(prev_valid_until)
181        .bind(now)
182        .execute(pool)
183        .await?;
184
185        Self::get(id).await?.ok_or(RealmError::NotFound)
186    }
187
188    /// Save (UPDATE) an existing realm to the database.
189    pub async fn save(&mut self) -> Result<(), RealmError> {
190        let db = get_database();
191        let pool = db.get_pool();
192        let now = Utc::now().timestamp();
193        self.updated_at = Some(now as u64);
194
195        let (prev_hash, prev_valid_until) = match &self.secret_previous {
196            Some((hash, valid_until)) => (Some(hash.as_str()), Some(*valid_until as i64)),
197            None => (None, None),
198        };
199
200        sqlx::query(
201            "UPDATE realm SET name = ?, status = ?, enabled = ?, expires_at = ?,
202             updated_at = ?, secret_current = ?,
203             secret_previous_hash = ?, secret_previous_valid_until = ?
204             WHERE id = ?",
205        )
206        .bind(&self.name)
207        .bind(self.status.to_string())
208        .bind(self.enabled as i32)
209        .bind(self.expires_at.map(|v| v as i64))
210        .bind(now)
211        .bind(&self.secret_current)
212        .bind(prev_hash)
213        .bind(prev_valid_until)
214        .bind(self.id as i64)
215        .execute(pool)
216        .await?;
217
218        Ok(())
219    }
220
221    /// Get a realm by its auto-increment id.
222    pub async fn get(id: u32) -> Result<Option<Self>, RealmError> {
223        let db = get_database();
224        let pool = db.get_pool();
225
226        let result = sqlx::query_as::<_, Realm>(
227            "SELECT id, name, status, enabled, expires_at, created_at, updated_at,
228                    secret_current, secret_previous_hash, secret_previous_valid_until
229             FROM realm WHERE id = ?",
230        )
231        .bind(id as i64)
232        .fetch_optional(pool)
233        .await?;
234
235        Ok(result)
236    }
237
238    /// Get a realm by name.
239    pub async fn get_by_name(name: &str) -> Result<Option<Self>, RealmError> {
240        let db = get_database();
241        let pool = db.get_pool();
242
243        let result = sqlx::query_as::<_, Realm>(
244            "SELECT id, name, status, enabled, expires_at, created_at, updated_at,
245                    secret_current, secret_previous_hash, secret_previous_valid_until
246             FROM realm WHERE name = ?",
247        )
248        .bind(name)
249        .fetch_optional(pool)
250        .await?;
251
252        Ok(result)
253    }
254
255    /// Get all realms.
256    pub async fn get_all() -> Result<Vec<Self>, RealmError> {
257        let db = get_database();
258        let pool = db.get_pool();
259
260        let realms = sqlx::query_as::<_, Realm>(
261            "SELECT id, name, status, enabled, expires_at, created_at, updated_at,
262                    secret_current, secret_previous_hash, secret_previous_valid_until
263             FROM realm",
264        )
265        .fetch_all(pool)
266        .await?;
267
268        Ok(realms)
269    }
270
271    /// Delete a realm by id.
272    pub async fn delete(id: u32) -> Result<u64, RealmError> {
273        let db = get_database();
274        let pool = db.get_pool();
275
276        let result = sqlx::query("DELETE FROM realm WHERE id = ?")
277            .bind(id as i64)
278            .execute(pool)
279            .await?;
280
281        Ok(result.rows_affected())
282    }
283
284    /// Disable a realm without physically removing it.
285    pub async fn soft_delete(id: u32) -> Result<bool, RealmError> {
286        let Some(mut realm) = Self::get(id).await? else {
287            return Ok(false);
288        };
289
290        realm.enabled = false;
291        realm.status = RealmStatus::Inactive;
292        realm.save().await?;
293        Ok(true)
294    }
295
296    pub fn is_expired(&self) -> bool {
297        if let Some(expires_at) = self.expires_at {
298            let now = Utc::now().timestamp() as u64;
299            now > expires_at
300        } else {
301            false
302        }
303    }
304
305    pub fn is_active(&self) -> bool {
306        self.status == RealmStatus::Active && self.enabled && !self.is_expired()
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::util::test_utils::utils::setup_test_db;
314    use serial_test::serial;
315
316    #[test]
317    fn test_realm_status_display() {
318        assert_eq!(RealmStatus::Active.to_string(), "Active");
319        assert_eq!(RealmStatus::Inactive.to_string(), "Inactive");
320        assert_eq!(RealmStatus::Suspended.to_string(), "Suspended");
321    }
322
323    #[test]
324    fn test_realm_status_parse() {
325        assert_eq!(
326            RealmStatus::from_str("Active").unwrap(),
327            RealmStatus::Active
328        );
329        assert_eq!(
330            RealmStatus::from_str("Inactive").unwrap(),
331            RealmStatus::Inactive
332        );
333        assert_eq!(
334            RealmStatus::from_str("Suspended").unwrap(),
335            RealmStatus::Suspended
336        );
337    }
338
339    #[test]
340    fn test_realm_default() {
341        let realm = Realm::default();
342        assert_eq!(realm.id, 0);
343        assert_eq!(realm.status, RealmStatus::Active);
344        assert!(realm.enabled);
345        assert!(realm.secret_current.is_empty());
346        assert!(realm.secret_previous.is_none());
347    }
348
349    #[test]
350    fn test_realm_expired() {
351        let mut realm = Realm::default();
352        let past_time = Utc::now().timestamp() as u64 - 3600;
353        realm.expires_at = Some(past_time);
354        assert!(realm.is_expired());
355        assert!(!realm.is_active());
356    }
357
358    #[test]
359    fn test_realm_active() {
360        let mut realm = Realm::default();
361        let future_time = Utc::now().timestamp() as u64 + 3600;
362        realm.expires_at = Some(future_time);
363        assert!(!realm.is_expired());
364        assert!(realm.is_active());
365    }
366
367    #[tokio::test]
368    #[serial]
369    async fn test_upsert_managed_uses_external_id_and_updates() -> anyhow::Result<()> {
370        setup_test_db().await?;
371
372        let realm = Realm::upsert_managed(
373            424_242,
374            "managed-one".to_string(),
375            RealmStatus::Active,
376            true,
377            Some(1_900_000_000),
378            "hash-current".to_string(),
379            None,
380        )
381        .await?;
382        assert_eq!(realm.id, 424_242);
383        assert_eq!(realm.name, "managed-one");
384        assert_eq!(realm.secret_current, "hash-current");
385
386        let updated = Realm::upsert_managed(
387            424_242,
388            "managed-one-renamed".to_string(),
389            RealmStatus::Suspended,
390            false,
391            Some(1_900_000_001),
392            "hash-current-2".to_string(),
393            Some(("hash-prev".to_string(), 1_800_000_000)),
394        )
395        .await?;
396        assert_eq!(updated.id, 424_242);
397        assert_eq!(updated.name, "managed-one-renamed");
398        assert_eq!(updated.status, RealmStatus::Suspended);
399        assert!(!updated.enabled);
400        assert_eq!(updated.secret_current, "hash-current-2");
401        assert_eq!(
402            updated.secret_previous,
403            Some(("hash-prev".to_string(), 1_800_000_000))
404        );
405
406        Ok(())
407    }
408
409    #[tokio::test]
410    #[serial]
411    async fn test_soft_delete_marks_realm_inactive() -> anyhow::Result<()> {
412        setup_test_db().await?;
413
414        Realm::upsert_managed(
415            424_243,
416            "managed-delete".to_string(),
417            RealmStatus::Active,
418            true,
419            None,
420            "delete-current".to_string(),
421            None,
422        )
423        .await?;
424
425        assert!(Realm::soft_delete(424_243).await?);
426        let realm = Realm::get(424_243).await?.expect("realm should remain");
427        assert_eq!(realm.status, RealmStatus::Inactive);
428        assert!(!realm.enabled);
429        assert!(!Realm::soft_delete(999_999).await?);
430
431        Ok(())
432    }
433
434    #[test]
435    fn test_realm_suspended_not_active() {
436        let realm = Realm {
437            status: RealmStatus::Suspended,
438            ..Default::default()
439        };
440        assert!(!realm.is_active());
441    }
442
443    #[test]
444    fn test_realm_disabled_not_active() {
445        let realm = Realm {
446            enabled: false,
447            ..Default::default()
448        };
449        assert!(!realm.is_active());
450    }
451}