systemprompt_users/services/
device_cert_service.rs1use 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}