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::repository::{ArchiveOutcome, ArchiveParams, ArchiveState, PurgeCount};
18
19impl UserService {
20    pub async fn archive(&self, id: &UserId, params: ArchiveParams<'_>) -> Result<ArchiveOutcome> {
21        self.repository.archive(id, params).await
22    }
23
24    pub async fn restore(&self, id: &UserId, window_days: u32) -> Result<()> {
25        if self.repository.restore(id, window_days).await? {
26            return Ok(());
27        }
28        match self.repository.find_archive_state(id).await? {
29            None => Err(UserError::NotFound(id.clone())),
30            Some(_) => Err(UserError::RestoreRefused {
31                id: id.clone(),
32                window_days,
33            }),
34        }
35    }
36
37    pub async fn set_legal_hold(&self, id: &UserId, hold: bool) -> Result<()> {
38        self.repository.set_legal_hold(id, hold).await
39    }
40
41    pub async fn find_archive_state(&self, id: &UserId) -> Result<Option<ArchiveState>> {
42        self.repository.find_archive_state(id).await
43    }
44
45    pub async fn purge(&self, id: &UserId) -> Result<Vec<PurgeCount>> {
46        let state = self
47            .repository
48            .find_archive_state(id)
49            .await?
50            .ok_or_else(|| UserError::NotFound(id.clone()))?;
51        if !state.is_archived() {
52            return Err(UserError::NotArchived(id.clone()));
53        }
54        if state.legal_hold {
55            return Err(UserError::LegalHold(id.clone()));
56        }
57        self.repository.delete(id).await
58    }
59
60    pub async fn purge_expired_archives(
61        &self,
62        window_days: u32,
63        limit: i64,
64    ) -> Result<Vec<UserId>> {
65        let ids = self
66            .repository
67            .list_purgeable_archives(window_days, limit)
68            .await?;
69        let mut purged = Vec::with_capacity(ids.len());
70        for id in ids {
71            self.purge(&id).await?;
72            purged.push(id);
73        }
74        Ok(purged)
75    }
76
77    pub async fn list_purgeable_archives(
78        &self,
79        window_days: u32,
80        limit: i64,
81    ) -> Result<Vec<UserId>> {
82        self.repository
83            .list_purgeable_archives(window_days, limit)
84            .await
85    }
86}