1use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
2
3use crate::auth::{
4 AccessTokenClaims, Auth, PaginationParams, Permission, Role, User, UserChangeset, UserSession,
5 UserSessionChangeset, UserSessionJson, UserSessionResponse, ID,
6};
7use crate::{Connection, Database, Mailer};
8
9use lazy_static::lazy_static;
10use serde::{Deserialize, Serialize};
11
12pub const COOKIE_NAME: &str = "refresh_token";
13
14lazy_static! {
15 pub static ref ARGON_CONFIG: argon2::Config<'static> = argon2::Config {
16 variant: argon2::Variant::Argon2id,
17 version: argon2::Version::Version13,
18 secret: std::env::var("SECRET_KEY").map_or_else(|_| panic!("No SECRET_KEY environment variable set!"), |s| Box::leak(s.into_boxed_str()).as_bytes()),
19 ..Default::default()
20 };
21 }
23
24#[cfg(not(debug_assertions))]
25type Seconds = i64;
26type StatusCode = u16;
27type Message = &'static str;
28
29#[derive(Deserialize, Serialize)]
30#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::ToSchema))]
31pub struct LoginInput {
34 email: String,
35 password: String,
36 device: Option<String>,
37 #[cfg(not(debug_assertions))]
38 ttl: Option<Seconds>, #[cfg(debug_assertions)]
40 ttl: Option<i64>, }
42
43#[derive(Debug, Serialize, Deserialize)]
44pub struct RefreshTokenClaims {
46 exp: usize,
47 sub: ID,
48 token_type: String,
49}
50
51#[derive(Serialize, Deserialize)]
52#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::ToSchema))]
53pub struct RegisterInput {
56 email: String,
57 password: String,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61pub struct RegistrationClaims {
63 exp: usize,
64 sub: ID,
65 token_type: String,
66}
67
68#[derive(Serialize, Deserialize)]
69#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::IntoParams))]
70pub struct ActivationInput {
73 activation_token: String,
74}
75
76#[derive(Serialize, Deserialize)]
77#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::ToSchema))]
78pub struct ForgotInput {
81 email: String,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85pub struct ResetTokenClaims {
87 exp: usize,
88 sub: ID,
89 token_type: String,
90}
91
92#[derive(Serialize, Deserialize)]
93#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::ToSchema))]
94pub struct ChangeInput {
97 old_password: String,
98 new_password: String,
99}
100
101#[derive(Serialize, Deserialize)]
102#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::ToSchema))]
103pub struct ResetInput {
106 reset_token: String,
107 new_password: String,
108}
109
110pub fn get_sessions(
130 db: &Database,
131 auth: &Auth,
132 info: &PaginationParams,
133) -> Result<UserSessionResponse, (StatusCode, Message)> {
134 let mut db = db.get_connection().unwrap();
135
136 let Ok(sessions) = UserSession::read_all(&mut db, info, auth.user_id) else {
137 return Err((500, "Could not fetch sessions."));
138 };
139
140 let sessions_json: Vec<UserSessionJson> = sessions
141 .iter()
142 .map(|s| UserSessionJson {
143 id: s.id,
144 device: s.device.clone(),
145 created_at: s.created_at,
146 #[cfg(not(feature = "database_sqlite"))]
147 updated_at: s.updated_at,
148 })
149 .collect();
150
151 let Ok(num_sessions) = UserSession::count_all(&mut db, auth.user_id) else {
152 return Err((500, "Could not fetch sessions."));
153 };
154
155 let num_pages = (num_sessions / info.page_size) + i64::from(num_sessions % info.page_size != 0);
156
157 let resp = UserSessionResponse {
158 sessions: sessions_json,
159 num_pages,
160 };
161
162 Ok(resp)
163}
164
165pub fn destroy_session(
180 db: &Database,
181 auth: &Auth,
182 item_id: ID,
183) -> Result<(), (StatusCode, Message)> {
184 let mut db = db.get_connection().unwrap();
185
186 let user_session = match UserSession::read(&mut db, item_id) {
187 Ok(user_session) if user_session.user_id == auth.user_id => user_session,
188 Ok(_) => return Err((404, "Session not found.")),
189 Err(_) => return Err((500, "Internal error.")),
190 };
191
192 UserSession::delete(&mut db, user_session.id)
193 .map_err(|_| (500, "Could not delete session."))?;
194
195 Ok(())
196}
197
198pub fn destroy_sessions(db: &Database, auth: &Auth) -> Result<(), (StatusCode, Message)> {
211 let mut db = db.get_connection().unwrap();
212
213 UserSession::delete_all_for_user(&mut db, auth.user_id)
214 .map_err(|_| (500, "Could not delete sessions."))?;
215
216 Ok(())
217}
218
219type AccessToken = String;
220type RefreshToken = String;
221
222pub fn login(
244 db: &Database,
245 item: &LoginInput,
246) -> Result<(AccessToken, RefreshToken), (StatusCode, Message)> {
247 let mut db = db.get_connection().unwrap();
248
249 let device = match item.device {
251 Some(ref device) if device.len() > 256 => {
252 return Err((400, "'device' cannot be longer than 256 characters."));
253 }
254 Some(ref device) => Some(device.clone()),
255 None => None,
256 };
257
258 let user = match User::find_by_email(&mut db, item.email.clone()) {
259 Ok(user) if user.activated => user,
260 Ok(_) => return Err((400, "Account has not been activated.")),
261 Err(_) => return Err((401, "Invalid credentials.")),
262 };
263
264 let is_valid = argon2::verify_encoded_ext(
265 &user.hash_password,
266 item.password.as_bytes(),
267 ARGON_CONFIG.secret,
268 ARGON_CONFIG.ad,
269 )
270 .unwrap();
271
272 if !is_valid {
273 return Err((401, "Invalid credentials."));
274 }
275
276 create_user_session(&mut db, device, None, user.id)
277}
278
279pub fn create_user_session(
293 db: &mut Connection,
294 device_type: Option<String>,
295 ttl: Option<i64>,
296 user_id: i32,
297) -> Result<(AccessToken, RefreshToken), (StatusCode, Message)> {
298 let device = match device_type {
300 Some(device) if device.len() > 256 => {
301 return Err((400, "'device' cannot be longer than 256 characters."));
302 }
303 Some(device) => Some(device),
304 None => None,
305 };
306
307 let Ok(permissions) = Permission::fetch_all(db, user_id) else {
308 return Err((500, "An internal server error occurred."));
309 };
310
311 let Ok(roles) = Role::fetch_all(db, user_id) else {
312 return Err((500, "An internal server error occurred."));
313 };
314
315 let access_token_duration = chrono::Duration::seconds(
316 ttl.map_or_else(|| 15 * 60, |tt| std::cmp::max(tt, 1)),
317 );
318
319 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
320 let access_token_claims = AccessTokenClaims {
321 exp: (chrono::Utc::now() + access_token_duration).timestamp() as usize,
322 sub: user_id,
323 token_type: "access_token".to_string(),
324 roles,
325 permissions,
326 };
327
328 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
329 let refresh_token_claims = RefreshTokenClaims {
330 exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
331 sub: user_id,
332 token_type: "refresh_token".to_string(),
333 };
334
335 let access_token = encode(
336 &Header::default(),
337 &access_token_claims,
338 &EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
339 )
340 .unwrap();
341
342 let refresh_token = encode(
343 &Header::default(),
344 &refresh_token_claims,
345 &EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
346 )
347 .unwrap();
348
349 UserSession::create(
350 db,
351 &UserSessionChangeset {
352 user_id,
353 refresh_token: refresh_token.clone(),
354 device,
355 },
356 )
357 .map_err(|_| (500, "Could not create session."))?;
358
359 Ok((access_token, refresh_token))
360}
361
362pub fn logout(db: &Database, refresh_token: Option<&'_ str>) -> Result<(), (StatusCode, Message)> {
375 let mut db = db.get_connection().unwrap();
376
377 let Some(refresh_token) = refresh_token else {
378 return Err((401, "Invalid session."));
379 };
380
381 let Ok(session) = UserSession::find_by_refresh_token(&mut db, refresh_token) else {
382 return Err((401, "Invalid session."));
383 };
384
385 UserSession::delete(&mut db, session.id).map_err(|_| (500, "Could not delete session."))?;
386
387 Ok(())
388}
389
390pub fn refresh(
412 db: &Database,
413 refresh_token_str: Option<&'_ str>,
414) -> Result<(AccessToken, RefreshToken), (StatusCode, Message)> {
415 let mut db = db.get_connection().unwrap();
416
417 let Some(refresh_token_str) = refresh_token_str else {
418 return Err((401, "Invalid session."));
419 };
420
421 let _refresh_token = match decode::<RefreshTokenClaims>(
422 refresh_token_str,
423 &DecodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
424 &Validation::default(),
425 ) {
426 Ok(token)
427 if token
428 .claims
429 .token_type
430 .eq_ignore_ascii_case("refresh_token") =>
431 {
432 token
433 }
434 _ => return Err((401, "Invalid token.")),
435 };
436
437 let Ok(session) = UserSession::find_by_refresh_token(&mut db, refresh_token_str) else {
438 return Err((401, "Invalid session."));
439 };
440
441 let Ok(permissions) = Permission::fetch_all(&mut db, session.user_id) else {
442 return Err((500, "An internal server error occurred."));
443 };
444
445 let Ok(roles) = Role::fetch_all(&mut db, session.user_id) else {
446 return Err((500, "An internal server error occurred."));
447 };
448
449 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
450 let access_token_claims = AccessTokenClaims {
451 exp: (chrono::Utc::now() + chrono::Duration::minutes(15)).timestamp() as usize,
452 sub: session.user_id,
453 token_type: "access_token".to_string(),
454 roles,
455 permissions,
456 };
457
458 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
459 let refresh_token_claims = RefreshTokenClaims {
460 exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
461 sub: session.user_id,
462 token_type: "refresh_token".to_string(),
463 };
464
465 let access_token = encode(
466 &Header::default(),
467 &access_token_claims,
468 &EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
469 )
470 .unwrap();
471
472 let refresh_token_str = encode(
473 &Header::default(),
474 &refresh_token_claims,
475 &EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
476 )
477 .unwrap();
478
479 UserSession::update(
481 &mut db,
482 session.id,
483 &UserSessionChangeset {
484 user_id: session.user_id,
485 refresh_token: refresh_token_str.clone(),
486 device: session.device,
487 },
488 )
489 .map_err(|_| (500, "Could not update session."))?;
490
491 Ok((access_token, refresh_token_str))
492}
493
494pub fn register(
512 db: &Database,
513 item: &RegisterInput,
514 mailer: &Mailer,
515) -> Result<(), (StatusCode, Message)> {
516 let mut db = db.get_connection().unwrap();
517
518 match User::find_by_email(&mut db, item.email.to_string()) {
519 Ok(user) if user.activated => return Err((400, "Already registered.")),
520 Ok(user) => {
521 User::delete(&mut db, user.id).unwrap();
522 }
523 Err(_) => (),
524 }
525
526 let salt = generate_salt();
527 let hash = argon2::hash_encoded(item.password.as_bytes(), &salt, &ARGON_CONFIG).unwrap();
528
529 let user = User::create(
530 &mut db,
531 &UserChangeset {
532 activated: false,
533 email: item.email.clone(),
534 hash_password: hash,
535 },
536 )
537 .unwrap();
538
539 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
540 let registration_claims = RegistrationClaims {
541 exp: (chrono::Utc::now() + chrono::Duration::days(30)).timestamp() as usize,
542 sub: user.id,
543 token_type: "activation_token".to_string(),
544 };
545
546 let token = encode(
547 &Header::default(),
548 ®istration_claims,
549 &EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
550 )
551 .unwrap();
552
553 mailer
554 .templates
555 .send_register(mailer, &user.email, &format!("activate?token={token}"));
556
557 Ok(())
558}
559
560pub fn activate(
577 db: &Database,
578 item: &ActivationInput,
579 mailer: &Mailer,
580) -> Result<(), (StatusCode, Message)> {
581 let mut db = db.get_connection().unwrap();
582
583 let token = match decode::<RegistrationClaims>(
584 &item.activation_token,
585 &DecodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
586 &Validation::default(),
587 ) {
588 Ok(token)
589 if token
590 .claims
591 .token_type
592 .eq_ignore_ascii_case("activation_token") =>
593 {
594 token
595 }
596 _ => return Err((401, "Invalid token.")),
597 };
598
599 let user = match User::read(&mut db, token.claims.sub) {
600 Ok(user) if !user.activated => user,
601 Ok(_) => return Err((200, "Already activated!")),
602 Err(_) => return Err((400, "Invalid token.")),
603 };
604
605 User::update(
606 &mut db,
607 user.id,
608 &UserChangeset {
609 activated: true,
610 email: user.email.clone(),
611 hash_password: user.hash_password,
612 },
613 )
614 .map_err(|_| (500, "Could not activate user."))?;
615
616 mailer.templates.send_activated(mailer, &user.email);
617
618 Ok(())
619}
620
621pub fn forgot_password(
640 db: &Database,
641 item: &ForgotInput,
642 mailer: &Mailer,
643) -> Result<(), (StatusCode, Message)> {
644 let mut db = db.get_connection().unwrap();
645
646 let user_result = User::find_by_email(&mut db, item.email.clone());
647
648 if let Ok(user) = user_result {
649 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
654 let reset_token_claims = ResetTokenClaims {
655 exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
656 sub: user.id,
657 token_type: "reset_token".to_string(),
658 };
659
660 let reset_token = encode(
661 &Header::default(),
662 &reset_token_claims,
663 &EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
664 )
665 .unwrap();
666
667 let link = &format!("reset?token={reset_token}");
668 mailer
669 .templates
670 .send_recover_existent_account(mailer, &user.email, link);
671 } else {
672 let link = &"register".to_string();
673 mailer
674 .templates
675 .send_recover_nonexistent_account(mailer, &item.email, link);
676 }
677
678 Ok(())
679}
680
681pub fn change_password(
700 db: &Database,
701 item: &ChangeInput,
702 auth: &Auth,
703 mailer: &Mailer,
704) -> Result<(), (StatusCode, Message)> {
705 if item.old_password.is_empty() || item.new_password.is_empty() {
706 return Err((400, "Missing password"));
707 }
708
709 if item.old_password.eq(&item.new_password) {
710 return Err((400, "The new password must be different"));
711 }
712
713 let mut db = db.get_connection().unwrap();
714
715 let user = match User::read(&mut db, auth.user_id) {
716 Ok(user) if user.activated => user,
717 Ok(_) => return Err((400, "Account has not been activated")),
718 Err(_) => return Err((500, "Could not find user")),
719 };
720
721 let is_old_password_valid = argon2::verify_encoded_ext(
722 &user.hash_password,
723 item.old_password.as_bytes(),
724 ARGON_CONFIG.secret,
725 ARGON_CONFIG.ad,
726 )
727 .unwrap();
728
729 if !is_old_password_valid {
730 return Err((401, "Invalid credentials"));
731 }
732
733 let salt = generate_salt();
734 let new_hash =
735 argon2::hash_encoded(item.new_password.as_bytes(), &salt, &ARGON_CONFIG).unwrap();
736
737 User::update(
738 &mut db,
739 auth.user_id,
740 &UserChangeset {
741 email: user.email.clone(),
742 hash_password: new_hash,
743 activated: user.activated,
744 },
745 )
746 .map_err(|_| (500, "Could not update password"))?;
747
748 mailer.templates.send_password_changed(mailer, &user.email);
749
750 Ok(())
751}
752
753pub const fn check(_: &Auth) {}
758
759pub fn reset_password(
777 db: &Database,
778 item: &ResetInput,
779 mailer: &Mailer,
780) -> Result<(), (StatusCode, Message)> {
781 let mut db = db.get_connection().unwrap();
782
783 if item.new_password.is_empty() {
784 return Err((400, "Missing password"));
785 }
786
787 let token = match decode::<ResetTokenClaims>(
788 &item.reset_token,
789 &DecodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()),
790 &Validation::default(),
791 ) {
792 Ok(token) if token.claims.token_type.eq_ignore_ascii_case("reset_token") => token,
793 _ => return Err((401, "Invalid token.")),
794 };
795
796 let user = match User::read(&mut db, token.claims.sub) {
797 Ok(user) if user.activated => user,
798 Ok(_) => return Err((400, "Account has not been activated")),
799 Err(_) => return Err((400, "Invalid token.")),
800 };
801
802 let salt = generate_salt();
803 let new_hash =
804 argon2::hash_encoded(item.new_password.as_bytes(), &salt, &ARGON_CONFIG).unwrap();
805
806 User::update(
807 &mut db,
808 user.id,
809 &UserChangeset {
810 email: user.email.clone(),
811 hash_password: new_hash,
812 activated: user.activated,
813 },
814 )
815 .map_err(|_| (500, "Could not update password"))?;
816
817 mailer.templates.send_password_reset(mailer, &user.email);
818
819 Ok(())
820}
821
822#[must_use]
823#[allow(clippy::missing_panics_doc)]
824pub fn generate_salt() -> [u8; 16] {
825 use rand::Fill;
826 let mut salt = [0; 16];
827 salt.try_fill(&mut rand::thread_rng()).unwrap();
829 salt
830}