use crate::errors::SpaceError; use crate::services::service::SpaceSettings; use auth::Accounts; use cache::AppRedis; use models::space::{AllowJoinLevel, spaces}; use time::{Duration, OffsetDateTime}; #[derive(Clone)] pub struct SpacePolicy { pub(crate) settings: SpaceSettings, pub(crate) accounts: Accounts, redis: AppRedis, } impl SpacePolicy { pub fn new(settings: SpaceSettings, accounts: Accounts, redis: AppRedis) -> Self { Self { settings, accounts, redis, } } pub(crate) async fn allow_room_creation(&self, caller: i64) -> Result<(), SpaceError> { if self .redis .quota_allow( &format!("quota:room_create:{caller}"), self.settings.quota.room_create_per_day, 86400, ) .await { Ok(()) } else { Err(SpaceError::RateLimited( "room creation quota exceeded, retry tomorrow".to_owned(), )) } } pub(crate) async fn allow_join_request(&self, caller: i64) -> Result<(), SpaceError> { if self .redis .quota_allow( &format!("quota:space_join_request:{caller}"), self.settings.quota.space_join_request_per_day, 86400, ) .await { Ok(()) } else { Err(SpaceError::RateLimited( "join request quota exceeded, retry tomorrow".to_owned(), )) } } pub(crate) async fn assert_join_requirements( &self, space: &spaces::Model, uid: i64, ) -> Result<(), SpaceError> { match AllowJoinLevel::from_i16(space.allow_join_level) { AllowJoinLevel::None => {} AllowJoinLevel::NeedMailActiveAndPhoneActive | AllowJoinLevel::NeedMailActive | AllowJoinLevel::NeedMailActiveAndLastOneMid | AllowJoinLevel::NeedMailActiveAndLastOneHours => { if !crate::authority::has_verified_email(&self.accounts, uid).await? { return Err(SpaceError::JoinRequirementNotMet( "an activated email address is required".to_string(), )); } let now = OffsetDateTime::now_utc(); let created_at = crate::authority::account_created_at(&self.accounts, uid).await?; match AllowJoinLevel::from_i16(space.allow_join_level) { AllowJoinLevel::NeedMailActiveAndLastOneMid if created_at.is_some_and(|created| created > now - Duration::days(1)) => { return Err(SpaceError::JoinRequirementNotMet( "the account must be at least one day old".to_string(), )); } AllowJoinLevel::NeedMailActiveAndLastOneHours if created_at.is_some_and(|created| created > now - Duration::hours(1)) => { return Err(SpaceError::JoinRequirementNotMet( "the account must be at least one hour old".to_string(), )); } _ => {} } } } if space.member_must_enable_2fa && !crate::authority::mfa_enabled(&self.accounts, uid).await? { return Err(SpaceError::JoinRequirementNotMet( "two-factor authentication is required".to_string(), )); } Ok(()) } pub async fn require_session( &self, session: &session::Session, _request_id: Option<&str>, ) -> Result { let user_id = session.user_id()?.ok_or(SpaceError::Unauthorized)?; let version = session .security_version()? .ok_or(SpaceError::Unauthorized)?; let id = session .get::("auth_session_id")? .ok_or(SpaceError::Unauthorized)?; self.accounts.get_session(user_id, version, &id).await?; Ok(user_id) } pub(crate) async fn recipient_locale(&self, uid: Option) -> i18n::UiLocale { let fallback = i18n::config_fallback(self.settings.email.default_locale()); match uid { Some(uid) => i18n::resolve_user_locale(self.accounts.db().read(), uid, fallback).await, None => fallback, } } }