gitdataai/libs/room/src/service/validation.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

146 lines
5.6 KiB
Rust

use models::projects::{project, project_history_name, project_members};
use models::rooms::room;
use sea_orm::*;
use uuid::Uuid;
use super::RoomService;
use crate::error::RoomError;
impl RoomService {
pub(crate) fn validate_name(name: &str, max_len: usize) -> Result<(), RoomError> {
if name.trim().is_empty() {
return Err(RoomError::BadRequest("name cannot be empty".to_string()));
}
if name.len() > max_len {
return Err(RoomError::BadRequest(format!(
"name exceeds maximum length of {} characters",
max_len
)));
}
Ok(())
}
pub(crate) fn validate_content(content: &str, max_len: usize) -> Result<(), RoomError> {
if content.trim().is_empty() {
return Err(RoomError::BadRequest("content cannot be empty".to_string()));
}
if content.len() > max_len {
return Err(RoomError::BadRequest(format!(
"content exceeds maximum length of {} characters",
max_len
)));
}
Ok(())
}
pub(crate) fn sanitize_content(content: &str) -> String {
ammonia::clean(content)
}
pub(crate) fn parse_message_content_type(
content_type: Option<String>,
) -> Result<models::rooms::MessageContentType, RoomError> {
match content_type.unwrap_or_else(|| "text".to_string()).to_lowercase().as_str() {
"text" => Ok(models::rooms::MessageContentType::Text),
"image" => Ok(models::rooms::MessageContentType::Image),
"audio" => Ok(models::rooms::MessageContentType::Audio),
"video" => Ok(models::rooms::MessageContentType::Video),
"file" => Ok(models::rooms::MessageContentType::File),
_ => Err(RoomError::BadRequest("invalid message content_type".to_string())),
}
}
pub async fn utils_find_project_by_name(&self, name: String) -> Result<project::Model, RoomError> {
match project::Entity::find()
.filter(project::Column::Name.eq(name.clone()))
.one(&self.db)
.await
.inspect_err(|e| tracing::warn!(error = %e, project_name = %name, "utils_find_project_by_name: DB error"))
.ok()
.flatten()
{
Some(project) => Ok(project),
None => match project_history_name::Entity::find()
.filter(project_history_name::Column::HistoryName.eq(name.clone()))
.one(&self.db)
.await
.inspect_err(|e| tracing::warn!(error = %e, name = %name, "project_history_name lookup failed"))
.ok()
.flatten()
{
Some(project) => self.utils_find_project_by_uid(project.project_uid).await,
None => Err(RoomError::NotFound("Project not found".to_string())),
},
}
}
pub async fn utils_find_project_by_uid(&self, uid: Uuid) -> Result<project::Model, RoomError> {
project::Entity::find_by_id(uid)
.one(&self.db)
.await
.inspect_err(|e| tracing::warn!(error = %e, project_uid = %uid, "utils_find_project_by_uid: DB error"))
.ok()
.flatten()
.ok_or_else(|| RoomError::NotFound("Project not found".to_string()))
}
pub async fn check_project_access(&self, project_uid: Uuid, user_uid: Uuid) -> Result<(), RoomError> {
let project = project::Entity::find_by_id(project_uid)
.one(&self.db)
.await
.inspect_err(|e| tracing::warn!(error = %e, project_uid = %project_uid, "check_project_access: DB error"))
.ok()
.flatten()
.ok_or_else(|| RoomError::NotFound("Project not found".to_string()))?;
if project.is_public {
return Ok(());
}
let member = project_members::Entity::find()
.filter(project_members::Column::Project.eq(project_uid))
.filter(project_members::Column::User.eq(user_uid))
.one(&self.db)
.await?;
if member.is_some() { Ok(()) } else { Err(RoomError::NoPower) }
}
pub async fn ensure_room_visible_for_user(
&self, room: &room::Model, user_id: Uuid,
) -> Result<(), RoomError> {
self.require_room_access(room.id, user_id).await
}
pub async fn get_room_version(&self, room_id: Uuid) -> Result<i64, RoomError> {
let version_key = format!("room:version:{}", room_id);
let mut conn = self.cache.conn().await.map_err(|e| {
RoomError::Internal(format!("failed to get redis for version: {}", e))
})?;
let version: Option<i64> = redis::cmd("GET")
.arg(&version_key)
.query_async(&mut conn)
.await
.map_err(|e| RoomError::Internal(format!("version GET: {}", e)))?;
Ok(version.unwrap_or(0))
}
pub async fn increment_room_version(&self, room_id: Uuid) -> Result<i64, RoomError> {
Self::raw_increment_room_version(&self.cache, room_id).await
}
pub async fn raw_increment_room_version(
cache: &db::cache::AppCache, room_id: Uuid,
) -> Result<i64, RoomError> {
let version_key = format!("room:version:{}", room_id);
let mut conn = cache.conn().await.map_err(|e| {
RoomError::Internal(format!("failed to get redis for version: {}", e))
})?;
let version: i64 = redis::cmd("INCR")
.arg(&version_key)
.query_async(&mut conn)
.await
.map_err(|e| RoomError::Internal(format!("version INCR: {}", e)))?;
Ok(version)
}
}