sofie_tenancy/
account_data.rs1use 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
14pub 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 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
129struct 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
155struct 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}