Skip to main content

shared/domain/password_reset/
model.rs

1use chrono::{DateTime, Utc};
2use mongodb::bson;
3use serde::{Deserialize, Serialize};
4use sqlx::prelude::FromRow;
5use utoipa::ToSchema;
6
7use crate::domain::query::IntoBsonDocument;
8
9use super::super::serde::{
10    deserialize_datetime, deserialize_object_id, deserialize_object_id_as_string,
11    deserialize_option_datetime,
12};
13
14#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
15#[schema(example = json!({"link": String::default(), "expires_at": "2026-02-19T22:42:23.467Z"}))]
16pub struct ExpiringLink {
17    pub link: String,
18    pub expires_at: chrono::DateTime<chrono::Utc>,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, FromRow)]
22pub struct PasswordResetToken {
23    #[serde(
24        rename = "_id",
25        default,
26        skip_serializing_if = "Option::is_none",
27        deserialize_with = "deserialize_object_id_as_string"
28    )]
29    pub id: Option<String>,
30
31    #[sqlx(rename = "userId")]
32    #[serde(
33        rename = "userId",
34        default,
35        // serialize_with = "serialize_object_id_as_string",
36        deserialize_with = "deserialize_object_id"
37    )]
38    pub user_id: String,
39
40    #[sqlx(rename = "issuedAt")]
41    #[serde(rename = "issuedAt", deserialize_with = "deserialize_datetime")]
42    pub issued_at: DateTime<Utc>,
43    #[sqlx(rename = "expiresAt")]
44    #[serde(rename = "expiresAt", deserialize_with = "deserialize_datetime")]
45    pub expires_at: DateTime<Utc>,
46    #[sqlx(rename = "usedAt")]
47    #[serde(rename = "usedAt", deserialize_with = "deserialize_option_datetime")]
48    pub used_at: Option<DateTime<Utc>>,
49
50    pub token: String,
51}
52
53impl Default for PasswordResetToken {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58impl PasswordResetToken {
59    pub fn new() -> Self {
60        Self {
61            id: None,
62            user_id: String::default(),
63            token: String::default(),
64            issued_at: Utc::now(),
65            expires_at: Utc::now() + chrono::Duration::seconds(86400),
66            used_at: None,
67        }
68    }
69}
70impl PasswordResetToken {
71    pub fn with_user_id(mut self, user_id: &str) -> Self {
72        self.user_id = user_id.into();
73        self
74    }
75    pub fn with_token_hash(mut self, hash: &str) -> Self {
76        self.token = hash.into();
77        self
78    }
79    pub fn with_expiray(mut self, expires_at: &chrono::DateTime<chrono::Utc>) -> Self {
80        self.expires_at = *expires_at;
81        self
82    }
83}
84
85impl PasswordResetToken {
86    pub fn id(&self) -> Result<&str, crate::error::CoreError> {
87        self.id.as_deref().ok_or_else(|| {
88            tracing::error!(
89                error_code = "ValidationError::Malformed",
90                "Unexpected null/missing data"
91            );
92            crate::error::CoreError::Validation(crate::error::ValidationError::Malformed {
93                field: crate::error::CredentialField::ObjectId,
94            })
95        })
96    }
97}
98impl IntoBsonDocument for PasswordResetToken {
99    fn into_bson_document(self) -> Result<bson::Document, bson::ser::Error> {
100        let mut doc = bson::to_document(&self)?;
101
102        for key in &["issuedAt", "expiresAt", "usedAt"] {
103            if let Some(bson::Bson::String(s)) = doc.get(*key).cloned()
104                && let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&s)
105            {
106                doc.insert(
107                    *key,
108                    bson::Bson::DateTime(bson::DateTime::from_millis(dt.timestamp_millis())),
109                );
110            }
111        }
112
113        Ok(doc)
114    }
115}