use std::sync::Arc;
#[cfg(any(feature = "redis", feature = "full"))]
use crate::rediscache::RedisPool;
use crate::response::error::{AppError, AppResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptchaType {
Slider,
Numeric,
Alphanumeric,
}
#[derive(Debug, Clone, crate::serde::Serialize, crate::serde::Deserialize)]
pub struct CaptchaData {
pub id: String,
pub code: String,
pub expires_in: u64,
}
pub struct CaptchaService;
impl CaptchaService {
const CACHE_PREFIX_SLIDER: &'static str = ":captcha:slider:";
const CACHE_PREFIX_NUMERIC: &'static str = ":captcha:numeric:";
const CACHE_PREFIX_ALPHA: &'static str = ":captcha:alpha:";
const DEFAULT_EXPIRATION: u64 = 120;
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn gen_captcha_slider(
redis_pool: &Arc<RedisPool>,
prefix: &str,
code: &str,
account: &str,
expires_in: Option<u64>,
) -> AppResult<()> {
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_SLIDER, account);
let value = Self::hash_code(code);
let seconds = expires_in.unwrap_or(Self::DEFAULT_EXPIRATION);
redis_pool
.setex(key, value.clone(), seconds)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
crate::tracing::info!(
"gen_captcha_slider success for account: {}, value: {}",
account,
value
);
Ok(())
}
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn captcha_slider_valid(
redis_pool: &Arc<RedisPool>,
prefix: &str,
code: &str,
account: &str,
delete: bool,
) -> AppResult<()> {
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_SLIDER, account);
let result = redis_pool
.get::<_, String>(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
match result {
Some(stored_code) => {
let hashed_input = Self::hash_code(code);
if stored_code != hashed_input {
return Err(AppError::ClientError(
"Slider captcha verification failed, please refresh and try again"
.to_string(),
));
}
}
None => {
return Err(AppError::ClientError(
"Captcha expired or not found".to_string(),
));
}
}
if delete {
redis_pool
.del(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
}
crate::tracing::info!("captcha_slider_valid success for account: {}", account);
Ok(())
}
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn captcha_slider_delete(
redis_pool: &Arc<RedisPool>,
prefix: &str,
account: &str,
) -> AppResult<()> {
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_SLIDER, account);
redis_pool
.del(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
Ok(())
}
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn gen_numeric_captcha(
redis_pool: &Arc<RedisPool>,
prefix: &str,
account: &str,
length: Option<usize>,
expires_in: Option<u64>,
) -> AppResult<CaptchaData> {
let len = length.unwrap_or(6).clamp(4, 8);
let uuid = crate::uuid::Uuid::new_v4();
let uuid_bytes = uuid.as_bytes();
let code: String = (0..len)
.map(|i| (uuid_bytes[i % 16] % 10).to_string())
.collect();
let id = crate::uuid::Uuid::new_v4().to_string();
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_NUMERIC, id);
let seconds = expires_in.unwrap_or(Self::DEFAULT_EXPIRATION);
redis_pool
.setex(&key, code.clone(), seconds)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
crate::tracing::info!(
"gen_numeric_captcha success for account: {}, id: {}",
account,
id
);
Ok(CaptchaData {
id,
code,
expires_in: seconds,
})
}
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn validate_numeric_captcha(
redis_pool: &Arc<RedisPool>,
prefix: &str,
id: &str,
code: &str,
delete: bool,
) -> AppResult<()> {
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_NUMERIC, id);
let result = redis_pool
.get::<_, String>(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
match result {
Some(stored_code) => {
if stored_code != code {
return Err(AppError::ClientError(
"Numeric captcha verification failed".to_string(),
));
}
}
None => {
return Err(AppError::ClientError(
"Captcha expired or not found".to_string(),
));
}
}
if delete {
redis_pool
.del(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
}
crate::tracing::info!("validate_numeric_captcha success for id: {}", id);
Ok(())
}
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn gen_alphanumeric_captcha(
redis_pool: &Arc<RedisPool>,
prefix: &str,
account: &str,
length: Option<usize>,
expires_in: Option<u64>,
) -> AppResult<CaptchaData> {
let len = length.unwrap_or(6).clamp(4, 10);
let charset = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let uuid = crate::uuid::Uuid::new_v4();
let uuid_bytes = uuid.as_bytes();
let code: String = (0..len)
.map(|i| {
let idx = (uuid_bytes[i % 16] as usize) % charset.len();
charset[idx] as char
})
.collect();
let id = crate::uuid::Uuid::new_v4().to_string();
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_ALPHA, id);
let seconds = expires_in.unwrap_or(Self::DEFAULT_EXPIRATION);
redis_pool
.setex(&key, code.clone(), seconds)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
crate::tracing::info!(
"gen_alphanumeric_captcha success for account: {}, id: {}",
account,
id
);
Ok(CaptchaData {
id,
code,
expires_in: seconds,
})
}
#[cfg(any(feature = "redis", feature = "full"))]
pub async fn validate_alphanumeric_captcha(
redis_pool: &Arc<RedisPool>,
prefix: &str,
id: &str,
code: &str,
delete: bool,
) -> AppResult<()> {
let key = format!("{}{}{}", prefix, Self::CACHE_PREFIX_ALPHA, id);
let result = redis_pool
.get::<_, String>(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
match result {
Some(stored_code) => {
if stored_code.to_uppercase() != code.to_uppercase() {
return Err(AppError::ClientError(
"Captcha verification failed".to_string(),
));
}
}
None => {
return Err(AppError::ClientError(
"Captcha expired or not found".to_string(),
));
}
}
if delete {
redis_pool
.del(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
}
crate::tracing::info!("validate_alphanumeric_captcha success for id: {}", id);
Ok(())
}
fn hash_code(code: &str) -> String {
use crate::md5;
format!("{:x}", md5::compute(code))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_captcha_type() {
assert_eq!(CaptchaType::Slider, CaptchaType::Slider);
assert_ne!(CaptchaType::Numeric, CaptchaType::Alphanumeric);
}
#[test]
fn test_hash_code() {
let hash1 = CaptchaService::hash_code("test123");
let hash2 = CaptchaService::hash_code("test123");
let hash3 = CaptchaService::hash_code("different");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
}