use crate::error::{ConflictReason, InvalidRequestReason};
use rand_core::CryptoRng;
use time::OffsetDateTime;
use super::{Client, Effect, LinkKind, Refusal, Transition, UNEXPECTED, send_link};
use crate::Error;
use crate::account::{Account, Email, NewAccount, Role};
use crate::config::Config;
use crate::ids::UserId;
use crate::session::{Access, Session};
pub const MAX_NAME_LEN: usize = 100;
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Invite {
Start,
Sent {
account: NewAccount,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum InviteEvent {
Send {
inviter: Account,
session: Session,
email: Email,
display_name: Option<String>,
email_taken: bool,
client: Client,
},
}
impl Invite {
pub fn advance<R: CryptoRng + ?Sized>(
self,
config: &Config,
event: InviteEvent,
now: OffsetDateTime,
rng: &mut R,
) -> Result<Transition<Invite>, Refusal> {
let (
Invite::Start,
InviteEvent::Send {
inviter,
session,
email,
display_name,
email_taken,
client,
},
) = (self, event)
else {
return Err(UNEXPECTED.into());
};
if session.user_id != inviter.id {
return Err(Error::Unauthorized.into());
}
session.authorize(inviter.active, Access::Recent, config.step_up_window(), now)?;
if inviter.role != Role::Owner {
return Err(Error::Forbidden.into());
}
let display_name = optional_name(display_name)?.unwrap_or_else(|| email.to_string());
if email_taken {
return Err(Error::Conflict(ConflictReason::EmailTaken).into());
}
let account = NewAccount {
id: UserId::random(rng),
email,
display_name,
role: Role::Member,
created_at: now,
};
let mut effects = vec![Effect::CreateAccount(account.clone())];
effects.extend(send_link(
config,
&account.to_account(),
LinkKind::Invite,
None,
&client,
now,
rng,
));
Ok(Transition {
state: Invite::Sent { account },
effects,
})
}
}
pub(crate) fn optional_name(raw: Option<String>) -> Result<Option<String>, Error> {
match raw.map(|name| name.trim().to_owned()) {
Some(name) if name.chars().count() > MAX_NAME_LEN => {
Err(Error::InvalidRequest(InvalidRequestReason::NameTooLong))
}
Some(name) if name.is_empty() => Ok(None),
other => Ok(other),
}
}