Skip to main content

sofie_tenancy/
account_data.rs

1use std::collections::HashMap;
2
3use sofie::core::{Cast, SqlValue};
4use sofie::db::{Aggregate, BoxFuture, Db, Outcome, Plan, Step, Uuid};
5use sofie::prelude::*;
6use sofie::serde_json::{Value, json};
7use sofie_identity::AccountId;
8use sofie_identity::account_data::{Closable, PersonalData};
9
10use crate::organization::{
11    Held, Kept, Organization, OrganizationId, OrganizationMember, OrganizationRole,
12};
13
14/// For `sofie_identity::account_data::register`. The organizations own the memberships, so
15/// they are read beyond the closing account's reach.
16pub struct OrganizationMemberships(Db);
17
18impl OrganizationMemberships {
19    #[must_use]
20    pub fn new(db: &Db) -> Self {
21        Self(db.clone())
22    }
23}
24
25impl PersonalData for OrganizationMemberships {
26    fn name(&self) -> &'static str {
27        "sofie_tenancy"
28    }
29
30    fn export<'a>(&'a self, _read: &'a Read, account: Uuid) -> BoxFuture<'a, Result<Value>> {
31        Box::pin(async move {
32            let held = of_account(&self.0.read(), account).await?;
33            Ok(json!(
34                held.iter()
35                    .map(|member| json!({
36                        "organization": member.organization().uuid(),
37                        "role": member.role().name(),
38                    }))
39                    .collect::<Vec<_>>()
40            ))
41        })
42    }
43
44    fn closing<'a>(
45        &'a self,
46        _read: &'a Read,
47        account: Uuid,
48    ) -> BoxFuture<'a, Result<Vec<Box<dyn Closable>>>> {
49        Box::pin(async move {
50            let read = self.0.read();
51            let held = of_account(&read, account).await?;
52            let alone = owned_alone(&read, &held).await?;
53            if !alone.is_empty() {
54                return Err(last_owner_of(&read, alone).await?);
55            }
56            // In one order, so two accounts closing at once never take each other's rows.
57            let mut held = held;
58            held.sort_by_key(|member| member.organization().uuid());
59            let organizations = held.iter().map(OrganizationMember::organization).collect();
60            let mut closing = held
61                .into_iter()
62                .map(|mut member| {
63                    member.leave()?;
64                    let organization = member.organization();
65                    let left: Box<dyn Closable> =
66                        Box::new(Held::new(organization, Kept::AnOwner, Left(member)));
67                    Ok(left)
68                })
69                .collect::<Result<Vec<_>>>()?;
70            closing.push(Box::new(Withdrawn {
71                account: AccountId::from_uuid(account),
72                organizations,
73            }));
74            Ok(closing)
75        })
76    }
77}
78
79async fn of_account(read: &Read, account: Uuid) -> Result<Vec<OrganizationMember>> {
80    OrganizationMember::query()
81        .where_(OrganizationMember::ACCOUNT.eq(AccountId::from_uuid(account)))
82        .all(read)
83        .await
84}
85
86async fn owned_alone(read: &Read, held: &[OrganizationMember]) -> Result<Vec<OrganizationId>> {
87    let owned: Vec<OrganizationId> = held
88        .iter()
89        .filter(|member| member.role() == OrganizationRole::Owner)
90        .map(OrganizationMember::organization)
91        .collect();
92    if owned.is_empty() {
93        return Ok(owned);
94    }
95    let owners = OrganizationMember::query()
96        .where_(OrganizationMember::ORGANIZATION.in_(owned.iter().copied()))
97        .where_(OrganizationMember::ROLE.eq(OrganizationRole::Owner))
98        .all(read)
99        .await?;
100    let mut counted: HashMap<OrganizationId, usize> = HashMap::new();
101    for owner in &owners {
102        *counted.entry(owner.organization()).or_default() += 1;
103    }
104    Ok(owned
105        .into_iter()
106        .filter(|organization| counted.get(organization).copied().unwrap_or(0) <= 1)
107        .collect())
108}
109
110async fn last_owner_of(read: &Read, organizations: Vec<OrganizationId>) -> Result<Error> {
111    let organizations = Organization::query()
112        .where_(Organization::ID.in_(organizations))
113        .all(read)
114        .await?;
115    let mut names: Vec<&str> = organizations.iter().map(Organization::name).collect();
116    names.sort_unstable();
117    let listed = names.join(", ");
118    let message = format!(
119        "This account is the last owner of {listed}; hand each over to another member or delete \
120         it first."
121    );
122    Ok(Error::rule_keyed(
123        "tenancy.rule.last_owner_of",
124        [("organizations", listed)],
125        message,
126    ))
127}
128
129/// Written as a statement: the closing commits within the account's reach, which the
130/// organization's membership is beyond. Holds the member only for the event its leaving raised.
131struct Left(OrganizationMember);
132
133impl Outcome for Left {
134    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
135        let dialect = plan.dialect();
136        let [deleted_at, updated_at, version] =
137            ["deleted_at", "updated_at", "version"].map(|column| dialect.quote(column));
138        let sql = format!(
139            "UPDATE {} SET {deleted_at} = {now}, {updated_at} = {now}, {version} = {version} + 1 \
140             WHERE {} = {} AND {deleted_at} IS NULL",
141            dialect.qualified(OrganizationMember::MODULE, OrganizationMember::NAME),
142            dialect.quote("id"),
143            dialect.placeholder(1),
144            now = dialect.placeholder(2),
145        );
146        let mut params = vec![SqlValue::Uuid(self.0.id().uuid())];
147        plan.stamp().now.encode(&mut params);
148        plan.push_as::<OrganizationMember>(Step::statement(sql, params, Some(1)));
149        plan.touched::<OrganizationMember>(Some(self.0.organization().uuid()));
150        plan.raise(self.0.meta().events());
151        Ok(())
152    }
153}
154
155/// The open invitations the account sent, withdrawn, so none it no longer stands behind joins
156/// anybody or holds a seat. Its invitations lie in the organizations it belongs to, since leaving
157/// or losing a role withdraws the rest; each is told, as the closing's reach owns none of them.
158struct Withdrawn {
159    account: AccountId,
160    organizations: Vec<OrganizationId>,
161}
162
163impl Outcome for Withdrawn {
164    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
165        let dialect = plan.dialect();
166        let [expires_at, updated_at, version] =
167            ["expires_at", "updated_at", "version"].map(|column| dialect.quote(column));
168        let sql = format!(
169            "UPDATE {} SET {expires_at} = {now}, {updated_at} = {now}, {version} = {version} + 1 \
170             WHERE {} = {} AND {} IS NULL AND {expires_at} > {now}",
171            dialect.qualified(crate::Invitation::MODULE, crate::Invitation::NAME),
172            dialect.quote("invited_by"),
173            dialect.placeholder(1),
174            dialect.quote("accepted_at"),
175            now = dialect.placeholder(2),
176        );
177        let mut params = vec![SqlValue::Uuid(self.account.uuid())];
178        plan.stamp().now.encode(&mut params);
179        plan.push_as::<crate::Invitation>(Step::statement(sql, params, None));
180        for organization in &self.organizations {
181            plan.touched::<crate::Invitation>(Some(organization.uuid()));
182        }
183        Ok(())
184    }
185}