Skip to main content

systemprompt_users/services/user/
archive.rs

1//! Archive, restore, legal hold and the guarded purge (NFR-5.2).
2//!
3//! `archive` is what deleting a user means: the account stops signing in and
4//! leaves every roster, its credentials are revoked, and its history stays.
5//! `purge` is the physical delete, allowed only for an archived account that
6//! is not under legal hold; `database_cleanup` calls it for archives past the
7//! retention window. [`UserService::merge_users`] is unchanged: a merge still
8//! deletes its source once every row has moved.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use systemprompt_identifiers::UserId;
14
15use super::UserService;
16use crate::error::{Result, UserError};
17use crate::models::User;
18use crate::repository::{ArchiveOutcome, ArchiveParams, ArchiveState, PurgeCount};
19
20impl UserService {
21    pub async fn archive(&self, id: &UserId, params: ArchiveParams<'_>) -> Result<ArchiveOutcome> {
22        self.repository.archive(id, params).await
23    }
24
25    pub async fn restore(&self, id: &UserId, window_days: u32) -> Result<()> {
26        if self.repository.restore(id, window_days).await? {
27            return Ok(());
28        }
29        match self.repository.find_archive_state(id).await? {
30            None => Err(UserError::NotFound(id.clone())),
31            Some(_) => Err(UserError::RestoreRefused {
32                id: id.clone(),
33                window_days,
34            }),
35        }
36    }
37
38    pub async fn set_legal_hold(&self, id: &UserId, hold: bool) -> Result<()> {
39        self.repository.set_legal_hold(id, hold).await
40    }
41
42    pub async fn find_by_reference_any_status(&self, reference: &str) -> Result<Option<User>> {
43        self.repository
44            .find_by_reference_any_status(reference)
45            .await
46    }
47
48    pub async fn find_archive_state(&self, id: &UserId) -> Result<Option<ArchiveState>> {
49        self.repository.find_archive_state(id).await
50    }
51
52    pub async fn purge(&self, id: &UserId) -> Result<Vec<PurgeCount>> {
53        let state = self
54            .repository
55            .find_archive_state(id)
56            .await?
57            .ok_or_else(|| UserError::NotFound(id.clone()))?;
58        if !state.is_archived() {
59            return Err(UserError::NotArchived(id.clone()));
60        }
61        if state.legal_hold {
62            return Err(UserError::LegalHold(id.clone()));
63        }
64        self.repository.delete(id).await
65    }
66
67    pub async fn purge_expired_archives(
68        &self,
69        window_days: u32,
70        limit: i64,
71    ) -> Result<Vec<UserId>> {
72        let ids = self
73            .repository
74            .list_purgeable_archives(window_days, limit)
75            .await?;
76        let mut purged = Vec::with_capacity(ids.len());
77        for id in ids {
78            self.purge(&id).await?;
79            purged.push(id);
80        }
81        Ok(purged)
82    }
83
84    pub async fn list_purgeable_archives(
85        &self,
86        window_days: u32,
87        limit: i64,
88    ) -> Result<Vec<UserId>> {
89        self.repository
90            .list_purgeable_archives(window_days, limit)
91            .await
92    }
93}