1use iso8601_timestamp::{Duration, Timestamp};
2
3use nanoid::nanoid;
4use revolt_config::config;
5use revolt_result::Result;
6use serde_json::json;
7
8use crate::{
9 events::client::EventV1,
10 util::{
11 email::{email_templates, normalise_email, send_email},
12 password::hash_password,
13 },
14 Database, MFATicket, Session,
15};
16use revolt_models::v0;
17
18auto_derived_partial!(
19 pub struct Account {
21 #[serde(rename = "_id")]
23 pub id: String,
24
25 pub email: String,
27
28 pub email_normalised: String,
32
33 pub password: String,
35
36 #[serde(default)]
38 pub disabled: bool,
39
40 pub verification: EmailVerification,
42
43 pub password_reset: Option<PasswordReset>,
45
46 pub deletion: Option<DeletionInfo>,
48
49 pub lockout: Option<Lockout>,
51
52 pub mfa: MultiFactorAuthentication,
54 },
55 "PartialAccount"
56);
57
58auto_derived!(
59 #[serde(tag = "status")]
61 pub enum EmailVerification {
62 Verified,
64 Pending { token: String, expiry: Timestamp },
66 Moving {
68 new_email: String,
69 token: String,
70 expiry: Timestamp,
71 },
72 }
73
74 pub struct PasswordReset {
76 pub token: String,
78 pub expiry: Timestamp,
80 }
81
82 #[serde(tag = "status")]
84 pub enum DeletionInfo {
85 WaitingForVerification { token: String, expiry: Timestamp },
87 Scheduled { after: Timestamp },
89 Deleted,
91 }
92
93 pub struct Lockout {
95 pub attempts: i32,
97 pub expiry: Option<Timestamp>,
99 }
100
101 #[derive(Default)]
103 pub struct MultiFactorAuthentication {
104 #[serde(skip_serializing_if = "Totp::is_empty", default)]
122 pub totp_token: Totp,
123
124 #[serde(skip_serializing_if = "Vec::is_empty", default)]
131 pub recovery_codes: Vec<String>,
132 }
133
134 #[derive(Hash)]
136 pub enum MFAMethod {
137 Password,
138 Recovery,
139 Totp,
140 }
141
142 #[derive(Default)]
143 #[serde(tag = "status")]
144 pub enum Totp {
145 #[default]
147 Disabled,
148 Pending { secret: String },
150 Enabled { secret: String },
152 }
153);
154
155impl MultiFactorAuthentication {
156 pub fn is_active(&self) -> bool {
158 matches!(self.totp_token, Totp::Enabled { .. })
159 }
160
161 pub fn has_recovery(&self) -> bool {
163 !self.recovery_codes.is_empty()
164 }
165
166 pub fn get_methods(&self) -> Vec<MFAMethod> {
168 if let Totp::Enabled { .. } = self.totp_token {
169 let mut methods = vec![MFAMethod::Totp];
170
171 if self.has_recovery() {
172 methods.push(MFAMethod::Recovery);
173 }
174
175 methods
176 } else {
177 vec![MFAMethod::Password]
178 }
179 }
180
181 pub fn generate_recovery_codes(&mut self) {
183 static ALPHABET: [char; 32] = [
184 '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
185 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
186 ];
187
188 let mut codes = vec![];
189 for _ in 1..=10 {
190 codes.push(format!(
191 "{}-{}",
192 nanoid!(5, &ALPHABET),
193 nanoid!(5, &ALPHABET)
194 ));
195 }
196
197 self.recovery_codes = codes;
198 }
199
200 pub fn generate_new_totp_secret(&mut self) -> Result<String> {
202 if let Totp::Enabled { .. } = self.totp_token {
203 return Err(create_error!(OperationFailed));
204 }
205
206 let secret: [u8; 10] = rand::random();
207 let secret = base32::encode(base32::Alphabet::RFC4648 { padding: false }, &secret);
208
209 self.totp_token = Totp::Pending {
210 secret: secret.clone(),
211 };
212
213 Ok(secret)
214 }
215
216 pub fn enable_totp(&mut self, response: v0::MFAResponse) -> Result<()> {
218 if let v0::MFAResponse::Totp { totp_code } = response {
219 let code = self.totp_token.generate_code()?;
220
221 if code == totp_code {
222 let mut totp = Totp::Disabled;
223 std::mem::swap(&mut totp, &mut self.totp_token);
224
225 if let Totp::Pending { secret } = totp {
226 self.totp_token = Totp::Enabled { secret };
227
228 Ok(())
229 } else {
230 Err(create_error!(OperationFailed))
231 }
232 } else {
233 Err(create_error!(InvalidToken))
234 }
235 } else {
236 Err(create_error!(InvalidToken))
237 }
238 }
239}
240
241impl Totp {
242 pub fn is_empty(&self) -> bool {
244 matches!(self, Totp::Disabled)
245 }
246
247 pub fn is_disabled(&self) -> bool {
249 !matches!(self, Totp::Enabled { .. })
250 }
251
252 pub fn generate_code(&self) -> Result<String> {
254 if let Totp::Enabled { secret } | Totp::Pending { secret } = &self {
255 let seconds: u64 = std::time::SystemTime::now()
256 .duration_since(std::time::UNIX_EPOCH)
257 .unwrap()
258 .as_secs();
259
260 Ok(totp_lite::totp_custom::<totp_lite::Sha1>(
261 totp_lite::DEFAULT_STEP,
262 6,
263 &base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret)
264 .expect("valid base32 secret"),
265 seconds,
266 ))
267 } else {
268 Err(create_error!(OperationFailed))
269 }
270 }
271}
272
273impl Account {
274 pub async fn save(&self, db: &Database) -> Result<()> {
276 db.save_account(self).await
277 }
278
279 pub async fn new(
281 db: &Database,
282 email: String,
283 plaintext_password: String,
284 verify_email: bool,
285 ) -> Result<Account> {
286 let email_normalised = normalise_email(email.clone());
288
289 if let Some(mut account) = db
291 .fetch_account_by_normalised_email(&email_normalised)
292 .await?
293 {
294 if let EmailVerification::Pending { .. } = &account.verification {
296 account.start_email_verification(db).await?;
297 } else {
298 account.start_password_reset(db, true).await?;
299 }
300
301 Ok(account)
302 } else {
303 let password = hash_password(plaintext_password)?;
305
306 let mut account = Account {
308 id: ulid::Ulid::new().to_string(),
309
310 email,
311 email_normalised,
312 password,
313
314 disabled: false,
315 verification: EmailVerification::Verified,
316 password_reset: None,
317 deletion: None,
318 lockout: None,
319
320 mfa: Default::default(),
321 };
322
323 if verify_email {
325 account.start_email_verification(db).await?;
326 } else {
327 account.save(db).await?;
328 }
329
330 EventV1::CreateAccount {
332 account: account.clone(),
333 }
334 .global()
335 .await;
336
337 Ok(account)
338 }
339 }
340
341 pub async fn create_session(&self, db: &Database, name: String) -> Result<Session> {
343 let config = config().await;
344
345 let session = Session {
346 id: ulid::Ulid::new().to_string(),
347 token: nanoid!(64),
348
349 user_id: self.id.clone(),
350 name,
351
352 last_seen: Timestamp::now_utc(),
353
354 origin: Some(config.environment),
355 subscription: None,
356 };
357
358 db.save_session(&session).await?;
360
361 EventV1::CreateSession {
363 session: session.clone(),
364 }
365 .global()
366 .await;
367
368 Ok(session)
369 }
370
371 pub async fn start_email_verification(&mut self, db: &Database) -> Result<()> {
373 let config = config().await;
374
375 if !config.api.smtp.host.is_empty() {
376 let templates = email_templates().await;
377
378 let token = nanoid!(32);
379 let url = format!("{}{}", templates.verify.url, token);
380
381 send_email(
382 &config.api.smtp,
383 self.email.clone(),
384 &templates.verify,
385 json!({
386 "email": self.email.clone(),
387 "url": url
388 }),
389 )?;
390
391 self.verification = EmailVerification::Pending {
392 token,
393 expiry: Timestamp::now_utc()
394 .checked_add(Duration::seconds(
395 config.api.smtp.expiry.expire_verification,
396 ))
397 .unwrap(),
398 };
399 } else {
400 self.verification = EmailVerification::Verified;
401 }
402
403 self.save(db).await
404 }
405
406 pub async fn start_email_move(&mut self, db: &Database, new_email: String) -> Result<()> {
408 if let EmailVerification::Pending { .. } = self.verification {
411 return Err(create_error!(UnverifiedAccount));
412 }
413
414 let config = config().await;
415
416 if !config.api.smtp.host.is_empty() {
417 let templates = email_templates().await;
418
419 let token = nanoid!(32);
420 let url = format!("{}{}", templates.verify.url, token);
421
422 send_email(
423 &config.api.smtp,
424 new_email.clone(),
425 &templates.verify,
426 json!({
427 "email": self.email.clone(),
428 "url": url
429 }),
430 )?;
431
432 self.verification = EmailVerification::Moving {
433 new_email,
434 token,
435 expiry: Timestamp::now_utc()
436 .checked_add(Duration::seconds(
437 config.api.smtp.expiry.expire_verification,
438 ))
439 .unwrap(),
440 };
441 } else {
442 self.email_normalised = normalise_email(new_email.clone());
443 self.email = new_email;
444 }
445
446 self.save(db).await
447 }
448
449 pub async fn start_password_reset(
451 &mut self,
452 db: &Database,
453 existing_account: bool,
454 ) -> Result<()> {
455 let config = config().await;
456
457 if !config.api.smtp.host.is_empty() {
458 let templates = email_templates().await;
459
460 let template = if existing_account {
461 &templates.reset_existing
462 } else {
463 &templates.reset
464 };
465
466 let token = nanoid!(32);
467 let url = format!("{}{}", template.url, token);
468
469 send_email(
470 &config.api.smtp,
471 self.email.clone(),
472 template,
473 json!({
474 "email": self.email.clone(),
475 "url": url
476 }),
477 )?;
478
479 self.password_reset = Some(PasswordReset {
480 token,
481 expiry: Timestamp::now_utc()
482 .checked_add(Duration::seconds(
483 config.api.smtp.expiry.expire_password_reset,
484 ))
485 .unwrap(),
486 });
487 } else {
488 return Err(create_error!(OperationFailed));
489 }
490
491 self.save(db).await
492 }
493
494 pub async fn start_account_deletion(&mut self, db: &Database) -> Result<()> {
498 let config = config().await;
499
500 if !config.api.smtp.host.is_empty() {
501 let templates = email_templates().await;
502
503 let token = nanoid!(32);
504 let url = format!("{}{}", templates.deletion.url, token);
505
506 send_email(
507 &config.api.smtp,
508 self.email.clone(),
509 &templates.deletion,
510 json!({
511 "email": self.email.clone(),
512 "url": url
513 }),
514 )?;
515
516 self.deletion = Some(DeletionInfo::WaitingForVerification {
517 token,
518 expiry: Timestamp::now_utc()
519 .checked_add(Duration::seconds(
520 config.api.smtp.expiry.expire_password_reset,
521 ))
522 .unwrap(),
523 });
524
525 self.save(db).await
526 } else {
527 self.schedule_deletion(db).await
528 }
529 }
530
531 pub fn verify_password(&self, plaintext_password: &str) -> Result<()> {
533 argon2::verify_encoded(&self.password, plaintext_password.as_bytes())
534 .map(|v| {
535 if v {
536 Ok(())
537 } else {
538 Err(create_error!(InvalidCredentials))
539 }
540 })
541 .map_err(|_| create_error!(InvalidCredentials))?
544 }
545
546 pub async fn consume_mfa_response(
548 &mut self,
549 db: &Database,
550 response: v0::MFAResponse,
551 ticket: Option<MFATicket>,
552 ) -> Result<()> {
553 let allowed_methods = self.mfa.get_methods();
554
555 match response {
556 v0::MFAResponse::Password { password } => {
557 if allowed_methods.contains(&MFAMethod::Password) {
558 self.verify_password(&password)
559 } else {
560 Err(create_error!(DisallowedMFAMethod))
561 }
562 }
563 v0::MFAResponse::Totp { totp_code } => {
564 if allowed_methods.contains(&MFAMethod::Totp) {
565 if let Totp::Enabled { .. } = &self.mfa.totp_token {
566 if let Some(ticket) = ticket {
568 if let Some(code) = ticket.last_totp_code {
569 if code == totp_code {
570 return Ok(());
571 }
572 }
573 }
574
575 if self.mfa.totp_token.generate_code()? == totp_code {
577 Ok(())
578 } else {
579 Err(create_error!(InvalidToken))
580 }
581 } else {
582 unreachable!()
583 }
584 } else {
585 Err(create_error!(DisallowedMFAMethod))
586 }
587 }
588 v0::MFAResponse::Recovery { recovery_code } => {
589 if allowed_methods.contains(&MFAMethod::Recovery) {
590 if let Some(index) = self
591 .mfa
592 .recovery_codes
593 .iter()
594 .position(|x| x == &recovery_code)
595 {
596 self.mfa.recovery_codes.remove(index);
597 self.save(db).await
598 } else {
599 Err(create_error!(InvalidToken))
600 }
601 } else {
602 Err(create_error!(DisallowedMFAMethod))
603 }
604 }
605 }
606 }
607
608 pub async fn delete_all_sessions(
610 &self,
611 db: &Database,
612 exclude_session_id: Option<String>,
613 ) -> Result<()> {
614 db.delete_all_sessions(&self.id, exclude_session_id.clone())
615 .await?;
616
617 EventV1::DeleteAllSessions {
619 user_id: self.id.clone(),
620 exclude_session_id,
621 }
622 .private(self.id.clone())
623 .await;
624
625 Ok(())
626 }
627
628 pub async fn disable(&mut self, db: &Database) -> Result<()> {
630 self.disabled = true;
631 self.delete_all_sessions(db, None).await?;
632 self.save(db).await
633 }
634
635 pub async fn schedule_deletion(&mut self, db: &Database) -> Result<()> {
637 self.deletion = Some(DeletionInfo::Scheduled {
638 after: Timestamp::now_utc()
639 .checked_add(Duration::weeks(1))
640 .unwrap(),
641 });
642
643 self.disable(db).await
644 }
645
646 pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
648 self.email = format!("Deleted User {}", &self.id);
649 self.email_normalised = format!("Deleted User {}", &self.id);
650 self.deletion = Some(DeletionInfo::Deleted);
651
652 self.save(db).await?;
653
654 Ok(())
655 }
656}