#
mcaptcha/api/v1/account/
password.rs

    1// Copyright (C) 2022  Aravinth Manivannan <realaravinth@batsense.net>
    2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
    3//
    4// SPDX-License-Identifier: AGPL-3.0-or-later
    5
    6use actix_identity::Identity;
    7use actix_web::{web, HttpResponse, Responder};
    8use argon2_creds::Config;
    9use db_core::Login;
    10use serde::{Deserialize, Serialize};
    11
    12use crate::errors::*;
    13use crate::*;
    14
    15#[derive(Clone, Debug, Deserialize, Serialize)]
    16pub struct ChangePasswordReqest {
    17    pub password: String,
    18    pub new_password: String,
    19    pub confirm_new_password: String,
    20}
    21
    22pub struct UpdatePassword {
    23    pub new_password: String,
    24    pub confirm_new_password: String,
    25}
    26
    27impl From<ChangePasswordReqest> for UpdatePassword {
    28    fn from(s: ChangePasswordReqest) -> Self {
    29        UpdatePassword {
    30            new_password: s.new_password,
    31            confirm_new_password: s.confirm_new_password,
    32        }
    33    }
    34}
    35
    36async fn update_password_runner(
    37    user: &str,
    38    update: UpdatePassword,
    39    data: &Data,
    40) -> ServiceResult<()> {
    41    if update.new_password != update.confirm_new_password {
    42        return Err(ServiceError::PasswordsDontMatch);
    43    }
    44
    45    let new_hash = data.creds.password(&update.new_password)?;
    46
    47    let p = db_core::NameHash {
    48        username: user.to_owned(),
    49        hash: new_hash,
    50    };
    51
    52    data.db.update_password(&p).await?;
    53    Ok(())
    54}
    55
    56#[my_codegen::post(
    57    path = "crate::V1_API_ROUTES.account.update_password",
    58    wrap = "crate::api::v1::get_middleware()"
    59)]
    60async fn update_user_password(
    61    id: Identity,
    62    data: AppData,
    63    payload: web::Json<ChangePasswordReqest>,
    64) -> ServiceResult<impl Responder> {
    65    if payload.new_password != payload.confirm_new_password {
    66        return Err(ServiceError::PasswordsDontMatch);
    67    }
    68
    69    let username = id.identity().unwrap();
    70
    71    // TODO: verify behavior when account is not found
    72    let res = data.db.get_password(&Login::Username(&username)).await?;
    73
    74    if Config::verify(&res.hash, &payload.password)? {
    75        let update: UpdatePassword = payload.into_inner().into();
    76        update_password_runner(&username, update, &data).await?;
    77        Ok(HttpResponse::Ok())
    78    } else {
    79        Err(ServiceError::WrongPassword)
    80    }
    81}
    82
    83pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
    84    cfg.service(update_user_password);
    85}
    86
    87#[cfg(test)]
    88pub mod tests {
    89    use super::*;
    90
    91    use actix_web::http::StatusCode;
    92    use actix_web::test;
    93
    94    use crate::api::v1::ROUTES;
    95    use crate::tests::*;
    96
    97    #[actix_rt::test]
    98    async fn update_password_works_pg() {
    99        let data = crate::tests::pg::get_data().await;
    100        update_password_works(data).await;
    101    }
    102
    103    #[actix_rt::test]
    104    async fn update_password_works_maria() {
    105        let data = crate::tests::maria::get_data().await;
    106        update_password_works(data).await;
    107    }
    108
    109    pub async fn update_password_works(data: ArcData) {
    110        const NAME: &str = "updatepassuser";
    111        const PASSWORD: &str = "longpassword2";
    112        const EMAIL: &str = "updatepassuser@a.com";
    113
    114        let data = &data;
    115
    116        delete_user(data, NAME).await;
    117
    118        let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    119        let cookies = get_cookie!(signin_resp);
    120        let app = get_app!(data).await;
    121
    122        let new_password = "newpassword";
    123
    124        let update_password = ChangePasswordReqest {
    125            password: PASSWORD.into(),
    126            new_password: new_password.into(),
    127            confirm_new_password: PASSWORD.into(),
    128        };
    129
    130        let res = update_password_runner(NAME, update_password.into(), data).await;
    131        assert!(res.is_err());
    132        assert_eq!(res, Err(ServiceError::PasswordsDontMatch));
    133
    134        let update_password = ChangePasswordReqest {
    135            password: PASSWORD.into(),
    136            new_password: new_password.into(),
    137            confirm_new_password: new_password.into(),
    138        };
    139
    140        assert!(update_password_runner(NAME, update_password.into(), data)
    141            .await
    142            .is_ok());
    143
    144        let update_password = ChangePasswordReqest {
    145            password: new_password.into(),
    146            new_password: new_password.into(),
    147            confirm_new_password: PASSWORD.into(),
    148        };
    149
    150        bad_post_req_test(
    151            data,
    152            NAME,
    153            new_password,
    154            ROUTES.account.update_password,
    155            &update_password,
    156            ServiceError::PasswordsDontMatch,
    157        )
    158        .await;
    159
    160        let update_password = ChangePasswordReqest {
    161            password: PASSWORD.into(),
    162            new_password: PASSWORD.into(),
    163            confirm_new_password: PASSWORD.into(),
    164        };
    165
    166        bad_post_req_test(
    167            data,
    168            NAME,
    169            new_password,
    170            ROUTES.account.update_password,
    171            &update_password,
    172            ServiceError::WrongPassword,
    173        )
    174        .await;
    175
    176        let update_password = ChangePasswordReqest {
    177            password: new_password.into(),
    178            new_password: PASSWORD.into(),
    179            confirm_new_password: PASSWORD.into(),
    180        };
    181
    182        let update_password_resp = test::call_service(
    183            &app,
    184            post_request!(&update_password, ROUTES.account.update_password)
    185                .cookie(cookies)
    186                .to_request(),
    187        )
    188        .await;
    189        assert_eq!(update_password_resp.status(), StatusCode::OK);
    190    }
    191}