koprogo_api/application/ports/magic_link_repository.rs
1//! Port for persisting and looking up [`MagicLink`] entities.
2//!
3//! All methods return `Result<_, AppError>` natively (no `Result<_, String>`
4//! debt to migrate — Story 3.2 ships AppError-clean per CRITICAL.md #4 / #555).
5
6use crate::application::error::AppError;
7use crate::domain::entities::MagicLink;
8use async_trait::async_trait;
9use uuid::Uuid;
10
11#[async_trait]
12pub trait MagicLinkRepository: Send + Sync {
13 /// Persist a freshly issued MagicLink. Caller guarantees `token_hash`
14 /// uniqueness (sha256 of 256-bit random token is collision-free in practice).
15 async fn save(&self, link: &MagicLink) -> Result<(), AppError>;
16
17 /// Look up a link by its `token_hash` (already hashed by the caller).
18 /// Returns `Ok(None)` if no record matches — callers must translate this
19 /// to `AppError::MagicLinkInvalid` so a forged token and an unknown token
20 /// are indistinguishable from the client side (no enumeration).
21 async fn find_by_token_hash(&self, token_hash: &str) -> Result<Option<MagicLink>, AppError>;
22
23 /// Atomically mark a link as consumed (sets `consumed_at = NOW()`).
24 /// The implementation MUST be a single `UPDATE ... WHERE id = $1
25 /// AND consumed_at IS NULL` so two concurrent consumers cannot both
26 /// succeed (race-free single-use guarantee).
27 async fn mark_consumed(&self, id: Uuid) -> Result<(), AppError>;
28}