Skip to main content

fr_rust/otp/
otp.rs

1use crate::prelude::*;
2use rand::Rng;
3use deadpool_redis::redis::AsyncCommands;
4use crate::crypto::crypto::CryptoError;
5
6// --- ERROR HANDLING ---
7#[derive(thiserror::Error, Debug)]
8pub enum OtpError {
9    #[error("Redis manager error: {0}")]
10    RedisManager(#[from] RedisManagerError),
11
12    #[error("Redis error: {0}")]
13    Redis(#[from] deadpool_redis::redis::RedisError),
14
15    #[error("Redis pool error: {0}")]
16    RedisPool(#[from] deadpool_redis::PoolError),
17
18    #[error("Crypto error: {0}")]
19    Crypto(#[from] CryptoError),
20}
21
22
23pub type Result<T> = std::result::Result<T, OtpError>;
24
25// --- SERVICE IMPLEMENTATION ---
26
27#[derive(Clone)]
28pub struct OtpConfig {
29    pub secret: String,
30    pub crypto: CryptoService,
31    pub redis: RedisManager,
32}
33
34#[derive(Clone)]
35pub struct OtpService {
36    config: OtpConfig,
37}
38
39impl OtpService {
40    pub fn new(config: OtpConfig) -> Self {
41        Self { config }
42    }
43
44    pub async fn generate_otp(&self, user_id: &str, digits: u32, expiry_time: u64) -> Result<String> {
45        let otp = Self::random_digits(digits);
46        let content_to_hash = format!("{}:{}", self.config.secret, otp);
47        let hash = self.config.crypto.sha256_hash(&content_to_hash)?;
48        let redis_key = format!("otp:{}", user_id);
49        
50        let mut con = self.config.redis.get_connection().await?;
51        
52        let _res: () = con.set_ex(&redis_key, &hash, expiry_time).await?;
53        
54        Ok(otp)
55    }
56
57    pub async fn verify_otp(&self, user_id: &str, otp: &str) -> Result<bool> {
58        let redis_key = format!("otp:{}", user_id);
59        
60        let mut con = self.config.redis.get_connection().await?;
61        let stored_hash: Option<String> = con.get(&redis_key).await?;
62        
63        let hash_to_check = match stored_hash {
64            Some(h) => h,
65            None => return Ok(false),
66        };
67        
68        let content_to_hash = format!("{}:{}", self.config.secret, otp);
69        let calculated_hash = self.config.crypto.sha256_hash(&content_to_hash)?;
70        let ok = calculated_hash == hash_to_check;
71        
72        if ok {
73            let _res: () = con.del(&redis_key).await?;
74        }
75        
76        Ok(ok)
77    }
78
79    fn random_digits(digits: u32) -> String {
80        let mut bytes = [0u8; 8];
81
82        rand::rng().fill_bytes(&mut bytes);
83        
84        let num = u64::from_le_bytes(bytes);
85        let otp = num % 10u64.pow(digits);
86        format!("{:0width$}", otp, width = digits as usize)
87    }
88}