brewlog/src/infrastructure/repositories/auth/passkey_credentials.rs
Jon Seager edc0021792
refactor: split large directories into coffee/auth/analytics subdirs
- Restructure domain/, repositories/, and routes/api/ into themed
  subdirectories: coffee/, auth/, analytics/ (and system/ for routes)
- Convert super:: relative imports to crate:: absolute paths
- Promote macro visibility from pub(super) to pub(crate)
- Add pub use re-exports for backward-compatible import paths
2026-02-08 18:16:16 +00:00

185 lines
5.6 KiB
Rust

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use sqlx::{query, query_as};
use crate::domain::RepositoryError;
use crate::domain::ids::{PasskeyCredentialId, UserId};
use crate::domain::passkey_credentials::{NewPasskeyCredential, PasskeyCredential};
use crate::domain::repositories::PasskeyCredentialRepository;
use crate::infrastructure::database::DatabasePool;
#[derive(Clone)]
pub struct SqlPasskeyCredentialRepository {
pool: DatabasePool,
}
impl SqlPasskeyCredentialRepository {
pub fn new(pool: DatabasePool) -> Self {
Self { pool }
}
fn to_domain(record: PasskeyCredentialRecord) -> PasskeyCredential {
let PasskeyCredentialRecord {
id,
user_id,
credential_json,
name,
created_at,
last_used_at,
} = record;
PasskeyCredential {
id: PasskeyCredentialId::from(id),
user_id: UserId::from(user_id),
credential_json,
name,
created_at,
last_used_at,
}
}
}
#[async_trait]
impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
async fn insert(
&self,
credential: NewPasskeyCredential,
) -> Result<PasskeyCredential, RepositoryError> {
let sql = r"
INSERT INTO passkey_credentials (user_id, credential_json, name)
VALUES (?, ?, ?)
RETURNING id, user_id, credential_json, name, created_at, last_used_at
";
let record = query_as::<_, PasskeyCredentialRecord>(sql)
.bind(i64::from(credential.user_id))
.bind(&credential.credential_json)
.bind(&credential.name)
.fetch_one(&self.pool)
.await
.map_err(|err| {
RepositoryError::unexpected(format!("failed to insert passkey credential: {err}"))
})?;
Ok(Self::to_domain(record))
}
async fn get(&self, id: PasskeyCredentialId) -> Result<PasskeyCredential, RepositoryError> {
let sql = r"
SELECT id, user_id, credential_json, name, created_at, last_used_at
FROM passkey_credentials
WHERE id = ?
";
let record = query_as::<_, PasskeyCredentialRecord>(sql)
.bind(i64::from(id))
.fetch_one(&self.pool)
.await
.map_err(|err| match err {
sqlx::Error::RowNotFound => RepositoryError::NotFound,
err => {
RepositoryError::unexpected(format!("failed to get passkey credential: {err}"))
}
})?;
Ok(Self::to_domain(record))
}
async fn list_by_user(
&self,
user_id: UserId,
) -> Result<Vec<PasskeyCredential>, RepositoryError> {
let sql = r"
SELECT id, user_id, credential_json, name, created_at, last_used_at
FROM passkey_credentials
WHERE user_id = ?
ORDER BY created_at ASC
";
let records = query_as::<_, PasskeyCredentialRecord>(sql)
.bind(i64::from(user_id))
.fetch_all(&self.pool)
.await
.map_err(|err| {
RepositoryError::unexpected(format!("failed to list passkey credentials: {err}"))
})?;
Ok(records.into_iter().map(Self::to_domain).collect())
}
async fn list_all(&self) -> Result<Vec<PasskeyCredential>, RepositoryError> {
let sql = r"
SELECT id, user_id, credential_json, name, created_at, last_used_at
FROM passkey_credentials
ORDER BY created_at ASC
";
let records = query_as::<_, PasskeyCredentialRecord>(sql)
.fetch_all(&self.pool)
.await
.map_err(|err| {
RepositoryError::unexpected(format!(
"failed to list all passkey credentials: {err}"
))
})?;
Ok(records.into_iter().map(Self::to_domain).collect())
}
async fn update_credential_json(
&self,
id: PasskeyCredentialId,
credential_json: &str,
) -> Result<(), RepositoryError> {
query("UPDATE passkey_credentials SET credential_json = ? WHERE id = ?")
.bind(credential_json)
.bind(i64::from(id))
.execute(&self.pool)
.await
.map_err(|err| {
RepositoryError::unexpected(format!(
"failed to update passkey credential json: {err}"
))
})?;
Ok(())
}
async fn update_last_used(&self, id: PasskeyCredentialId) -> Result<(), RepositoryError> {
let now = Utc::now();
query("UPDATE passkey_credentials SET last_used_at = ? WHERE id = ?")
.bind(now)
.bind(i64::from(id))
.execute(&self.pool)
.await
.map_err(|err| {
RepositoryError::unexpected(format!(
"failed to update passkey credential last_used_at: {err}"
))
})?;
Ok(())
}
async fn delete(&self, id: PasskeyCredentialId) -> Result<(), RepositoryError> {
query("DELETE FROM passkey_credentials WHERE id = ?")
.bind(i64::from(id))
.execute(&self.pool)
.await
.map_err(|err| {
RepositoryError::unexpected(format!("failed to delete passkey credential: {err}"))
})?;
Ok(())
}
}
#[derive(sqlx::FromRow)]
struct PasskeyCredentialRecord {
id: i64,
user_id: i64,
credential_json: String,
name: String,
created_at: DateTime<Utc>,
last_used_at: Option<DateTime<Utc>>,
}