sofie_identity/account_data/
mod.rs1use std::sync::Arc;
6
7use sofie::db::{BoxFuture, Db, Extensions, Outcome, Plan, Reply, Stamp, Uuid};
8use sofie::http::{Routes, Shipped};
9use sofie::prelude::*;
10use sofie::serde_json::{Map, Value, json};
11
12use crate::account::Account;
13pub use crate::account::Closable;
14use crate::membership::Membership;
15use crate::signing_in::OwnSession;
16use crate::{AccountId, MemberArea, Unscoped, account_of};
17
18#[cfg(feature = "notify")]
19pub use notifications::Notifications;
20
21const CONFIRM: &str = "is not this account's address";
22
23pub trait PersonalData: Send + Sync + 'static {
25 fn name(&self) -> &'static str;
26
27 fn export<'a>(&'a self, read: &'a Read, account: Uuid) -> BoxFuture<'a, Result<Value>>;
28
29 fn closing<'a>(
32 &'a self,
33 read: &'a Read,
34 account: Uuid,
35 ) -> BoxFuture<'a, Result<Vec<Box<dyn Closable>>>> {
36 let _ = (read, account);
37 Box::pin(std::future::ready(Ok(Vec::new())))
38 }
39}
40
41pub fn register(db: &Db, data: Arc<dyn PersonalData>) {
42 sofie::db::extend(db, data);
43}
44
45#[derive(Event)]
47pub struct AccountClosed {
48 pub account: Uuid,
49}
50
51pub fn routes<A: MemberArea>(routes: &mut Routes<A>) {
53 let describe: sofie::http::Describe =
54 |screens, key| screens.describe_form::<Closing>(key, "close-account");
55 routes
56 .shipped(
57 "/account-data",
58 "account_data",
59 (include_str!("screen.html"), Some(describe)),
60 page,
61 )
62 .name("account_data_page");
63 routes
64 .post("/account-data/export", export::<A>)
65 .name("export_account_data");
66 routes
67 .post("/account-data/close", close::<A>)
68 .name("close_account")
69 .then_redirect(A::SIGN_IN);
70}
71
72#[derive(ReadModel)]
73pub struct Page {}
74
75async fn page() -> Result<Page> {
76 Ok(Page {})
77}
78
79pub struct Exported(Value);
80
81impl Outcome for Exported {
82 fn reply(self) -> Result<Reply> {
83 Ok(Reply::Json(self.0))
84 }
85}
86
87async fn export<A: MemberArea>(
88 principal: Principal<A>,
89 _own: OwnSession,
90 read: Read,
91 others: Extensions<dyn PersonalData>,
92) -> Result<Exported> {
93 let account = account_of(&principal);
94 let mut exported = Map::new();
95 exported.insert("sofie_identity".to_owned(), own(&read, account).await?);
96 for data in others.iter() {
97 exported.insert(
98 data.name().to_owned(),
99 data.export(&read, account.uuid()).await?,
100 );
101 }
102 Ok(Exported(Value::Object(exported)))
103}
104
105async fn own(read: &Read, account: AccountId) -> Result<Value> {
106 let held = Account::stored(read, account).await?;
107 let memberships = Membership::query()
108 .where_(Membership::ACCOUNT.eq(account))
109 .all(read)
110 .await?;
111 #[cfg_attr(
112 not(any(
113 feature = "sessions",
114 feature = "passkeys",
115 feature = "oauth",
116 feature = "totp",
117 feature = "tokens",
118 feature = "impersonation"
119 )),
120 expect(unused_mut)
121 )]
122 let mut own = json!({
123 "account": {
124 "email": held.email().as_str(),
125 "created_at": held.created_at(),
126 "verified_at": held.verified_at(),
127 },
128 "memberships": memberships.iter().map(|membership| json!({
129 "area": membership.area(),
130 "created_at": membership.created_at(),
131 })).collect::<Vec<_>>(),
132 });
133 #[cfg(feature = "sessions")]
134 {
135 own["sessions"] = crate::sessions::exported(read, account).await?;
136 }
137 #[cfg(feature = "passkeys")]
138 {
139 own["passkeys"] = crate::passkeys::exported(read, account).await?;
140 }
141 #[cfg(feature = "oauth")]
142 {
143 own["connections"] = crate::oauth::exported(read, account).await?;
144 }
145 #[cfg(feature = "totp")]
146 {
147 own["second_factor"] = crate::two_factor::exported(read, account).await?;
148 }
149 #[cfg(feature = "tokens")]
150 {
151 own["tokens"] = crate::tokens::exported(read, account).await?;
152 }
153 #[cfg(feature = "impersonation")]
154 {
155 own["impersonations"] = crate::impersonation::exported(read, account).await?;
156 }
157 Ok(own)
158}
159
160#[derive(Input)]
161pub struct Closing {
162 email: Email,
164}
165
166struct Closed {
167 account: Account,
168 records: Vec<Box<dyn Closable>>,
169}
170
171impl Outcome for Closed {
172 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
173 Outcome::plan(&self.account, plan)?;
174 self.records
175 .iter()
176 .try_for_each(|record| record.plan(plan))?;
177 sofie::db::forget_principal(plan, self.account.id().uuid());
178 Ok(())
179 }
180
181 fn committed(&mut self, stamp: Stamp) {
182 Outcome::committed(&mut self.account, stamp);
183 for record in &mut self.records {
184 record.committed(stamp);
185 }
186 }
187}
188
189async fn close<A: MemberArea>(
191 principal: Principal<A>,
192 _own: OwnSession,
193 closing: Closing,
194 read: Read,
195 unscoped: Unscoped,
196 others: Extensions<dyn PersonalData>,
197 session: Session,
198) -> Result<()> {
199 let id = account_of(&principal);
200 let mut account = Account::stored(&read, id).await?;
201 if *account.email() != closing.email {
202 return Err(Error::invalid("email", CONFIRM));
203 }
204 account.close()?;
205 account.raise(AccountClosed { account: id.uuid() })?;
206 let mut records: Vec<Box<dyn Closable>> = Vec::new();
207 #[cfg(feature = "account-links")]
208 records.push(Box::new(
209 crate::account_link::closing(&read, id, unscoped.0.clock().now()).await?,
210 ));
211 #[cfg(feature = "sessions")]
212 records.push(Box::new(
213 crate::sessions::closing(&unscoped.0.read(), id, unscoped.0.clock().now()).await?,
214 ));
215 #[cfg(feature = "passkeys")]
216 records.push(Box::new(crate::passkeys::removing(&read, id).await?));
217 #[cfg(feature = "oauth")]
218 records.push(Box::new(crate::oauth::removing(&read, id).await?));
219 #[cfg(feature = "totp")]
220 records.push(Box::new(crate::two_factor::removing(&read, id).await?));
221 #[cfg(feature = "tokens")]
222 records.push(Box::new(crate::tokens::revoking(&read, id).await?));
223 for data in others.iter() {
224 records.extend(data.closing(&read, id.uuid()).await?);
225 }
226 unscoped.0.commit(&mut Closed { account, records }).await?;
227 session.sign_out();
228 Ok(())
229}
230
231#[cfg(feature = "notify")]
232mod notifications {
233 use sofie::db::{BoxFuture, Db, LocalJobs, Reach, Uuid};
234 use sofie::prelude::*;
235 use sofie::serde_json::Value;
236
237 use super::{Closable, PersonalData};
238
239 pub struct Notifications(Db);
242
243 impl Notifications {
244 #[must_use]
245 pub fn new(db: &Db) -> Self {
246 Self(db.clone())
247 }
248 }
249
250 impl PersonalData for Notifications {
251 fn name(&self) -> &'static str {
252 "sofie_notify"
253 }
254
255 fn export<'a>(&'a self, read: &'a Read, account: Uuid) -> BoxFuture<'a, Result<Value>> {
256 Box::pin(sofie::notify::personal_data::exported(read, account))
257 }
258
259 fn closing<'a>(
260 &'a self,
261 _read: &'a Read,
262 account: Uuid,
263 ) -> BoxFuture<'a, Result<Vec<Box<dyn Closable>>>> {
264 Box::pin(async move {
265 let jobs = LocalJobs::within(&self.0, Reach::OwnedBy(account));
266 let forgotten = sofie::notify::personal_data::forgetting(&jobs, account).await?;
267 let closing: Vec<Box<dyn Closable>> = vec![Box::new(forgotten)];
268 Ok(closing)
269 })
270 }
271 }
272}