sofie_identity/account/
mod.rs1pub(crate) mod factory;
2
3use sofie::core::Instant;
4use sofie::db::{Outcome, Plan, Stamp};
5use sofie::prelude::*;
6
7use crate::PasswordHash;
8
9pub(crate) const EMAIL_INDEX: &str = "account_email";
10pub(crate) const ALREADY_REGISTERED: &str = "is already registered";
11#[cfg(any(feature = "account-data", feature = "oauth", feature = "impersonation"))]
12const CLOSED_DOMAIN: &str = "@closed.invalid";
13
14#[derive(Aggregate)]
15#[owner]
16pub struct Account {
17 meta: Meta,
18 email: Email,
19 password_hash: PasswordHash,
20 verified_at: Option<Instant>,
21}
22
23impl Account {
24 #[cfg(any(
25 feature = "links",
26 feature = "totp",
27 feature = "passkeys",
28 feature = "account-data",
29 feature = "impersonation",
30 feature = "oauth",
31 feature = "permissions"
32 ))]
33 #[cfg_attr(
34 not(any(
35 feature = "password-reset",
36 feature = "email-verification",
37 feature = "sign-in-links",
38 feature = "totp",
39 feature = "passkeys",
40 feature = "account-data",
41 feature = "impersonation",
42 feature = "oauth",
43 feature = "permissions"
44 )),
45 expect(
46 dead_code,
47 reason = "with `links` alone, only the unused mailed links call it"
48 )
49 )]
50 pub(crate) async fn stored(read: &Read, id: crate::AccountId) -> Result<Self> {
51 Self::query()
52 .where_(Self::ID.eq(id))
53 .first(read)
54 .await?
55 .ok_or(Error::NotFound)
56 }
57
58 pub(crate) async fn of_email(read: &Read, email: Email) -> Result<Option<Self>> {
59 Self::query()
60 .where_(Self::EMAIL.eq(email))
61 .first(read)
62 .await
63 }
64
65 #[cfg(any(feature = "oauth", feature = "impersonation"))]
66 pub(crate) fn is_closed(&self) -> bool {
67 self.email.as_str().ends_with(CLOSED_DOMAIN)
68 }
69
70 #[must_use]
71 pub fn email(&self) -> &Email {
72 &self.email
73 }
74
75 #[must_use]
76 pub fn verified_at(&self) -> Option<Instant> {
77 self.verified_at
78 }
79
80 pub(crate) fn password_hash(&self) -> &PasswordHash {
81 &self.password_hash
82 }
83
84 pub(crate) fn change_password(&mut self, hash: PasswordHash) {
85 self.password_hash = hash;
86 }
87
88 pub fn confirm_email(&mut self, now: Instant) {
89 self.verified_at.get_or_insert(now);
90 }
91
92 #[cfg(feature = "email-verification")]
94 pub(crate) fn asking_verification<A: crate::MemberArea>(mut self) -> Result<Self> {
95 let purpose = crate::account_link::LinkPurpose::EmailVerification;
96 let asked = crate::mailed::LinkAsked::of::<A>(self.email.clone(), purpose);
97 self.meta.raise(asked)?;
98 Ok(self)
99 }
100
101 #[cfg(feature = "account-data")]
104 pub(crate) fn close(&mut self) -> Result<()> {
105 self.email = Email::parse(&format!("closed-{}{CLOSED_DOMAIN}", self.id().uuid()))?;
106 self.password_hash = PasswordHash::unmatchable()?;
107 self.verified_at = None;
108 Ok(())
109 }
110
111 #[cfg(feature = "account-data")]
112 pub(crate) fn raise<E: sofie::db::Event>(&mut self, event: E) -> Result<()> {
113 self.meta.raise(event)
114 }
115}
116
117pub trait Closable: Send {
118 fn plan(&self, plan: &mut Plan<'_>) -> Result<()>;
119 fn committed(&mut self, stamp: Stamp);
120}
121
122impl<T: Outcome> Closable for T {
123 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
124 Outcome::plan(self, plan)
125 }
126
127 fn committed(&mut self, stamp: Stamp) {
128 Outcome::committed(self, stamp);
129 }
130}
131
132pub(crate) struct Distrusted(Vec<Box<dyn Closable>>);
134
135impl Outcome for Distrusted {
136 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
137 self.0.iter().try_for_each(|record| record.plan(plan))
138 }
139
140 fn committed(&mut self, stamp: Stamp) {
141 for record in &mut self.0 {
142 record.committed(stamp);
143 }
144 }
145}
146
147impl Account {
148 pub(crate) fn is_confirmed(&self) -> bool {
149 self.verified_at.is_some()
150 }
151
152 #[cfg(any(feature = "password-reset", feature = "sign-in-links"))]
153 pub(crate) async fn prove_email(
154 &mut self,
155 read: &Read,
156 now: Instant,
157 ) -> Result<Option<Distrusted>> {
158 if self.is_confirmed() {
159 return Ok(None);
160 }
161 self.confirm_email(now);
162 self.distrust(read, now).await.map(Some)
163 }
164
165 #[cfg(feature = "email-verification")]
168 pub(crate) async fn confirm_unproven(
169 &mut self,
170 read: &Read,
171 now: Instant,
172 ) -> Result<Option<Distrusted>> {
173 if self.is_confirmed() {
174 return Ok(None);
175 }
176 self.confirm_email(now);
177 let records = self.added_ways_in(read).await?;
178 Ok(Some(Distrusted(records)))
179 }
180
181 #[cfg_attr(not(any(feature = "sessions", feature = "tokens")), expect(unused_mut))]
183 pub(crate) async fn distrust(&mut self, read: &Read, now: Instant) -> Result<Distrusted> {
184 self.password_hash = PasswordHash::unmatchable()?;
185 let id = self.id();
186 let mut records = self.added_ways_in(read).await?;
187 #[cfg(feature = "sessions")]
188 records.push(Box::new(
189 crate::sessions::ending_all(read, id, None, now).await?,
190 ));
191 #[cfg(feature = "tokens")]
192 records.push(Box::new(crate::tokens::revoking(read, id).await?));
193 #[cfg(not(feature = "sessions"))]
194 let _ = now;
195 #[cfg(not(any(feature = "tokens", feature = "sessions")))]
196 let _ = id;
197 Ok(Distrusted(records))
198 }
199
200 #[cfg_attr(
202 not(any(feature = "passkeys", feature = "oauth", feature = "totp")),
203 expect(clippy::unused_async_trait_impl, unused_mut)
204 )]
205 async fn added_ways_in(&self, read: &Read) -> Result<Vec<Box<dyn Closable>>> {
206 let id = self.id();
207 let mut records: Vec<Box<dyn Closable>> = Vec::new();
208 #[cfg(feature = "passkeys")]
209 records.push(Box::new(crate::passkeys::removing(read, id).await?));
210 #[cfg(feature = "oauth")]
211 records.push(Box::new(crate::oauth::removing(read, id).await?));
212 #[cfg(feature = "totp")]
213 records.push(Box::new(crate::two_factor::removing(read, id).await?));
214 #[cfg(not(any(feature = "passkeys", feature = "oauth", feature = "totp")))]
215 let _ = (read, id);
216 Ok(records)
217 }
218}