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 pub(crate) async fn stored(read: &Read, id: crate::AccountId) -> Result<Self> {
34 Self::query()
35 .where_(Self::ID.eq(id))
36 .first(read)
37 .await?
38 .ok_or(Error::NotFound)
39 }
40
41 pub(crate) async fn of_email(read: &Read, email: Email) -> Result<Option<Self>> {
42 Self::query()
43 .where_(Self::EMAIL.eq(email))
44 .first(read)
45 .await
46 }
47
48 #[cfg(any(feature = "oauth", feature = "impersonation"))]
49 pub(crate) fn is_closed(&self) -> bool {
50 self.email.as_str().ends_with(CLOSED_DOMAIN)
51 }
52
53 #[must_use]
54 pub fn email(&self) -> &Email {
55 &self.email
56 }
57
58 #[must_use]
59 pub fn verified_at(&self) -> Option<Instant> {
60 self.verified_at
61 }
62
63 pub(crate) fn password_hash(&self) -> &PasswordHash {
64 &self.password_hash
65 }
66
67 pub(crate) fn change_password(&mut self, hash: PasswordHash) {
68 self.password_hash = hash;
69 }
70
71 pub fn confirm_email(&mut self, now: Instant) {
72 self.verified_at.get_or_insert(now);
73 }
74
75 #[cfg(feature = "email-verification")]
77 pub(crate) fn asking_verification<A: crate::MemberArea>(mut self) -> Result<Self> {
78 let purpose = crate::account_link::LinkPurpose::EmailVerification;
79 let asked = crate::mailed::LinkAsked::of::<A>(self.email.clone(), purpose);
80 self.meta.raise(asked)?;
81 Ok(self)
82 }
83
84 #[cfg(feature = "account-data")]
87 pub(crate) fn close(&mut self) -> Result<()> {
88 self.email = Email::parse(&format!("closed-{}{CLOSED_DOMAIN}", self.id().uuid()))?;
89 self.password_hash = PasswordHash::unmatchable()?;
90 self.verified_at = None;
91 Ok(())
92 }
93
94 #[cfg(feature = "account-data")]
95 pub(crate) fn raise<E: sofie::db::Event>(&mut self, event: E) -> Result<()> {
96 self.meta.raise(event)
97 }
98}
99
100pub trait Closable: Send {
101 fn plan(&self, plan: &mut Plan<'_>) -> Result<()>;
102 fn committed(&mut self, stamp: Stamp);
103}
104
105impl<T: Outcome> Closable for T {
106 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
107 Outcome::plan(self, plan)
108 }
109
110 fn committed(&mut self, stamp: Stamp) {
111 Outcome::committed(self, stamp);
112 }
113}
114
115pub(crate) struct Distrusted(Vec<Box<dyn Closable>>);
117
118impl Outcome for Distrusted {
119 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
120 self.0.iter().try_for_each(|record| record.plan(plan))
121 }
122
123 fn committed(&mut self, stamp: Stamp) {
124 for record in &mut self.0 {
125 record.committed(stamp);
126 }
127 }
128}
129
130impl Account {
131 pub(crate) fn is_confirmed(&self) -> bool {
132 self.verified_at.is_some()
133 }
134
135 #[cfg(any(feature = "password-reset", feature = "sign-in-links"))]
136 pub(crate) async fn prove_email(
137 &mut self,
138 read: &Read,
139 now: Instant,
140 ) -> Result<Option<Distrusted>> {
141 if self.is_confirmed() {
142 return Ok(None);
143 }
144 self.confirm_email(now);
145 self.distrust(read, now).await.map(Some)
146 }
147
148 #[cfg(feature = "email-verification")]
151 pub(crate) async fn confirm_unproven(
152 &mut self,
153 read: &Read,
154 now: Instant,
155 ) -> Result<Option<Distrusted>> {
156 if self.is_confirmed() {
157 return Ok(None);
158 }
159 self.confirm_email(now);
160 let records = self.added_ways_in(read).await?;
161 Ok(Some(Distrusted(records)))
162 }
163
164 #[cfg_attr(not(any(feature = "sessions", feature = "tokens")), expect(unused_mut))]
166 pub(crate) async fn distrust(&mut self, read: &Read, now: Instant) -> Result<Distrusted> {
167 self.password_hash = PasswordHash::unmatchable()?;
168 let id = self.id();
169 let mut records = self.added_ways_in(read).await?;
170 #[cfg(feature = "sessions")]
171 records.push(Box::new(
172 crate::sessions::ending_everywhere(read, id, now).await?,
173 ));
174 #[cfg(feature = "tokens")]
175 records.push(Box::new(crate::tokens::revoking(read, id).await?));
176 #[cfg(not(feature = "sessions"))]
177 let _ = now;
178 #[cfg(not(any(feature = "tokens", feature = "sessions")))]
179 let _ = id;
180 Ok(Distrusted(records))
181 }
182
183 #[cfg_attr(
185 not(any(feature = "passkeys", feature = "oauth", feature = "totp")),
186 expect(clippy::unused_async_trait_impl, unused_mut)
187 )]
188 async fn added_ways_in(&self, read: &Read) -> Result<Vec<Box<dyn Closable>>> {
189 let id = self.id();
190 let mut records: Vec<Box<dyn Closable>> = Vec::new();
191 #[cfg(feature = "passkeys")]
192 records.push(Box::new(crate::passkeys::removing(read, id).await?));
193 #[cfg(feature = "oauth")]
194 records.push(Box::new(crate::oauth::removing(read, id).await?));
195 #[cfg(feature = "totp")]
196 records.push(Box::new(crate::two_factor::removing(read, id).await?));
197 #[cfg(not(any(feature = "passkeys", feature = "oauth", feature = "totp")))]
198 let _ = (read, id);
199 Ok(records)
200 }
201}