gitdataai/libs/service/auth/captcha.rs
ZhenYi 14f6e1e500 feat(core): initialize project with access control and AI integration
- Add gitignore and prettier configuration files for project scaffolding
- Implement room access control service with project member verification
- Create user access key management with CRUD operations and activity logging
- Add accordion UI component for frontend expandable sections
- Implement room AI configuration with list, upsert, and delete operations
- Add AI event types for agent join/leave/status change tracking
- Create streaming AI processing services for mode and react patterns
- Build room AI service with model detection and idempotency handling
- Integrate chat service orchestration for AI message processing
- Add typing indicators and stream cancellation for AI interactions
- Implement mention parsing and context extraction for AI agents
2026-05-03 06:04:31 +08:00

79 lines
2.2 KiB
Rust

use crate::AppService;
use crate::auth::rsa::RsaResponse;
use crate::error::AppError;
use session::Session;
use utoipa::{IntoParams, ToSchema};
#[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<RsaResponse>,
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<CaptchaResponse, AppError> {
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::<String>(Self::CAPTCHA_KEY)
.map_err(|_| AppError::CaptchaError)?
.ok_or(AppError::CaptchaError)?;
if !constant_time_str_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(())
}
}
/// Constant-time string comparison to prevent timing side-channel attacks.
fn constant_time_str_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.bytes()
.zip(b.bytes())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}