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 mut closing = held
60                .into_iter()
61                .map(|mut member| {
62                    member.leave()?;
63                    let organization = member.organization();
64                    let left: Box<dyn Closable> =
65                        Box::new(Held::new(organization, Kept::AnOwner, Left(member)));
66                    Ok(left)
67                })
68                .collect::<Result<Vec<_>>>()?;
69            closing.push(Box::new(Withdrawn(AccountId::from_uuid(account))));
70            Ok(closing)
71        })
72    }
73}
74
75async fn of_account(read: &Read, account: Uuid) -> Result<Vec<OrganizationMember>> {
76    OrganizationMember::query()
77        .where_(OrganizationMember::ACCOUNT.eq(AccountId::from_uuid(account)))
78        .all(read)
79        .await
80}
81
82async fn owned_alone(read: &Read, held: &[OrganizationMember]) -> Result<Vec<OrganizationId>> {
83    let owned: Vec<OrganizationId> = held
84        .iter()
85        .filter(|member| member.role() == OrganizationRole::Owner)
86        .map(OrganizationMember::organization)
87        .collect();
88    if owned.is_empty() {
89        return Ok(owned);
90    }
91    let owners = OrganizationMember::query()
92        .where_(OrganizationMember::ORGANIZATION.in_(owned.iter().copied()))
93        .where_(OrganizationMember::ROLE.eq(OrganizationRole::Owner))
94        .all(read)
95        .await?;
96    let mut counted: HashMap<OrganizationId, usize> = HashMap::new();
97    for owner in &owners {
98        *counted.entry(owner.organization()).or_default() += 1;
99    }
100    Ok(owned
101        .into_iter()
102        .filter(|organization| counted.get(organization).copied().unwrap_or(0) <= 1)
103        .collect())
104}
105
106async fn last_owner_of(read: &Read, organizations: Vec<OrganizationId>) -> Result<Error> {
107    let organizations = Organization::query()
108        .where_(Organization::ID.in_(organizations))
109        .all(read)
110        .await?;
111    let mut names: Vec<&str> = organizations.iter().map(Organization::name).collect();
112    names.sort_unstable();
113    Ok(Error::Rule(format!(
114        "This account is the last owner of {}; hand each over to another member or delete it \
115         first.",
116        names.join(", ")
117    )))
118}
119
120/// Written as a statement: the closing commits within the account's reach, which the
121/// organization's membership is beyond. Holds the member only for the event its leaving raised.
122struct Left(OrganizationMember);
123
124impl Outcome for Left {
125    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
126        let dialect = plan.dialect();
127        let [deleted_at, updated_at, version] =
128            ["deleted_at", "updated_at", "version"].map(|column| dialect.quote(column));
129        let sql = format!(
130            "UPDATE {} SET {deleted_at} = {now}, {updated_at} = {now}, {version} = {version} + 1 \
131             WHERE {} = {} AND {deleted_at} IS NULL",
132            dialect.qualified(OrganizationMember::MODULE, OrganizationMember::NAME),
133            dialect.quote("id"),
134            dialect.placeholder(1),
135            now = dialect.placeholder(2),
136        );
137        let mut params = vec![SqlValue::Uuid(self.0.id().uuid())];
138        plan.stamp().now.encode(&mut params);
139        plan.push_as::<OrganizationMember>(Step::statement(sql, params, Some(1)));
140        plan.raise(self.0.meta().events());
141        Ok(())
142    }
143}
144
145/// The open invitations the account sent, withdrawn, so none it no longer stands behind joins
146/// anybody or holds a seat.
147struct Withdrawn(AccountId);
148
149impl Outcome for Withdrawn {
150    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
151        let dialect = plan.dialect();
152        let [expires_at, updated_at, version] =
153            ["expires_at", "updated_at", "version"].map(|column| dialect.quote(column));
154        let sql = format!(
155            "UPDATE {} SET {expires_at} = {now}, {updated_at} = {now}, {version} = {version} + 1 \
156             WHERE {} = {} AND {} IS NULL AND {expires_at} > {now}",
157            dialect.qualified(crate::Invitation::MODULE, crate::Invitation::NAME),
158            dialect.quote("invited_by"),
159            dialect.placeholder(1),
160            dialect.quote("accepted_at"),
161            now = dialect.placeholder(2),
162        );
163        let mut params = vec![SqlValue::Uuid(self.0.uuid())];
164        plan.stamp().now.encode(&mut params);
165        plan.push_as::<crate::Invitation>(Step::statement(sql, params, None));
166        Ok(())
167    }
168}