1use chrono::Duration;
2use iso8601_timestamp::Timestamp;
3
4use crate::{
5 config::EmailVerificationConfig,
6 models::{
7 totp::Totp, Account, DeletionInfo, EmailVerification, MFAMethod, MFAResponse, MFATicket,
8 PasswordReset, Session,
9 },
10 util::{hash_password, normalize_email},
11 Authifier, AuthifierEvent, Error, Result, Success,
12};
13
14impl Account {
15 pub async fn save(&self, authifier: &Authifier) -> Success {
17 authifier.database.save_account(self).await
18 }
19
20 pub async fn new(
22 authifier: &Authifier,
23 email: String,
24 plaintext_password: String,
25 verify_email: bool,
26 ) -> Result<Account> {
27 let password = hash_password(plaintext_password)?;
29
30 let email_normalized = normalize_email(email.clone());
32
33 if let Some(mut account) = authifier
35 .database
36 .find_account_by_normalized_email(&email_normalized)
37 .await?
38 {
39 if let EmailVerification::Pending { .. } = &account.verification {
41 account.start_email_verification(authifier).await?;
42 } else {
43 account.start_password_reset(authifier).await?;
44 }
45
46 Ok(account)
47 } else {
48 let mut account = Account {
50 id: ulid::Ulid::new().to_string(),
51
52 email,
53 email_normalized,
54 password,
55
56 disabled: false,
57 verification: EmailVerification::Verified,
58 password_reset: None,
59 deletion: None,
60 lockout: None,
61
62 mfa: Default::default(),
63 };
64
65 if verify_email {
67 account.start_email_verification(authifier).await?;
68 } else {
69 account.save(authifier).await?;
70 }
71
72 authifier
74 .publish_event(AuthifierEvent::CreateAccount {
75 account: account.clone(),
76 })
77 .await;
78
79 Ok(account)
80 }
81 }
82
83 pub async fn create_session(&self, authifier: &Authifier, name: String) -> Result<Session> {
85 let session = Session {
86 id: ulid::Ulid::new().to_string(),
87 token: nanoid!(64),
88
89 user_id: self.id.clone(),
90 name,
91
92 subscription: None,
93 };
94
95 authifier.database.save_session(&session).await?;
97
98 authifier
100 .publish_event(AuthifierEvent::CreateSession {
101 session: session.clone(),
102 })
103 .await;
104
105 Ok(session)
106 }
107
108 pub async fn start_email_verification(&mut self, authifier: &Authifier) -> Success {
110 if let EmailVerificationConfig::Enabled {
111 templates,
112 expiry,
113 smtp,
114 } = &authifier.config.email_verification
115 {
116 let token = nanoid!(32);
117 let url = format!("{}{}", templates.verify.url, token);
118
119 smtp.send_email(
120 self.email.clone(),
121 &templates.verify,
122 json!({
123 "email": self.email.clone(),
124 "url": url
125 }),
126 )?;
127
128 self.verification = EmailVerification::Pending {
129 token,
130 expiry: Timestamp::from_unix_timestamp_ms(
131 chrono::Utc::now()
132 .checked_add_signed(Duration::seconds(expiry.expire_verification))
133 .expect("failed to checked_add_signed")
134 .timestamp_millis(),
135 ),
136 };
137 } else {
138 self.verification = EmailVerification::Verified;
139 }
140
141 self.save(authifier).await
142 }
143
144 pub async fn start_email_move(&mut self, authifier: &Authifier, new_email: String) -> Success {
146 if let EmailVerification::Pending { .. } = self.verification {
149 return Err(Error::UnverifiedAccount);
150 }
151
152 if let EmailVerificationConfig::Enabled {
153 templates,
154 expiry,
155 smtp,
156 } = &authifier.config.email_verification
157 {
158 let token = nanoid!(32);
159 let url = format!("{}{}", templates.verify.url, token);
160
161 smtp.send_email(
162 new_email.clone(),
163 &templates.verify,
164 json!({
165 "email": self.email.clone(),
166 "url": url
167 }),
168 )?;
169
170 self.verification = EmailVerification::Moving {
171 new_email,
172 token,
173 expiry: Timestamp::from_unix_timestamp_ms(
174 chrono::Utc::now()
175 .checked_add_signed(Duration::seconds(expiry.expire_verification))
176 .expect("failed to checked_add_signed")
177 .timestamp_millis(),
178 ),
179 };
180 } else {
181 self.email_normalized = normalize_email(new_email.clone());
182 self.email = new_email;
183 }
184
185 self.save(authifier).await
186 }
187
188 pub async fn start_password_reset(&mut self, authifier: &Authifier) -> Success {
190 if let EmailVerificationConfig::Enabled {
191 templates,
192 expiry,
193 smtp,
194 } = &authifier.config.email_verification
195 {
196 let token = nanoid!(32);
197 let url = format!("{}{}", templates.reset.url, token);
198
199 smtp.send_email(
200 self.email.clone(),
201 &templates.reset,
202 json!({
203 "email": self.email.clone(),
204 "url": url
205 }),
206 )?;
207
208 self.password_reset = Some(PasswordReset {
209 token,
210 expiry: Timestamp::from_unix_timestamp_ms(
211 chrono::Utc::now()
212 .checked_add_signed(Duration::seconds(expiry.expire_password_reset))
213 .expect("failed to checked_add_signed")
214 .timestamp_millis(),
215 ),
216 });
217 } else {
218 return Err(Error::OperationFailed);
219 }
220
221 self.save(authifier).await
222 }
223
224 pub async fn start_account_deletion(&mut self, authifier: &Authifier) -> Success {
228 if let EmailVerificationConfig::Enabled {
229 templates,
230 expiry,
231 smtp,
232 } = &authifier.config.email_verification
233 {
234 let token = nanoid!(32);
235 let url = format!("{}{}", templates.deletion.url, token);
236
237 smtp.send_email(
238 self.email.clone(),
239 &templates.deletion,
240 json!({
241 "email": self.email.clone(),
242 "url": url
243 }),
244 )?;
245
246 self.deletion = Some(DeletionInfo::WaitingForVerification {
247 token,
248 expiry: Timestamp::from_unix_timestamp_ms(
249 chrono::Utc::now()
250 .checked_add_signed(Duration::seconds(expiry.expire_password_reset))
251 .expect("failed to checked_add_signed")
252 .timestamp_millis(),
253 ),
254 });
255
256 self.save(authifier).await
257 } else {
258 self.schedule_deletion(authifier).await
259 }
260 }
261
262 pub fn verify_password(&self, plaintext_password: &str) -> Success {
264 bcrypt::verify(plaintext_password, &self.password)
265 .map(|v| {
266 if v {
267 Ok(())
268 } else {
269 Err(Error::InvalidCredentials)
270 }
271 })
272 .map_err(|_| Error::InvalidCredentials)?
275 }
276
277 pub async fn consume_mfa_response(
279 &mut self,
280 authifier: &Authifier,
281 response: MFAResponse,
282 ticket: Option<MFATicket>,
283 ) -> Success {
284 let allowed_methods = self.mfa.get_methods();
285
286 match response {
287 MFAResponse::Password { password } => {
288 if allowed_methods.contains(&MFAMethod::Password) {
289 self.verify_password(&password)
290 } else {
291 Err(Error::DisallowedMFAMethod)
292 }
293 }
294 MFAResponse::Totp { totp_code } => {
295 if allowed_methods.contains(&MFAMethod::Totp) {
296 if let Totp::Enabled { .. } = &self.mfa.totp_token {
297 if let Some(ticket) = ticket {
299 if let Some(code) = ticket.last_totp_code {
300 if code == totp_code {
301 return Ok(());
302 }
303 }
304 }
305
306 if self.mfa.totp_token.generate_code()? == totp_code {
308 Ok(())
309 } else {
310 Err(Error::InvalidToken)
311 }
312 } else {
313 unreachable!()
314 }
315 } else {
316 Err(Error::DisallowedMFAMethod)
317 }
318 }
319 MFAResponse::Recovery { recovery_code } => {
320 if allowed_methods.contains(&MFAMethod::Recovery) {
321 if let Some(index) = self
322 .mfa
323 .recovery_codes
324 .iter()
325 .position(|x| x == &recovery_code)
326 {
327 self.mfa.recovery_codes.remove(index);
328 self.save(authifier).await
329 } else {
330 Err(Error::InvalidToken)
331 }
332 } else {
333 Err(Error::DisallowedMFAMethod)
334 }
335 }
336 }
337 }
338
339 pub async fn delete_all_sessions(
341 &self,
342 authifier: &Authifier,
343 exclude_session_id: Option<String>,
344 ) -> Success {
345 authifier
346 .database
347 .delete_all_sessions(&self.id, exclude_session_id.clone())
348 .await?;
349
350 authifier
352 .publish_event(AuthifierEvent::DeleteAllSessions {
353 user_id: self.id.to_string(),
354 exclude_session_id,
355 })
356 .await;
357
358 Ok(())
359 }
360
361 pub async fn disable(&mut self, authifier: &Authifier) -> Success {
363 self.disabled = true;
364 self.delete_all_sessions(authifier, None).await?;
365 self.save(authifier).await
366 }
367
368 pub async fn schedule_deletion(&mut self, authifier: &Authifier) -> Success {
370 self.deletion = Some(DeletionInfo::Scheduled {
371 after: Timestamp::from_unix_timestamp_ms(
372 chrono::Utc::now()
373 .checked_add_signed(Duration::weeks(1))
374 .expect("failed to checked_add_signed")
375 .timestamp_millis(),
376 ),
377 });
378
379 self.disable(authifier).await
380 }
381}