Skip to main content

sofie_tenancy/
invitation.rs

1use std::time::Duration;
2
3use sofie::core::Instant;
4use sofie::db::{Query, Source};
5use sofie::prelude::*;
6
7use sofie_identity::AccountId;
8
9use crate::organization::{OrganizationId, OrganizationRole};
10
11/// Long enough for a colleague back from a week away.
12pub(crate) const VALID_FOR: Duration = Duration::from_hours(7 * 24);
13pub(crate) const NOT_OPEN: &str = "This invitation was accepted already or has expired.";
14
15/// A role offered to whoever holds the mailed link and signs in with its address.
16#[derive(Aggregate)]
17pub struct Invitation {
18    meta: Meta,
19    #[owner]
20    organization: OrganizationId,
21    email: Email,
22    role: OrganizationRole,
23    invited_by: AccountId,
24    expires_at: Instant,
25    accepted_at: Option<Instant>,
26}
27
28impl Invitation {
29    pub(crate) fn new(
30        organization: OrganizationId,
31        email: Email,
32        role: OrganizationRole,
33        invited_by: AccountId,
34        now: Instant,
35    ) -> Result<Self> {
36        Ok(Self {
37            meta: Meta::new()?,
38            organization,
39            email,
40            role,
41            invited_by,
42            expires_at: now.checked_add(VALID_FOR)?,
43            accepted_at: None,
44        })
45    }
46
47    #[must_use]
48    pub fn organization(&self) -> OrganizationId {
49        self.organization
50    }
51
52    #[must_use]
53    pub fn email(&self) -> &Email {
54        &self.email
55    }
56
57    #[must_use]
58    pub fn role(&self) -> OrganizationRole {
59        self.role
60    }
61
62    pub(crate) fn invited_by(&self) -> AccountId {
63        self.invited_by
64    }
65
66    #[must_use]
67    pub fn expires_at(&self) -> Instant {
68        self.expires_at
69    }
70
71    #[must_use]
72    pub fn is_open(&self, now: Instant) -> bool {
73        self.accepted_at.is_none() && self.expires_at > now
74    }
75
76    /// Expired at once, so its link opens nothing and its seat is free.
77    pub(crate) fn withdraw(&mut self, now: Instant) -> Result<()> {
78        if !self.is_open(now) {
79            return Err(Error::Rule(NOT_OPEN.to_owned()));
80        }
81        self.expires_at = now;
82        Ok(())
83    }
84
85    pub(crate) fn accept(&mut self, now: Instant) -> Result<()> {
86        if !self.is_open(now) {
87            return Err(Error::Rule(NOT_OPEN.to_owned()));
88        }
89        self.accepted_at = Some(now);
90        Ok(())
91    }
92}
93
94/// Those still waiting, which hold a seat as a member does.
95pub(crate) fn pending<T: Source<Base = Invitation>>(
96    query: Query<T>,
97    organization: OrganizationId,
98    now: Instant,
99) -> Query<T> {
100    query
101        .where_(Invitation::ORGANIZATION.eq(organization))
102        .where_(Invitation::ACCEPTED_AT.is_null())
103        .where_(Invitation::EXPIRES_AT.gt(now))
104}