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;
10use crate::repository::UserRepository;
11
12pub struct EnrollDeviceCertParams<'a> {
13    pub id: &'a DeviceCertId,
14    pub user_id: &'a UserId,
15    pub fingerprint: &'a str,
16    pub label: &'a str,
17}
18
19impl UserRepository {
20    pub async fn enroll_device_cert(
21        &self,
22        params: EnrollDeviceCertParams<'_>,
23    ) -> Result<UserDeviceCert> {
24        let row = sqlx::query_as!(
25            UserDeviceCert,
26            r#"
27            INSERT INTO user_device_certs (id, user_id, fingerprint, label)
28            VALUES ($1, $2, $3, $4)
29            RETURNING id, user_id, fingerprint, label, enrolled_at, revoked_at
30            "#,
31            params.id.as_str(),
32            params.user_id.as_str(),
33            params.fingerprint,
34            params.label,
35        )
36        .fetch_one(&*self.write_pool)
37        .await?;
38        Ok(row)
39    }
40
41    pub async fn find_active_device_cert_by_fingerprint(
42        &self,
43        fingerprint: &str,
44    ) -> Result<Option<UserDeviceCert>> {
45        let row = sqlx::query_as!(
46            UserDeviceCert,
47            r#"
48            SELECT id, user_id, fingerprint, label, enrolled_at, revoked_at
49            FROM user_device_certs
50            WHERE fingerprint = $1 AND revoked_at IS NULL
51            "#,
52            fingerprint,
53        )
54        .fetch_optional(&*self.pool)
55        .await?;
56        Ok(row)
57    }
58
59    pub async fn list_device_certs_for_user(
60        &self,
61        user_id: &UserId,
62    ) -> Result<Vec<UserDeviceCert>> {
63        let rows = sqlx::query_as!(
64            UserDeviceCert,
65            r#"
66            SELECT id, user_id, fingerprint, label, enrolled_at, revoked_at
67            FROM user_device_certs
68            WHERE user_id = $1
69            ORDER BY enrolled_at DESC
70            "#,
71            user_id.as_str(),
72        )
73        .fetch_all(&*self.pool)
74        .await?;
75        Ok(rows)
76    }
77
78    pub async fn revoke_device_cert(&self, id: &DeviceCertId, user_id: &UserId) -> Result<bool> {
79        let result = sqlx::query!(
80            r#"
81            UPDATE user_device_certs
82            SET revoked_at = CURRENT_TIMESTAMP
83            WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
84            "#,
85            id.as_str(),
86            user_id.as_str(),
87        )
88        .execute(&*self.write_pool)
89        .await?;
90        Ok(result.rows_affected() > 0)
91    }
92}