diff --git a/sqlx-data.json b/sqlx-data.json index 9bfea1c0..7e93ebf8 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -250,33 +250,6 @@ "nullable": [] } }, - "546a37c638f7e50c94093894a0359c944b50ccb12fd4272b2d065f62a584625a": { - "query": "SELECT key, name from mcaptcha_config\n WHERE key = ($1) AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "key", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false - ] - } - }, "60081afa71dca3d10b372aabfdbc809f0cf62b33994a3bb43ea444159c6544fe": { "query": "INSERT INTO mcaptcha_notifications (\n heading, message, tx, rx)\n VALUES (\n $1, $2,\n (SELECT ID FROM mcaptcha_users WHERE name = $3),\n (SELECT ID FROM mcaptcha_users WHERE name = $4)\n );", "describe": { @@ -405,6 +378,32 @@ "nullable": [] } }, + "90608e874ec931db397dc7b357b60bc794fffec5e2eb59c0556808ea8dfef9e9": { + "query": "SELECT ID, password FROM mcaptcha_users WHERE name = ($1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "password", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + } + }, "931879575bb70dece5596bfae18f55a628d10627e4b6825e54642b254ca4ee64": { "query": "INSERT INTO mcaptcha_pow_confirmed_stats \n (config_id) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1))", "describe": { @@ -458,6 +457,19 @@ ] } }, + "9c7a654aefa0a1683d9b07ff00c8edb0ee292e003c13ec99a419e563591c15e4": { + "query": "DELETE FROM mcaptcha_config WHERE key = ($1) AND user_id = $2;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [] + } + }, "ad23588ee4bcbb13e208460ce21e2fa9f1373893934b530b339fea10360b34a8": { "query": "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)", "describe": { @@ -537,19 +549,6 @@ ] } }, - "ca46ec86f8fe891daf58cae9292783bd5b1c1103fef03138904e498c26831cc3": { - "query": "DELETE FROM mcaptcha_config \n WHERE key = ($1) AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - } - }, "ca9d5241f1234d1825f7ead391ebe9099fca776e7101ac6e1761881606def5fa": { "query": "DELETE FROM mcaptcha_users WHERE name = ($1)", "describe": { @@ -562,6 +561,19 @@ "nullable": [] } }, + "d85750d86bbafeaf6f52cec3d49d708bef1a9ef85bbd9c55d63c9c27cb93223c": { + "query": "DELETE FROM mcaptcha_levels \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE key = $1 AND user_id = $2\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [] + } + }, "daebbef26cf04fdc46226304d028528e121a9847c07139d7d3a56a0e7c165879": { "query": "SELECT solved_at FROM mcaptcha_pow_solved_stats WHERE config_id = \n (SELECT config_id FROM mcaptcha_config where key = $1)", "describe": { @@ -646,6 +658,19 @@ ] } }, + "e98d0614d982fe7c04d78d457c3ce79e8d4d0bcaac28c8a3edecdbc9def04ea2": { + "query": "UPDATE mcaptcha_users set password = $1\n WHERE name = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, "f330cb94c53d33495df94aacec7e4e91d8a920742b89a63d1c59a8ea8937c5c8": { "query": "INSERT INTO mcaptcha_levels (\n difficulty_factor, \n visitor_threshold,\n config_id) VALUES (\n $1, $2, (\n SELECT config_id FROM mcaptcha_config WHERE\n key = ($3) AND user_id = (\n SELECT ID FROM mcaptcha_users WHERE name = $4\n )));", "describe": { diff --git a/src/api/v1/account/mod.rs b/src/api/v1/account/mod.rs index b60012e1..59149f98 100644 --- a/src/api/v1/account/mod.rs +++ b/src/api/v1/account/mod.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; pub mod delete; pub mod email; +pub mod password; pub mod secret; #[cfg(test)] pub mod test; @@ -34,6 +35,7 @@ pub mod routes { pub email_exists: &'static str, pub get_secret: &'static str, pub update_email: &'static str, + pub update_password: &'static str, pub update_secret: &'static str, pub username_exists: &'static str, } @@ -46,12 +48,13 @@ pub mod routes { let email_exists = "/api/v1/account/email/exists"; let username_exists = "/api/v1/account/username/exists"; let update_email = "/api/v1/account/email/update"; + let update_password = "/api/v1/account/password/update"; Account { delete, email_exists, - get_secret, update_email, + update_password, update_secret, username_exists, } @@ -74,4 +77,5 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) { email::services(cfg); username::services(cfg); secret::services(cfg); + password::services(cfg); } diff --git a/src/api/v1/account/password.rs b/src/api/v1/account/password.rs new file mode 100644 index 00000000..8a548eb9 --- /dev/null +++ b/src/api/v1/account/password.rs @@ -0,0 +1,208 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use argon2_creds::Config; +use serde::{Deserialize, Serialize}; +use sqlx::Error::RowNotFound; + +use crate::api::v1::auth::runners::Password; +use crate::errors::*; +use crate::*; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ChangePasswordReqest { + pub password: String, + pub new_password: String, + pub confirm_new_password: String, +} + +pub struct UpdatePassword { + pub new_password: String, + pub confirm_new_password: String, +} + +impl From for UpdatePassword { + fn from(s: ChangePasswordReqest) -> Self { + UpdatePassword { + new_password: s.new_password, + confirm_new_password: s.confirm_new_password, + } + } +} + +async fn update_password_runner( + user: &str, + update: UpdatePassword, + data: &Data, +) -> ServiceResult<()> { + if update.new_password != update.confirm_new_password { + return Err(ServiceError::PasswordsDontMatch); + } + + let new_hash = data.creds.password(&update.new_password)?; + + sqlx::query!( + "UPDATE mcaptcha_users set password = $1 + WHERE name = $2", + &new_hash, + &user, + ) + .execute(&data.db) + .await?; + + Ok(()) +} + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.account.update_password", + wrap = "crate::CheckLogin" +)] +async fn update_user_password( + id: Identity, + data: AppData, + payload: web::Json, +) -> ServiceResult { + if payload.new_password != payload.confirm_new_password { + return Err(ServiceError::PasswordsDontMatch); + } + + let username = id.identity().unwrap(); + + let rec = sqlx::query_as!( + Password, + r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#, + &username, + ) + .fetch_one(&data.db) + .await; + + match rec { + Ok(s) => { + if Config::verify(&s.password, &payload.password)? { + let update: UpdatePassword = payload.into_inner().into(); + update_password_runner(&username, update, &data).await?; + Ok(HttpResponse::Ok()) + } else { + Err(ServiceError::WrongPassword) + } + } + Err(RowNotFound) => Err(ServiceError::AccountNotFound), + Err(_) => Err(ServiceError::InternalServerError), + } +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(update_user_password); +} + +#[cfg(test)] +mod tests { + use super::*; + + use actix_web::http::{header, StatusCode}; + use actix_web::test; + + use crate::api::v1::ROUTES; + use crate::data::Data; + use crate::tests::*; + + #[actix_rt::test] + async fn update_password_works() { + const NAME: &str = "updatepassuser"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "updatepassuser@a.com"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(data).await; + + let new_password = "newpassword"; + + let update_password = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: new_password.into(), + confirm_new_password: PASSWORD.into(), + }; + + let res = update_password_runner(NAME, update_password.into(), &data).await; + assert!(res.is_err()); + assert_eq!(res, Err(ServiceError::PasswordsDontMatch)); + + let update_password = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: new_password.into(), + confirm_new_password: new_password.into(), + }; + + assert!(update_password_runner(NAME, update_password.into(), &data) + .await + .is_ok()); + + let update_password = ChangePasswordReqest { + password: new_password.into(), + new_password: new_password.into(), + confirm_new_password: PASSWORD.into(), + }; + + bad_post_req_test( + NAME, + &new_password, + ROUTES.account.update_password, + &update_password, + ServiceError::PasswordsDontMatch, + StatusCode::BAD_REQUEST, + ) + .await; + + let update_password = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: PASSWORD.into(), + confirm_new_password: PASSWORD.into(), + }; + + bad_post_req_test( + NAME, + &new_password, + ROUTES.account.update_password, + &update_password, + ServiceError::WrongPassword, + StatusCode::UNAUTHORIZED, + ) + .await; + + let update_password = ChangePasswordReqest { + password: new_password.into(), + new_password: PASSWORD.into(), + confirm_new_password: PASSWORD.into(), + }; + + let update_password_resp = test::call_service( + &app, + post_request!(&update_password, ROUTES.account.update_password) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(update_password_resp.status(), StatusCode::OK); + } +} diff --git a/src/api/v1/account/test.rs b/src/api/v1/account/test.rs index 1eec5ecd..17924802 100644 --- a/src/api/v1/account/test.rs +++ b/src/api/v1/account/test.rs @@ -131,7 +131,7 @@ async fn email_udpate_password_validation_del_userworks() { } let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await; - let (data, creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let (data, _creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; let cookies = get_cookie!(signin_resp); let app = get_app!(data).await;