create_rust_app/auth/
controller.rs

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    // TODO: instead of initializing EncodingKey::from_secret(std::env::var("SECRET_KEY").unwrap().as_ref()) repeatedly in the code, just initialize it here and use it everywhere
22}
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))]
31/// Rust struct representing the Json body of
32/// POST requests to the .../login endpoint
33pub struct LoginInput {
34    email: String,
35    password: String,
36    device: Option<String>,
37    #[cfg(not(debug_assertions))]
38    ttl: Option<Seconds>, // Seconds
39    #[cfg(debug_assertions)]
40    ttl: Option<i64>, // Seconds
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44/// TODO: documentation
45pub 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))]
53/// Rust struct representing the Json body of
54/// POST requests to the .../register endpoint
55pub struct RegisterInput {
56    email: String,
57    password: String,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61/// TODO: documentation
62pub 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))]
70/// Rust struct representing the Json body of
71/// GET requests to the .../activate endpoint
72pub struct ActivationInput {
73    activation_token: String,
74}
75
76#[derive(Serialize, Deserialize)]
77#[cfg_attr(feature = "plugin_utoipa", derive(utoipa::ToSchema))]
78/// Rust struct representing the Json body of
79/// POST requests to the /forgot endpoint
80pub struct ForgotInput {
81    email: String,
82}
83
84#[derive(Debug, Serialize, Deserialize)]
85/// TODO: documentation
86pub 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))]
94/// Rust struct representing the Json body of
95/// POST requests to the /change endpoint
96pub 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))]
103/// Rust struct representing the Json body of
104/// POST requests to the /reset endpoint
105pub struct ResetInput {
106    reset_token: String,
107    new_password: String,
108}
109
110/// /sessions
111///
112/// queries [`db`](`Database`) for all sessions owned by the User
113/// associated with [`auth`](`Auth`)
114///
115/// breaks up the results of that query as defined by [`info`](`PaginationParams`)
116///
117/// # Returns [`Result`]
118/// - Ok([`UserSessionResponse`])
119///     - the results of the query paginated according to [`info`](`PaginationParams`)
120/// - Err([`StatusCode`], [`Message`])
121///
122/// # Errors
123/// - 500: Could not fetch sessions
124///
125/// # Panics
126/// - could not connect to database
127///
128/// TODO: don't panic if db connection fails, just return an error
129pub 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
165/// /sessions/{id}
166///
167/// deletes the entry in the `user_session` with the specified [`item_id`](`ID`) from
168/// [`db`](`Database`) if it's owned by the User associated with [`auth`](`Auth`)
169///
170/// # Errors
171/// - 404: Session not found
172/// - 500: Internal error
173/// - 500: Could not delete session
174///
175/// # Panics
176/// - could not connect to database
177///
178/// TODO: don't panic if db connection fails, just return an error
179pub 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
198/// /sessions
199///
200/// destroys all entries in the `user_session` table in [`db`](`Database`) owned
201/// by the User associated with [`auth`](`Auth`)
202///
203/// # Errors
204/// - 500: Could not delete sessions
205///
206/// # Panics
207/// - could not connect to database
208///
209/// TODO: don't panic if db connection fails, just return an error
210pub 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
222/// /login
223///
224/// creates a user session for the user associated with [`item`](`LoginInput`)
225/// in the request body (have the `content-type` header set to `application/json` and content that can be deserialized into [`LoginInput`])
226///
227/// # Returns [`Result`]
228/// - Ok([`AccessToken`], [`RefreshToken`])
229///     - an access token that should be sent to the user in the response body,
230///     - a reset token that should be sent as a secure, http-only, and `same_site=strict` cookie.
231/// - Err([`StatusCode`], [`Message`])
232///
233/// # Errors
234/// - 400: 'device' cannot be longer than 256 characters.
235/// - 400: Account has not been activated.
236/// - 401: Invalid credentials.
237///
238/// # Panics
239/// - could not connect to database
240/// - verifying the password hash fails
241///
242/// TODO: neither of these should panic, just return an error
243pub fn login(
244    db: &Database,
245    item: &LoginInput,
246) -> Result<(AccessToken, RefreshToken), (StatusCode, Message)> {
247    let mut db = db.get_connection().unwrap();
248
249    // verify device
250    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
279// TODO: Wrap this in a database transaction
280/// create a user session for the user with [`user_id`](`i32`)
281///
282/// # Errors
283/// - 400: 'device' cannot be longer than 256 characters.
284/// - 500: An internal server error occurred.
285/// - 500: Could not create session.
286///
287/// # Panics
288/// - could not connect to database
289/// - could not get `SECRET_KEY` from environment
290///
291/// TODO: don't panic if db connection fails, just return an error
292pub 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    // verify device
299    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 minutes */ 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
362/// /logout
363/// If this is successful, delete the cookie storing the refresh token
364///
365/// # Errors
366/// - 401: Invalid session
367/// - 401: Invalid token
368/// - 500: Could not delete session
369///
370/// # Panics
371/// - could not connect to database
372///
373/// TODO: don't panic if db connection fails, just return an error
374pub 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
390/// /refresh
391///
392/// refreshes the user session associated with the clients `refresh_token` cookie
393///
394/// # Returns [`Result`]
395/// - Ok([`AccessToken`], [`RefreshToken`])
396///     - an access token that should be sent to the user in the response body,
397///     - a reset token that should be sent as a secure, http-only, and `same_site=strict` cookie.
398/// - Err([`StatusCode`], [`Message`])
399///
400/// # Errors
401/// - 401: Invalid session
402/// - 401: Invalid token
403/// - 500: Could not update session
404/// - 500: An internal server error occurred
405///
406/// # Panics
407/// - could not connect to database
408/// - could not get `SECRET_KEY` from environment
409///
410/// TODO: don't panic if db connection fails, just return an error
411pub 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    // update session with the new refresh token
480    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
494/// /register
495///
496/// creates a new User with the information in [`item`](`RegisterInput`)
497///
498/// sends an email, using [`mailer`](`Mailer`), to the email address in [`item`](`RegisterInput`)
499/// that contains a unique link that allows the recipient to activate the account associated with
500/// that email address
501///
502/// # Errors
503/// - 400: Already registered
504///
505/// # Panics
506/// - could not connect to database
507/// - could not get `SECRET_KEY` from environment
508/// - any of the database operations fail
509///
510/// TODO: don't panic if db connection fails, just return an error
511pub 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        &registration_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
560/// /activate
561///
562/// activates the account associated with the token in [`item`](`ActivationInput`)
563///
564/// # Errors
565/// - 401: Invalid token
566/// - 401: Invalid token
567/// - 400: Invalid token
568/// - 200: Already activated!
569/// - 500: Could not activate user
570///
571/// # Panics
572/// - could not connect to database
573/// - could not get `SECRET_KEY` from environment
574///
575/// TODO: don't panic if db connection fails, just return an error
576pub 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
621/// /forgot
622/// sends an email to the email in the [`ForgotInput`] Json in the request body
623/// that will allow the user associated with that email to change their password
624///
625/// sends an email, using [`mailer`](`Mailer`), to the email address in [`item`](`RegisterInput`)
626/// that contains a unique link that allows the recipient to reset the password
627/// of the account associated with that email address (or create a new account if there is
628/// no accound accosiated with the email address)
629///
630/// # Errors
631/// - None
632///
633/// # Panics
634/// - could not connect to database
635/// - current timestamp could not be converted from `i64` to `usize`
636/// - could not get `SECRET_KEY` from environment
637///
638/// TODO: don't panic if db connection fails, just return an error
639pub 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        // if !user.activated {
650        //   return Ok(HttpResponse::build(400).body(" has not been activate"))
651        // }
652
653        #[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
681/// /change
682///
683/// change the password of the User associated with [`auth`](`Auth`)
684/// from [`item.old_password`](`ChangeInput`) to [`item.new_password`](`ChangeInput`)
685///
686/// # Errors
687/// - 400: Missing password
688/// - 400: The new password must be different
689/// - 400: Account has not been activated
690/// - 401: Invalid credentials
691/// - 500: Could not update password
692/// - 500: Could not find user
693///
694/// # Panics
695/// - could not connect to database
696/// - could not get `SECRET_KEY` from environment
697///
698/// TODO: don't panic if db connection fails, just return an error
699pub 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
753/// /check
754///
755/// just a lifeline function, clients can post to this endpoint to check
756/// if the auth service is running
757pub const fn check(_: &Auth) {}
758
759/// reset
760///
761/// changes the password of the user associated with [`item.reset_token`](`ResetInput`)
762/// to [`item.new_password`](`ResetInput`)
763///
764/// # Errors
765/// - 400: Missing password
766/// - 401: Invalid token
767/// - 400: Invalid token
768/// - 400: Account has not been activated
769/// - 500: Could not update password
770///
771/// # Panics
772/// - could not connect to database
773/// - could not get `SECRET_KEY` from environment
774///
775/// TODO: don't panic if db connection fails, just return an error
776pub 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    // this does not fail
828    salt.try_fill(&mut rand::thread_rng()).unwrap();
829    salt
830}