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
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            UserDeviceCert,
27            r#"
28            INSERT INTO user_device_certs (id, user_id, fingerprint, label)
29            VALUES ($1, $2, $3, $4)
30            RETURNING id, user_id as "user_id: UserId", 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        Ok(row)
40    }
41
42    pub async fn find_active_device_cert_by_fingerprint(
43        &self,
44        fingerprint: &str,
45    ) -> Result<Option<UserDeviceCert>> {
46        let row = sqlx::query_as!(
47            UserDeviceCert,
48            r#"
49            SELECT id, user_id as "user_id: UserId", fingerprint, label, enrolled_at, revoked_at
50            FROM user_device_certs
51            WHERE fingerprint = $1 AND revoked_at IS NULL
52            "#,
53            fingerprint,
54        )
55        .fetch_optional(&*self.write_pool)
56        .await?;
57        Ok(row)
58    }
59
60    pub async fn list_device_certs_for_user(
61        &self,
62        user_id: &UserId,
63    ) -> Result<Vec<UserDeviceCert>> {
64        let rows = sqlx::query_as!(
65            UserDeviceCert,
66            r#"
67            SELECT id, user_id as "user_id: UserId", fingerprint, label, enrolled_at, revoked_at
68            FROM user_device_certs
69            WHERE user_id = $1
70            ORDER BY enrolled_at DESC
71            "#,
72            user_id.as_str(),
73        )
74        .fetch_all(&*self.write_pool)
75        .await?;
76        Ok(rows)
77    }
78
79    pub async fn revoke_device_cert(&self, id: &DeviceCertId, user_id: &UserId) -> Result<bool> {
80        let result = sqlx::query!(
81            r#"
82            UPDATE user_device_certs
83            SET revoked_at = CURRENT_TIMESTAMP
84            WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
85            "#,
86            id.as_str(),
87            user_id.as_str(),
88        )
89        .execute(&*self.write_pool)
90        .await?;
91        Ok(result.rows_affected() > 0)
92    }
93}