Skip to main content

systemprompt_users/services/
device_cert_service.rs

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