1use crate::prelude::*;
2use rand::Rng;
3use deadpool_redis::redis::AsyncCommands;
4use crate::crypto::crypto::CryptoError;
5
6#[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#[derive(Clone)]
28pub struct OtpConfig {
29 pub secret: String,
30 pub crypto: CryptoService,
31 pub redis: RedisManager,
32 pub ttl_secs: u64,
33}
34
35#[derive(Clone)]
36pub struct OtpService {
37 config: OtpConfig,
38}
39
40impl OtpService {
41 pub fn new(config: OtpConfig) -> Self {
42 Self { config }
43 }
44
45 pub async fn generate_otp(&self, user_id: &str, digits: u32) -> Result<String> {
46 let otp = Self::random_digits(digits);
47 let content_to_hash = format!("{}:{}", self.config.secret, otp);
48 let hash = self.config.crypto.sha256_hash(&content_to_hash)?.hash;
49 let redis_key = format!("otp:{}", user_id);
50
51 let mut con = self.config.redis.get_connection().await?;
52
53 let _res: () = con.set_ex(&redis_key, &hash, self.config.ttl_secs).await?;
54
55 Ok(otp)
56 }
57
58 pub async fn verify_otp(&self, user_id: &str, otp: &str) -> Result<bool> {
59 let redis_key = format!("otp:{}", user_id);
60
61 let mut con = self.config.redis.get_connection().await?;
62 let stored_hash: Option<String> = con.get(&redis_key).await?;
63
64 let hash_to_check = match stored_hash {
65 Some(h) => h,
66 None => return Ok(false),
67 };
68
69 let content_to_hash = format!("{}:{}", self.config.secret, otp);
70 let calculated_hash = self.config.crypto.sha256_hash(&content_to_hash)?.hash;
71 let ok = calculated_hash == hash_to_check;
72
73 if ok {
74 let _res: () = con.del(&redis_key).await?;
75 }
76
77 Ok(ok)
78 }
79
80 fn random_digits(digits: u32) -> String {
81 let mut bytes = [0u8; 8];
82
83 rand::rng().fill_bytes(&mut bytes);
85
86 let num = u64::from_le_bytes(bytes);
87 let otp = num % 10u64.pow(digits);
88 format!("{:0width$}", otp, width = digits as usize)
89 }
90}