From a73725eb39d7f53789ed56f85dc615a56624b03e Mon Sep 17 00:00:00 2001 From: realaravinth Date: Thu, 11 Mar 2021 15:30:05 +0530 Subject: [PATCH] del user, add del domains --- Cargo.lock | 6 +- Cargo.toml | 4 +- src/api/v1/auth.rs | 133 +++++++++++++++++++++++------------------ src/api/v1/mcaptcha.rs | 116 +++++++++++++++++++++++++++++++++++ src/api/v1/mod.rs | 7 +++ src/errors.rs | 23 ++++--- src/main.rs | 3 + src/tests-migrate.rs | 3 +- src/tests/mod.rs | 87 +++++++++++++++++++++++++++ 9 files changed, 310 insertions(+), 72 deletions(-) create mode 100644 src/api/v1/mcaptcha.rs create mode 100644 src/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 42277a7d..2c366964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,7 +415,7 @@ dependencies = [ [[package]] name = "argon2-creds" version = "0.2.0" -source = "git+https://github.com/realaravinth/argon2-creds?tag=0.2.0#1934a6cc1b1fd4b9583da2ea6862f176c03cbfb6" +source = "git+https://github.com/realaravinth/argon2-creds#61f2d1d5a2660905939054f6be03e76584965b63" dependencies = [ "ammonia", "derive_builder", @@ -426,7 +426,6 @@ dependencies = [ "rust-argon2", "unicode-normalization", "validator", - "validator_derive", ] [[package]] @@ -1861,7 +1860,7 @@ dependencies = [ [[package]] name = "pow_sha256" version = "0.2.0" -source = "git+https://github.com/mcaptcha/pow_sha256#d0889fc9008e63795b024b57432ed0034a703d0b" +source = "git+https://github.com/mcaptcha/pow_sha256#1b65c603bdd527e3e1f3b8b565a11fcde48575b5" dependencies = [ "bincode", "derive_builder", @@ -2930,6 +2929,7 @@ dependencies = [ "serde_derive", "serde_json", "url", + "validator_derive", "validator_types", ] diff --git a/Cargo.toml b/Cargo.toml index d48e42b7..66d7bf03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,10 @@ actix-web = "3" actix = "0.10" sqlx = { version = "0.4.0", features = [ "runtime-actix-rustls", "postgres" ] } -argon2-creds = { version = "0.2", git = "https://github.com/realaravinth/argon2-creds", tag = "0.2.0" } +argon2-creds = { version = "0.2", git = "https://github.com/realaravinth/argon2-creds", commit = "61f2d1d" } config = "0.10" -validator = "0.12" +validator = { version = "0.12", features = ["derive"]} derive_builder = "0.9" derive_more = "0.99" diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs index 6089206a..cc673ad4 100644 --- a/src/api/v1/auth.rs +++ b/src/api/v1/auth.rs @@ -23,11 +23,6 @@ use serde::{Deserialize, Serialize}; use crate::errors::*; use crate::Data; -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct SomeData { - pub a: String, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Register { pub username: String, @@ -104,16 +99,55 @@ pub async fn signout(id: Identity) -> impl Responder { HttpResponse::Ok() } -fn is_authenticated(id: &Identity) -> ServiceResult { - debug!("{:?}", id.identity()); +/// Check if user is authenticated +// TODO use middleware +pub fn is_authenticated(id: &Identity) -> ServiceResult<()> { // access request identity if let Some(_) = id.identity() { - Ok(true) + Ok(()) } else { Err(ServiceError::AuthorizationRequired) } } +#[post("/api/v1/account/delete")] +pub async fn delete_account( + id: Identity, + payload: web::Json, + data: web::Data, +) -> ServiceResult { + use argon2_creds::Config; + use sqlx::Error::RowNotFound; + + is_authenticated(&id)?; + + let rec = sqlx::query_as!( + Password, + r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#, + &payload.username, + ) + .fetch_one(&data.db) + .await; + + match rec { + Ok(s) => { + if Config::verify(&s.password, &payload.password)? { + sqlx::query!( + "DELETE FROM mcaptcha_users WHERE name = ($1)", + &payload.username, + ) + .execute(&data.db) + .await?; + Ok(HttpResponse::Ok()) + } else { + Err(ServiceError::WrongPassword) + } + } + Err(RowNotFound) => return Err(ServiceError::UsernameNotFound), + Err(_) => return Err(ServiceError::InternalServerError)?, + } +} + #[cfg(test)] mod tests { use actix_web::http::{header, StatusCode}; @@ -124,34 +158,7 @@ mod tests { use crate::data::Data; use crate::*; - pub async fn delete_user(name: &str, data: &Data) { - let _ = sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,) - .execute(&data.db) - .await; - } - - macro_rules! post_request { - ($serializable:expr, $uri:expr) => { - test::TestRequest::post() - .uri($uri) - .header(header::CONTENT_TYPE, "application/json") - .set_payload(serde_json::to_string($serializable).unwrap()) - }; - } - - macro_rules! get_server { - () => { - App::new() - .wrap(middleware::Logger::default()) - .wrap(get_identity_service()) - .wrap(middleware::Compress::default()) - .wrap(middleware::NormalizePath::new( - middleware::normalize::TrailingSlash::Trim, - )) - .app_data(get_json_err()) - .configure(v1_services) - }; - } + use crate::tests::*; #[actix_rt::test] async fn auth_works() { @@ -160,39 +167,25 @@ mod tests { const PASSWORD: &str = "longpassword"; const EMAIL: &str = "testuser1@a.com"; - let mut app = test::init_service(get_server!().data(data.clone())).await; + let mut app = get_app!(data).await; delete_user(NAME, &data).await; - // 1. Register + // 1. Register and signin + let (data, _, signin_resp) = signin_util(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + + // 2. check if duplicate username is allowed let msg = Register { username: NAME.into(), password: PASSWORD.into(), email: EMAIL.into(), }; - let resp = - test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; - assert_eq!(resp.status(), StatusCode::OK); - - // 2. check if duplicate username is allowed let duplicate_user_resp = test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; assert_eq!(duplicate_user_resp.status(), StatusCode::BAD_REQUEST); - // 3. signin - let sigin_msg = Login { - username: NAME.into(), - password: PASSWORD.into(), - }; - let signin_resp = test::call_service( - &mut app, - post_request!(&sigin_msg, "/api/v1/signin").to_request(), - ) - .await; - assert_eq!(signin_resp.status(), StatusCode::OK); - let cookies = signin_resp.response().cookies().next().unwrap().to_owned(); - - // 4. sigining in with non-existent user + // 3. sigining in with non-existent user let nonexistantuser = Login { username: "nonexistantuser".into(), password: msg.password.clone(), @@ -206,7 +199,7 @@ mod tests { let txt: ErrorToResponse = test::read_body_json(userdoesntexist).await; assert_eq!(txt.error, format!("{}", ServiceError::UsernameNotFound)); - // 5. trying to signin with wrong password + // 4. trying to signin with wrong password let wrongpassword = Login { username: NAME.into(), password: NAME.into(), @@ -220,7 +213,7 @@ mod tests { let txt: ErrorToResponse = test::read_body_json(wrongpassword_resp).await; assert_eq!(txt.error, format!("{}", ServiceError::WrongPassword)); - // 6. signout + // 5. signout let signout_resp = test::call_service( &mut app, post_request!(&wrongpassword, "/api/v1/signout") @@ -232,4 +225,26 @@ mod tests { delete_user(NAME, &data).await; } + + #[actix_rt::test] + async fn del_userworks() { + const NAME: &str = "testuser2"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "testuser1@a.com2"; + + let (data, creds, signin_resp) = signin_util(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + let delete_user_resp = test::call_service( + &mut app, + post_request!(&creds, "/api/v1/account/delete") + .cookie(cookies) + .to_request(), + ) + .await; + + assert_eq!(delete_user_resp.status(), StatusCode::OK); + delete_user(NAME, &data).await; + } } diff --git a/src/api/v1/mcaptcha.rs b/src/api/v1/mcaptcha.rs new file mode 100644 index 00000000..cb6ab26f --- /dev/null +++ b/src/api/v1/mcaptcha.rs @@ -0,0 +1,116 @@ +/* +* 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::{post, web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::auth::is_authenticated; +use crate::errors::*; +use crate::Data; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Domain { + pub name: String, +} + +#[post("/api/v1/mcaptcha/domain/add")] +pub async fn add_domain( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + let url = Url::parse(&payload.name)?; + if let Some(host) = url.host_str() { + sqlx::query!("INSERT INTO mcaptcha_domains (name) VALUES ($1)", host,) + .execute(&data.db) + .await?; + Ok(HttpResponse::Ok()) + } else { + Err(ServiceError::NotAUrl) + } +} + +#[post("/api/v1/mcaptcha/domain/delete")] +pub async fn delete_domain( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + let url = Url::parse(&payload.name)?; + if let Some(host) = url.host_str() { + sqlx::query!("DELETE FROM mcaptcha_domains WHERE name = ($1)", host,) + .execute(&data.db) + .await?; + Ok(HttpResponse::Ok()) + } else { + Err(ServiceError::NotAUrl) + } +} + +#[cfg(test)] +mod tests { + use actix_web::http::{header, StatusCode}; + use actix_web::test; + + use super::*; + use crate::api::v1::services as v1_services; + use crate::tests::*; + use crate::*; + + #[actix_rt::test] + async fn add_domains_work() { + const NAME: &str = "testuserdomain"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testuserdomain@a.com"; + const DOMAIN: &str = "http://example.com"; + + let (data, _, signin_resp) = signin_util(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + delete_domain_util(DOMAIN, &data).await; + + // 1. add domain + let domain = Domain { + name: DOMAIN.into(), + }; + + let add_domain_resp = test::call_service( + &mut app, + post_request!(&domain, "/api/v1/mcaptcha/domain/add") + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_domain_resp.status(), StatusCode::OK); + + // 2. delete domain + let del_domain_resp = test::call_service( + &mut app, + post_request!(&domain, "/api/v1/mcaptcha/domain/delete") + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(del_domain_resp.status(), StatusCode::OK); + delete_user(NAME, &data).await; + } +} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 2ffcc287..bc06b77b 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -18,10 +18,17 @@ use actix_web::web::ServiceConfig; pub mod auth; +pub mod mcaptcha; pub fn services(cfg: &mut ServiceConfig) { use auth::*; + use mcaptcha::*; + cfg.service(signout); cfg.service(signin); cfg.service(signup); + cfg.service(delete_account); + + cfg.service(add_domain); + cfg.service(delete_domain); } diff --git a/src/errors.rs b/src/errors.rs index 869cb3e6..eb5b12b9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -23,11 +23,12 @@ use actix_web::{ }; use argon2_creds::errors::CredsError; +use url::ParseError; use derive_more::{Display, Error}; use log::debug; use serde::{Deserialize, Serialize}; -// use validator::ValidationErrors; +use validator::ValidationErrors; use std::convert::From; @@ -67,6 +68,8 @@ pub enum ServiceError { PasswordTooShort, #[display(fmt = "Username too long")] PasswordTooLong, + #[display(fmt = "The value you entered for URL is not a URL")] //405j + NotAUrl, } #[derive(Serialize, Deserialize)] @@ -91,6 +94,7 @@ impl ResponseError for ServiceError { match *self { ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, + ServiceError::NotAUrl => StatusCode::BAD_REQUEST, ServiceError::WrongPassword => StatusCode::UNAUTHORIZED, ServiceError::UsernameNotFound => StatusCode::UNAUTHORIZED, ServiceError::AuthorizationRequired => StatusCode::UNAUTHORIZED, @@ -120,12 +124,17 @@ impl From for ServiceError { } } -// impl From for ServiceError { -// fn from(_: ValidationErrors) -> ServiceError { -// ServiceError::NotAnEmail -// } -// } -// +impl From for ServiceError { + fn from(_: ValidationErrors) -> ServiceError { + ServiceError::NotAnEmail + } +} + +impl From for ServiceError { + fn from(_: ParseError) -> ServiceError { + ServiceError::NotAUrl + } +} #[cfg(not(tarpaulin_include))] impl From for ServiceError { diff --git a/src/main.rs b/src/main.rs index 57a8e375..150481f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ mod errors; //mod routes; mod api; mod settings; +#[cfg(test)] +#[macro_use] +mod tests; pub use data::Data; pub use settings::Settings; diff --git a/src/tests-migrate.rs b/src/tests-migrate.rs index 080f8a55..9397b5c2 100644 --- a/src/tests-migrate.rs +++ b/src/tests-migrate.rs @@ -23,8 +23,9 @@ mod settings; pub use data::Data; pub use settings::Settings; -lazy_static! { #[cfg(not(tarpaulin_include))] +lazy_static! { + #[cfg(not(tarpaulin_include))] pub static ref SETTINGS: Settings = Settings::new().unwrap(); } diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 00000000..aea9c144 --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,87 @@ +use actix_web::test; +use actix_web::{ + dev::ServiceResponse, + http::{header, StatusCode}, +}; + +use super::*; +use crate::api::v1::auth::{Login, Register}; +use crate::api::v1::services as v1_services; +use crate::data::Data; + +#[macro_export] +macro_rules! get_cookie { + ($resp:expr) => { + $resp.response().cookies().next().unwrap().to_owned() + }; +} + +pub async fn delete_user(name: &str, data: &Data) { + let _ = sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,) + .execute(&data.db) + .await; +} + +pub async fn delete_domain_util(name: &str, data: &Data) { + let _ = sqlx::query!("DELETE FROM mcaptcha_domains WHERE name = ($1)", name,) + .execute(&data.db) + .await; +} + +#[macro_export] +macro_rules! post_request { + ($serializable:expr, $uri:expr) => { + test::TestRequest::post() + .uri($uri) + .header(header::CONTENT_TYPE, "application/json") + .set_payload(serde_json::to_string($serializable).unwrap()) + }; +} + +#[macro_export] +macro_rules! get_app { + ($data:expr) => { + test::init_service( + App::new() + .wrap(get_identity_service()) + .configure(v1_services) + .data($data.clone()), + ) + }; +} + +/// register and signin utility +pub async fn signin_util<'a>( + name: &'a str, + email: &str, + password: &str, +) -> (data::Data, Login, ServiceResponse) { + let data = Data::new().await; + let mut app = get_app!(data).await; + + delete_user(&name, &data).await; + + // 1. Register + let msg = Register { + username: name.into(), + password: password.into(), + email: email.into(), + }; + let resp = + test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; + assert_eq!(resp.status(), StatusCode::OK); + + // 2. signin + let creds = Login { + username: name.into(), + password: password.into(), + }; + let signin_resp = test::call_service( + &mut app, + post_request!(&creds, "/api/v1/signin").to_request(), + ) + .await; + assert_eq!(signin_resp.status(), StatusCode::OK); + + (data, creds, signin_resp) +}