//! Passkey / WebAuthn credential queries. use sqlx::PgPool; use super::{PasskeyId, UserId}; use crate::error::Result; /// A stored passkey row for listing in the dashboard. #[derive(sqlx::FromRow)] pub struct DbPasskey { pub id: PasskeyId, pub name: String, pub created_at: chrono::DateTime, pub last_used_at: Option>, } /// Store a new passkey credential. #[tracing::instrument(skip_all)] pub async fn create_passkey( pool: &PgPool, user_id: UserId, name: &str, credential_json: &serde_json::Value, credential_id: &[u8], ) -> Result { let id: PasskeyId = sqlx::query_scalar( r#" INSERT INTO user_passkeys (user_id, name, credential_json, credential_id) VALUES ($1, $2, $3, $4) RETURNING id "#, ) .bind(user_id) .bind(name) .bind(credential_json) .bind(credential_id) .fetch_one(pool) .await?; Ok(id) } /// List passkeys for a user (dashboard display). #[tracing::instrument(skip_all)] pub async fn list_passkeys(pool: &PgPool, user_id: UserId) -> Result> { let rows: Vec = sqlx::query_as( "SELECT id, name, created_at, last_used_at FROM user_passkeys WHERE user_id = $1 ORDER BY created_at LIMIT 100", ) .bind(user_id) .fetch_all(pool) .await?; Ok(rows) } /// Get all credential JSONs for a user (used as exclusion list during registration). #[tracing::instrument(skip_all)] pub async fn get_passkey_credentials( pool: &PgPool, user_id: UserId, ) -> Result> { let rows: Vec = sqlx::query_scalar("SELECT credential_json FROM user_passkeys WHERE user_id = $1") .bind(user_id) .fetch_all(pool) .await?; Ok(rows) } /// Find which user owns a credential ID (for discoverable login). #[tracing::instrument(skip_all)] pub async fn find_user_by_credential_id( pool: &PgPool, credential_id: &[u8], ) -> Result> { let row: Option<(UserId, serde_json::Value)> = sqlx::query_as( "SELECT user_id, credential_json FROM user_passkeys WHERE credential_id = $1", ) .bind(credential_id) .fetch_optional(pool) .await?; Ok(row) } /// Update a passkey credential after successful authentication (bump counter + last_used_at). #[tracing::instrument(skip_all)] pub async fn update_passkey_after_auth( pool: &PgPool, credential_id: &[u8], updated_json: &serde_json::Value, ) -> Result<()> { sqlx::query( "UPDATE user_passkeys SET credential_json = $2, last_used_at = NOW() WHERE credential_id = $1", ) .bind(credential_id) .bind(updated_json) .execute(pool) .await?; Ok(()) } /// Rename a passkey (returns false if not found or not owned by user). #[tracing::instrument(skip_all)] pub async fn rename_passkey( pool: &PgPool, passkey_id: PasskeyId, user_id: UserId, name: &str, ) -> Result { let result = sqlx::query( "UPDATE user_passkeys SET name = $3 WHERE id = $1 AND user_id = $2", ) .bind(passkey_id) .bind(user_id) .bind(name) .execute(pool) .await?; Ok(result.rows_affected() > 0) } /// Delete a passkey (returns false if not found or not owned by user). #[tracing::instrument(skip_all)] pub async fn delete_passkey( pool: &PgPool, passkey_id: PasskeyId, user_id: UserId, ) -> Result { let result = sqlx::query( "DELETE FROM user_passkeys WHERE id = $1 AND user_id = $2", ) .bind(passkey_id) .bind(user_id) .execute(pool) .await?; Ok(result.rows_affected() > 0) } /// Count passkeys for a user. #[tracing::instrument(skip_all)] pub async fn count_passkeys(pool: &PgPool, user_id: UserId) -> Result { let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM user_passkeys WHERE user_id = $1") .bind(user_id) .fetch_one(pool) .await?; Ok(count) }