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