use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct VerificationToken {
pub id: Uuid,
pub user_id: Uuid,
pub token: String,
pub expires_at: DateTime<Utc>,
pub used_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[cfg(feature = "postgres")]
impl VerificationToken {
pub async fn create(pool: &sqlx::PgPool, user_id: Uuid) -> sqlx::Result<Self> {
use rand::Rng;
let token_bytes: [u8; 32] = {
let mut rng = rand::thread_rng();
rng.gen()
};
use base64::{engine::general_purpose, Engine as _};
let token = general_purpose::URL_SAFE_NO_PAD.encode(token_bytes);
let expires_at = Utc::now() + chrono::Duration::hours(24);
sqlx::query(
"UPDATE verification_tokens SET used_at = NOW() WHERE user_id = $1 AND used_at IS NULL",
)
.bind(user_id)
.execute(pool)
.await?;
let verification_token = sqlx::query_as::<_, Self>(
r#"
INSERT INTO verification_tokens (user_id, token, expires_at)
VALUES ($1, $2, $3)
RETURNING *
"#,
)
.bind(user_id)
.bind(&token)
.bind(expires_at)
.fetch_one(pool)
.await?;
Ok(verification_token)
}
pub async fn find_by_token(pool: &sqlx::PgPool, token: &str) -> sqlx::Result<Option<Self>> {
sqlx::query_as::<_, Self>(
"SELECT * FROM verification_tokens WHERE token = $1 AND used_at IS NULL",
)
.bind(token)
.fetch_optional(pool)
.await
}
pub async fn mark_as_used(pool: &sqlx::PgPool, token_id: Uuid) -> sqlx::Result<()> {
sqlx::query("UPDATE verification_tokens SET used_at = NOW() WHERE id = $1")
.bind(token_id)
.execute(pool)
.await?;
Ok(())
}
pub fn is_valid(&self) -> bool {
self.used_at.is_none() && self.expires_at > Utc::now()
}
}