Skip to main content

systemprompt_users/services/
device_cert_service.rs

1use systemprompt_database::DbPool;
2use systemprompt_identifiers::{DeviceCertId, UserId};
3
4use crate::error::{Result, UserError};
5use crate::models::UserDeviceCert;
6use crate::repository::{EnrollDeviceCertParams, UserRepository};
7
8const FINGERPRINT_LEN: usize = 64;
9
10#[derive(Debug, Clone)]
11pub struct EnrollParams<'a> {
12    pub user_id: &'a UserId,
13    pub fingerprint: &'a str,
14    pub label: &'a str,
15}
16
17#[derive(Debug, Clone)]
18pub struct DeviceCertService {
19    repository: UserRepository,
20}
21
22impl DeviceCertService {
23    pub fn new(db: &DbPool) -> Result<Self> {
24        Ok(Self {
25            repository: UserRepository::new(db)?,
26        })
27    }
28
29    pub async fn enroll(&self, params: EnrollParams<'_>) -> Result<UserDeviceCert> {
30        let label = params.label.trim();
31        if label.is_empty() {
32            return Err(UserError::Validation(
33                "device cert label must not be empty".into(),
34            ));
35        }
36        let fingerprint = normalize_fingerprint(params.fingerprint)?;
37        let id = DeviceCertId::generate();
38        self.repository
39            .enroll_device_cert(EnrollDeviceCertParams {
40                id: &id,
41                user_id: params.user_id,
42                fingerprint: &fingerprint,
43                label,
44            })
45            .await
46    }
47
48    pub async fn verify(&self, fingerprint: &str) -> Result<Option<UserDeviceCert>> {
49        let normalized = normalize_fingerprint(fingerprint)?;
50        self.repository
51            .find_active_device_cert_by_fingerprint(&normalized)
52            .await
53    }
54
55    pub async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<UserDeviceCert>> {
56        self.repository.list_device_certs_for_user(user_id).await
57    }
58
59    pub async fn revoke(&self, id: &DeviceCertId, user_id: &UserId) -> Result<bool> {
60        self.repository.revoke_device_cert(id, user_id).await
61    }
62}
63
64fn normalize_fingerprint(fingerprint: &str) -> Result<String> {
65    let trimmed = fingerprint.trim().to_ascii_lowercase();
66    if trimmed.len() != FINGERPRINT_LEN {
67        return Err(UserError::Validation(format!(
68            "device cert fingerprint must be {FINGERPRINT_LEN} hex chars (SHA-256)",
69        )));
70    }
71    if !trimmed.bytes().all(|b| b.is_ascii_hexdigit()) {
72        return Err(UserError::Validation(
73            "device cert fingerprint must be hex".into(),
74        ));
75    }
76    Ok(trimmed)
77}