Skip to main content

systemprompt_users/repository/
device_cert.rs

1//! Device-certificate persistence on the user repository.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use systemprompt_identifiers::{DeviceCertId, UserId};
7
8use crate::error::Result;
9use crate::models::{UserDeviceCert, UserDeviceCertRow};
10use crate::repository::UserRepository;
11
12#[derive(Debug)]
13pub struct EnrollDeviceCertParams<'a> {
14    pub id: &'a DeviceCertId,
15    pub user_id: &'a UserId,
16    pub fingerprint: &'a str,
17    pub label: &'a str,
18}
19
20impl UserRepository {
21    pub async fn enroll_device_cert(
22        &self,
23        params: EnrollDeviceCertParams<'_>,
24    ) -> Result<UserDeviceCert> {
25        let row = sqlx::query_as!(
26            UserDeviceCertRow,
27            r#"
28            INSERT INTO user_device_certs (id, user_id, fingerprint, label)
29            VALUES ($1, $2, $3, $4)
30            RETURNING id, user_id, fingerprint, label, enrolled_at, revoked_at
31            "#,
32            params.id.as_str(),
33            params.user_id.as_str(),
34            params.fingerprint,
35            params.label,
36        )
37        .fetch_one(&*self.write_pool)
38        .await
39        .map(UserDeviceCert::from)?;
40        Ok(row)
41    }
42
43    pub async fn find_active_device_cert_by_fingerprint(
44        &self,
45        fingerprint: &str,
46    ) -> Result<Option<UserDeviceCert>> {
47        let row = sqlx::query_as!(
48            UserDeviceCertRow,
49            r#"
50            SELECT id, user_id, fingerprint, label, enrolled_at, revoked_at
51            FROM user_device_certs
52            WHERE fingerprint = $1 AND revoked_at IS NULL
53            "#,
54            fingerprint,
55        )
56        .fetch_optional(&*self.write_pool)
57        .await
58        .map(|row| row.map(UserDeviceCert::from))?;
59        Ok(row)
60    }
61
62    pub async fn list_device_certs_for_user(
63        &self,
64        user_id: &UserId,
65    ) -> Result<Vec<UserDeviceCert>> {
66        let rows = sqlx::query_as!(
67            UserDeviceCertRow,
68            r#"
69            SELECT id, user_id, fingerprint, label, enrolled_at, revoked_at
70            FROM user_device_certs
71            WHERE user_id = $1
72            ORDER BY enrolled_at DESC
73            "#,
74            user_id.as_str(),
75        )
76        .fetch_all(&*self.write_pool)
77        .await
78        .map(|rows| {
79            rows.into_iter()
80                .map(UserDeviceCert::from)
81                .collect::<Vec<_>>()
82        })?;
83        Ok(rows)
84    }
85
86    pub async fn revoke_device_cert(&self, id: &DeviceCertId, user_id: &UserId) -> Result<bool> {
87        let result = sqlx::query!(
88            r#"
89            UPDATE user_device_certs
90            SET revoked_at = CURRENT_TIMESTAMP
91            WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
92            "#,
93            id.as_str(),
94            user_id.as_str(),
95        )
96        .execute(&*self.write_pool)
97        .await?;
98        Ok(result.rows_affected() > 0)
99    }
100}