Skip to main content

sofie_identity/membership/
adding.rs

1use sofie::db::{Outcome, Plan, Stamp};
2use sofie::prelude::*;
3
4use super::{Membership, factory, held};
5use crate::account::{ALREADY_REGISTERED, Account, Distrusted, EMAIL_INDEX};
6
7const MEMBERSHIP_INDEX: &str = "membership_account_area";
8const MEMBER_ALREADY: &str = "is a member already";
9
10/// An account made a member of an area from a task or a seed, as the first staff member of a
11/// `Permitted` area is, whose sign-up admits nobody to anything.
12pub struct MemberAdded {
13    membership: Membership,
14    account: Adopted,
15}
16
17enum Adopted {
18    Created(Account),
19    Distrusted(Account, Distrusted),
20    Kept,
21}
22
23impl MemberAdded {
24    #[must_use]
25    pub fn account(&self) -> crate::AccountId {
26        self.membership.account
27    }
28
29    #[cfg(feature = "permissions")]
30    pub async fn with_role(
31        mut self,
32        read: &Read,
33        clock: &Clock,
34        role: crate::RoleId,
35    ) -> Result<Self> {
36        if matches!(self.account, Adopted::Kept)
37            && super::roles::gives(&self.membership, Some(role))
38            && let Some((account, distrusted)) =
39                super::roles::unproven(read, self.membership.account, clock).await?
40        {
41            self.account = Adopted::Distrusted(account, distrusted);
42        }
43        self.membership.role = Some(role);
44        Ok(self)
45    }
46}
47
48/// An address no account holds gets one without a password, which signs in once its owner
49/// sets one (`password-reset`), follows a sign-in link or connects a provider. An account
50/// already a member keeps its membership, so adding it again changes nothing but a role.
51/// An account adopted, or given a role, whose address nobody proved may have been signed up
52/// by someone else expecting this, so it is distrusted.
53pub async fn add_member(
54    read: &Read,
55    clock: &Clock,
56    email: Email,
57    area: &str,
58) -> Result<MemberAdded> {
59    if area.trim().is_empty() {
60        return Err(Error::invalid("area", "is required"));
61    }
62    let Some(mut existing) = Account::of_email(read, email.clone()).await? else {
63        let account = crate::account::factory::without_password(email)?;
64        let membership = factory::grant(account.id(), area)?;
65        return Ok(MemberAdded {
66            membership,
67            account: Adopted::Created(account),
68        });
69    };
70    if let Some(membership) = held(read, existing.id(), area).await? {
71        return Ok(MemberAdded {
72            membership,
73            account: Adopted::Kept,
74        });
75    }
76    let membership = factory::grant(existing.id(), area)?;
77    if existing.is_confirmed() {
78        return Ok(MemberAdded {
79            membership,
80            account: Adopted::Kept,
81        });
82    }
83    let distrusted = existing.distrust(read, clock.now()).await?;
84    Ok(MemberAdded {
85        membership,
86        account: Adopted::Distrusted(existing, distrusted),
87    })
88}
89
90#[derive(Input)]
91pub struct AddingMember {
92    email: Email,
93    #[max(64)]
94    area: String,
95    #[max(200)]
96    role: Option<String>,
97}
98
99/// For an app's tasks: `Tasks::new().task("sofie_identity.add_member", add_member_task)`,
100/// then `<app> task sofie_identity.add_member --email … --area … [--role …]`.
101pub async fn add_member_task(
102    adding: AddingMember,
103    read: Read,
104    clock: Clock,
105) -> Result<MemberAdded> {
106    let added = add_member(&read, &clock, adding.email, &adding.area).await?;
107    let Some(role) = adding.role else {
108        return Ok(added);
109    };
110    #[cfg(feature = "permissions")]
111    return added
112        .with_role(&read, &clock, crate::role_named(&read, &role).await?)
113        .await;
114    #[cfg(not(feature = "permissions"))]
115    {
116        let _ = (added, role);
117        Err(Error::invalid(
118            "role",
119            "needs sofie-identity's `permissions`",
120        ))
121    }
122}
123
124impl Outcome for MemberAdded {
125    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
126        match &self.account {
127            Adopted::Created(account) => created(plan, account)?,
128            Adopted::Distrusted(account, distrusted) => {
129                created(plan, account)?;
130                distrusted.plan(plan)?;
131            }
132            Adopted::Kept => {}
133        }
134        plan.unique(MEMBERSHIP_INDEX, "area", MEMBER_ALREADY);
135        self.membership.plan(plan)
136    }
137
138    fn committed(&mut self, stamp: Stamp) {
139        match &mut self.account {
140            Adopted::Created(account) => account.committed(stamp),
141            Adopted::Distrusted(account, distrusted) => {
142                account.committed(stamp);
143                distrusted.committed(stamp);
144            }
145            Adopted::Kept => {}
146        }
147        self.membership.committed(stamp);
148    }
149}
150
151fn created(plan: &mut Plan<'_>, account: &Account) -> Result<()> {
152    plan.unique(EMAIL_INDEX, "email", ALREADY_REGISTERED);
153    account.plan(plan)
154}