main
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<i64, SpaceError> {
let user_id = session.user_id()?.ok_or(SpaceError::Unauthorized)?;
let version = session
.security_version()?
.ok_or(SpaceError::Unauthorized)?;
let id = session
.get::<String>("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<i64>) -> 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,
}
}
}