Skip to main content

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}