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