use session::Session; use utoipa::{IntoParams, ToSchema}; use crate::{ AppService, auth::rsa::RsaResponse, constant_time_eq, error::AppError, }; #[derive( serde::Deserialize, serde::Serialize, Clone, Debug, ToSchema, IntoParams, )] pub struct CaptchaQuery { pub w: u32, pub h: u32, pub dark: bool, pub rsa: bool, } #[derive(serde::Serialize, ToSchema)] pub struct CaptchaResponse { pub base64: String, pub rsa: Option, pub req: CaptchaQuery, } impl AppService { const CAPTCHA_KEY: &'static str = "captcha"; const CAPTCHA_LENGTH: usize = 4; pub async fn auth_captcha( &self, context: &Session, query: CaptchaQuery, ) -> Result { let CaptchaQuery { w, h, dark, rsa } = query; 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).ok(); 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!(ip = ?context.ip_address(), "Captcha verification failed"); return Err(AppError::CaptchaError); } context.remove(Self::CAPTCHA_KEY); Ok(()) } }