- 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
185 lines
5.6 KiB
Rust
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>>,
|
|
}
|