Skip to main content

sbol_db_postgres/repo/
tokens.rs

1//! The API-token store over Postgres.
2//!
3//! Tokens are persisted only as their hash (the application layer hashes the
4//! plaintext), so a leaked row cannot be replayed.
5
6use async_trait::async_trait;
7use sbol_db_core::{DomainError, UserId};
8use sbol_db_storage::TokenStore;
9use sqlx::Row;
10use uuid::Uuid;
11
12use crate::repo::db_err;
13use crate::PgPool;
14
15#[derive(Clone)]
16pub struct PgTokenStore {
17    pool: PgPool,
18}
19
20impl PgTokenStore {
21    pub fn new(pool: PgPool) -> Self {
22        Self { pool }
23    }
24}
25
26#[async_trait]
27impl TokenStore for PgTokenStore {
28    async fn issue(&self, token_hash: &str, user_id: UserId) -> Result<(), DomainError> {
29        sqlx::query("INSERT INTO sbh_api_token (token_hash, user_id) VALUES ($1, $2)")
30            .bind(token_hash)
31            .bind(user_id.as_uuid())
32            .execute(&self.pool)
33            .await
34            .map_err(db_err)?;
35        Ok(())
36    }
37
38    async fn resolve(&self, token_hash: &str) -> Result<Option<UserId>, DomainError> {
39        let row = sqlx::query("SELECT user_id FROM sbh_api_token WHERE token_hash = $1")
40            .bind(token_hash)
41            .fetch_optional(&self.pool)
42            .await
43            .map_err(db_err)?;
44        match row {
45            Some(r) => {
46                let user_id: Uuid = r.try_get("user_id").map_err(db_err)?;
47                Ok(Some(UserId(user_id)))
48            }
49            None => Ok(None),
50        }
51    }
52
53    async fn revoke(&self, token_hash: &str) -> Result<bool, DomainError> {
54        let res = sqlx::query("DELETE FROM sbh_api_token WHERE token_hash = $1")
55            .bind(token_hash)
56            .execute(&self.pool)
57            .await
58            .map_err(db_err)?;
59        Ok(res.rows_affected() == 1)
60    }
61}