Skip to main content

systemprompt_users/repository/
api_key.rs

1//! API-key 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 chrono::{DateTime, Utc};
7use systemprompt_identifiers::{ApiKeyId, UserId};
8
9use crate::error::Result;
10use crate::models::UserApiKey;
11use crate::repository::UserRepository;
12
13pub struct CreateApiKeyParams<'a> {
14    pub id: &'a ApiKeyId,
15    pub user_id: &'a UserId,
16    pub name: &'a str,
17    pub key_prefix: &'a str,
18    pub key_hash: &'a str,
19    pub expires_at: Option<DateTime<Utc>>,
20}
21
22impl UserRepository {
23    pub async fn create_api_key(&self, params: CreateApiKeyParams<'_>) -> Result<UserApiKey> {
24        let row = sqlx::query_as!(
25            UserApiKey,
26            r#"
27            INSERT INTO user_api_keys
28                (id, user_id, name, key_prefix, key_hash, expires_at)
29            VALUES ($1, $2, $3, $4, $5, $6)
30            RETURNING id, user_id, name, key_prefix, key_hash,
31                      created_at, last_used_at, expires_at, revoked_at
32            "#,
33            params.id.as_str(),
34            params.user_id.as_str(),
35            params.name,
36            params.key_prefix,
37            params.key_hash,
38            params.expires_at,
39        )
40        .fetch_one(&*self.write_pool)
41        .await?;
42        Ok(row)
43    }
44
45    pub async fn find_active_api_key_by_prefix(
46        &self,
47        key_prefix: &str,
48    ) -> Result<Option<UserApiKey>> {
49        let row = sqlx::query_as!(
50            UserApiKey,
51            r#"
52            SELECT id, user_id, name, key_prefix, key_hash,
53                   created_at, last_used_at, expires_at, revoked_at
54            FROM user_api_keys
55            WHERE key_prefix = $1
56              AND revoked_at IS NULL
57            "#,
58            key_prefix,
59        )
60        .fetch_optional(&*self.pool)
61        .await?;
62        Ok(row)
63    }
64
65    pub async fn list_api_keys_for_user(&self, user_id: &UserId) -> Result<Vec<UserApiKey>> {
66        let rows = sqlx::query_as!(
67            UserApiKey,
68            r#"
69            SELECT id, user_id, name, key_prefix, key_hash,
70                   created_at, last_used_at, expires_at, revoked_at
71            FROM user_api_keys
72            WHERE user_id = $1
73            ORDER BY created_at DESC
74            "#,
75            user_id.as_str(),
76        )
77        .fetch_all(&*self.pool)
78        .await?;
79        Ok(rows)
80    }
81
82    pub async fn revoke_api_key(&self, id: &ApiKeyId, user_id: &UserId) -> Result<bool> {
83        let result = sqlx::query!(
84            r#"
85            UPDATE user_api_keys
86            SET revoked_at = CURRENT_TIMESTAMP
87            WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
88            "#,
89            id.as_str(),
90            user_id.as_str(),
91        )
92        .execute(&*self.write_pool)
93        .await?;
94        Ok(result.rows_affected() > 0)
95    }
96
97    pub async fn list_revoked_api_key_ids_for_user(&self, user_id: &UserId) -> Result<Vec<String>> {
98        let rows = sqlx::query_scalar!(
99            r#"
100            SELECT id
101            FROM user_api_keys
102            WHERE user_id = $1 AND revoked_at IS NOT NULL
103            ORDER BY revoked_at DESC
104            "#,
105            user_id.as_str(),
106        )
107        .fetch_all(&*self.pool)
108        .await?;
109        Ok(rows)
110    }
111
112    pub async fn touch_api_key_usage(&self, id: &ApiKeyId) -> Result<()> {
113        sqlx::query!(
114            r#"
115            UPDATE user_api_keys
116            SET last_used_at = CURRENT_TIMESTAMP
117            WHERE id = $1
118            "#,
119            id.as_str(),
120        )
121        .execute(&*self.write_pool)
122        .await?;
123        Ok(())
124    }
125}