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