brewlog/src/infrastructure/repositories/tokens.rs
Jon Seager 2bd008a8d7
chore(lint): enable clippy pedantic and restriction lints
- Add [lints.clippy] section to Cargo.toml with pedantic group
- Cherry-pick restriction lints: dbg_macro (deny), todo, unwrap_used, expect_used (warn)
- Allow noisy pedantic lints (missing_errors_doc, module_name_repetitions, etc.)
- Fix unnecessary Result wrappers in token and user repository to_domain functions
- Merge duplicate match arms in TimelineEventViewModel
- Add justified #[allow] attributes for startup code and tests
2026-02-02 17:55:16 +00:00

151 lines
4.7 KiB
Rust

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use sqlx::query_as;
use crate::domain::RepositoryError;
use crate::domain::ids::{TokenId, UserId};
use crate::domain::repositories::TokenRepository;
use crate::domain::tokens::{NewToken, Token};
use crate::infrastructure::database::DatabasePool;
#[derive(Clone)]
pub struct SqlTokenRepository {
pool: DatabasePool,
}
impl SqlTokenRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: TokenRecord) -> Token {
let TokenRecord {
id,
user_id,
token_hash,
name,
created_at,
last_used_at,
revoked_at,
} = record;
Token::new(
TokenId::from(id),
UserId::from(user_id),
token_hash,
name,
created_at,
last_used_at,
revoked_at,
)
}
}
#[async_trait]
impl TokenRepository for SqlTokenRepository {
async fn insert(&self, token: NewToken) -> Result<Token, RepositoryError> {
let query = "INSERT INTO tokens (user_id, token_hash, name) VALUES (?, ?, ?) RETURNING id, user_id, token_hash, name, created_at, last_used_at, revoked_at";
let NewToken {
user_id,
token_hash,
name,
} = token;
let record = query_as::<_, TokenRecord>(query)
.bind(i64::from(user_id))
.bind(&token_hash)
.bind(&name)
.fetch_one(&self.pool)
.await
.map_err(|err| {
if let sqlx::Error::Database(db_err) = &err
&& db_err.is_unique_violation()
{
return RepositoryError::conflict("token already exists");
}
RepositoryError::unexpected(err.to_string())
})?;
Ok(Self::to_domain(record))
}
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError> {
let query = "SELECT id, user_id, token_hash, name, created_at, last_used_at, revoked_at FROM tokens WHERE id = ?";
let record = query_as::<_, TokenRecord>(query)
.bind(i64::from(id))
.fetch_optional(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
}
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError> {
let query = "SELECT id, user_id, token_hash, name, created_at, last_used_at, revoked_at FROM tokens WHERE token_hash = ?";
let record = query_as::<_, TokenRecord>(query)
.bind(token_hash)
.fetch_optional(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
.ok_or(RepositoryError::NotFound)?;
Ok(Self::to_domain(record))
}
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError> {
let query = "SELECT id, user_id, token_hash, name, created_at, last_used_at, revoked_at FROM tokens WHERE user_id = ? ORDER BY created_at DESC";
let records = query_as::<_, TokenRecord>(query)
.bind(i64::from(user_id))
.fetch_all(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(records.into_iter().map(Self::to_domain).collect())
}
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError> {
let query = "UPDATE tokens SET revoked_at = ? WHERE id = ? RETURNING id, user_id, token_hash, name, created_at, last_used_at, revoked_at";
let now = Utc::now();
let record = query_as::<_, TokenRecord>(query)
.bind(now)
.bind(i64::from(id))
.fetch_optional(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
match record {
Some(record) => Ok(Self::to_domain(record)),
None => Err(RepositoryError::NotFound),
}
}
async fn update_last_used(&self, id: TokenId) -> Result<(), RepositoryError> {
let now = Utc::now();
sqlx::query("UPDATE tokens SET last_used_at = ? WHERE id = ?")
.bind(now)
.bind(i64::from(id))
.execute(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
Ok(())
}
}
#[derive(sqlx::FromRow)]
struct TokenRecord {
id: i64,
user_id: i64,
token_hash: String,
name: String,
created_at: DateTime<Utc>,
last_used_at: Option<DateTime<Utc>>,
revoked_at: Option<DateTime<Utc>>,
}