del user, add del domains

This commit is contained in:
realaravinth 2021-03-11 15:30:05 +05:30
parent 68322def60
commit a73725eb39
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
9 changed files with 310 additions and 72 deletions

6
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"

View file

@ -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<bool> {
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<Login>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
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;
}
}

116
src/api/v1/mcaptcha.rs Normal file
View file

@ -0,0 +1,116 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<Domain>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
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<Domain>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
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;
}
}

View file

@ -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);
}

View file

@ -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<CredsError> for ServiceError {
}
}
// impl From<ValidationErrors> for ServiceError {
// fn from(_: ValidationErrors) -> ServiceError {
// ServiceError::NotAnEmail
// }
// }
//
impl From<ValidationErrors> for ServiceError {
fn from(_: ValidationErrors) -> ServiceError {
ServiceError::NotAnEmail
}
}
impl From<ParseError> for ServiceError {
fn from(_: ParseError) -> ServiceError {
ServiceError::NotAUrl
}
}
#[cfg(not(tarpaulin_include))]
impl From<sqlx::Error> for ServiceError {

View file

@ -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;

View file

@ -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();
}

87
src/tests/mod.rs Normal file
View file

@ -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)
}