- 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
151 lines
4.7 KiB
Rust
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>>,
|
|
}
|