use crate::session::Session; use serde::{Deserialize, Serialize}; use crate::error::AppError; use crate::service::AuthService; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct CaptchaQuery { pub w: u32, pub h: u32, pub dark: bool, pub rsa: bool, } #[derive(Serialize, utoipa::ToSchema)] pub struct CaptchaResponse { pub base64: String, pub rsa: Option, pub req: CaptchaQuery, } impl AuthService { const CAPTCHA_KEY: &'static str = "captcha"; const CAPTCHA_LENGTH: usize = 4; const CAPTCHA_MIN_WIDTH: u32 = 80; const CAPTCHA_MAX_WIDTH: u32 = 400; const CAPTCHA_MIN_HEIGHT: u32 = 30; const CAPTCHA_MAX_HEIGHT: u32 = 200; pub async fn auth_captcha( &self, context: &Session, query: CaptchaQuery, ) -> Result { let CaptchaQuery { w, h, dark, rsa } = query; if !(Self::CAPTCHA_MIN_WIDTH..=Self::CAPTCHA_MAX_WIDTH).contains(&w) || !(Self::CAPTCHA_MIN_HEIGHT..=Self::CAPTCHA_MAX_HEIGHT).contains(&h) { return Err(AppError::BadRequest("invalid captcha size".into())); } let captcha = captcha_rs::CaptchaBuilder::new() .width(w) .height(h) .dark_mode(dark) .length(Self::CAPTCHA_LENGTH) .build(); let base64 = captcha.to_base64(); let text = captcha.text; context .insert(Self::CAPTCHA_KEY, text) .map_err(|_| AppError::InternalServerError("session insert failed".into()))?; Ok(CaptchaResponse { base64, rsa: if rsa { Some(self.auth_rsa(context).await?) } else { None }, req: CaptchaQuery { w, h, dark, rsa }, }) } pub async fn auth_check_captcha( &self, context: &Session, captcha: String, ) -> Result<(), AppError> { let text = context .get::(Self::CAPTCHA_KEY) .map_err(|_| AppError::CaptchaError)? .ok_or(AppError::CaptchaError)?; if !constant_time_eq(&text.to_lowercase(), &captcha.to_lowercase()) { context.remove(Self::CAPTCHA_KEY); tracing::warn!("Captcha verification failed"); return Err(AppError::CaptchaError); } context.remove(Self::CAPTCHA_KEY); Ok(()) } } use crate::service::util::constant_time_eq;