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}