diff --git a/src/api/v1/account/delete.rs b/src/api/v1/account/delete.rs index 69005e4d..dbbc95a3 100644 --- a/src/api/v1/account/delete.rs +++ b/src/api/v1/account/delete.rs @@ -26,7 +26,7 @@ use crate::AppData; path = "crate::V1_API_ROUTES.account.delete", wrap = "crate::CheckLogin" )] -async fn delete_account( +pub async fn delete_account( id: Identity, payload: web::Json, data: AppData, diff --git a/src/api/v1/mcaptcha/create.rs b/src/api/v1/mcaptcha/create.rs new file mode 100644 index 00000000..bc09bb36 --- /dev/null +++ b/src/api/v1/mcaptcha/create.rs @@ -0,0 +1,153 @@ +/* + * 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 std::borrow::Cow; + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use libmcaptcha::defense::Level; +use serde::{Deserialize, Serialize}; + +use super::get_random; +use crate::errors::*; +use crate::AppData; + +#[derive(Serialize, Deserialize)] +pub struct CreateCaptcha { + pub levels: Vec, + pub duration: u32, + pub description: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MCaptchaDetails { + pub name: String, + pub key: String, +} + +// TODO redo mcaptcha table to include levels as json field +// so that the whole thing can be added/udpaed in a single stroke +#[my_codegen::post( + path = "crate::V1_API_ROUTES.captcha.create", + wrap = "crate::CheckLogin" +)] +pub async fn create( + payload: web::Json, + data: AppData, + id: Identity, +) -> ServiceResult { + let username = id.identity().unwrap(); + let mcaptcha_config = runner::create(&payload, &data, &username).await?; + Ok(HttpResponse::Ok().json(mcaptcha_config)) +} + +pub mod runner { + use futures::future::try_join_all; + use libmcaptcha::DefenseBuilder; + use log::debug; + + use super::*; + + pub async fn create( + payload: &CreateCaptcha, + data: &AppData, + username: &str, + ) -> ServiceResult { + let mut defense = DefenseBuilder::default(); + for level in payload.levels.iter() { + defense.add_level(*level)?; + } + + defense.build()?; + + debug!("creating config"); + let mcaptcha_config = + // add_mcaptcha_util(payload.duration, &payload.description, &data, username).await?; + + { + let mut key; + + let resp; + + loop { + key = get_random(32); + + let res = sqlx::query!( + "INSERT INTO mcaptcha_config + (key, user_id, duration, name) + VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)", + &key, + &username, + payload.duration as i32, + &payload.description, + ) + .execute(&data.db) + .await; + + match res { + Err(sqlx::Error::Database(err)) => { + if err.code() == Some(Cow::from("23505")) + && err.message().contains("mcaptcha_config_key_key") + { + continue; + } else { + return Err(sqlx::Error::Database(err).into()); + } + } + Err(e) => return Err(e.into()), + + Ok(_) => { + resp = MCaptchaDetails { + key, + name: payload.description.to_owned(), + }; + break; + } + } + } + resp + }; + + debug!("config created"); + + let mut futs = Vec::with_capacity(payload.levels.len()); + + for level in payload.levels.iter() { + let difficulty_factor = level.difficulty_factor as i32; + let visitor_threshold = level.visitor_threshold as i32; + let fut = sqlx::query!( + "INSERT INTO mcaptcha_levels ( + difficulty_factor, + visitor_threshold, + config_id) VALUES ( + $1, $2, ( + SELECT config_id FROM mcaptcha_config WHERE + key = ($3) AND user_id = ( + SELECT ID FROM mcaptcha_users WHERE name = $4 + )));", + difficulty_factor, + visitor_threshold, + &mcaptcha_config.key, + &username, + ) + .execute(&data.db); + futs.push(fut); + } + + try_join_all(futs).await?; + Ok(mcaptcha_config) + } +} diff --git a/src/api/v1/mcaptcha/delete.rs b/src/api/v1/mcaptcha/delete.rs new file mode 100644 index 00000000..3ffb07d4 --- /dev/null +++ b/src/api/v1/mcaptcha/delete.rs @@ -0,0 +1,95 @@ +/* + * 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 libmcaptcha::master::messages::RemoveCaptcha; +use serde::{Deserialize, Serialize}; + +use crate::errors::*; +use crate::AppData; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DeleteCaptcha { + pub key: String, + pub password: String, +} + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.captcha.delete", + wrap = "crate::CheckLogin" +)] +async fn delete( + payload: web::Json, + data: AppData, + id: Identity, +) -> ServiceResult { + use argon2_creds::Config; + use sqlx::Error::RowNotFound; + + let username = id.identity().unwrap(); + + struct PasswordID { + password: String, + id: i32, + } + + let rec = sqlx::query_as!( + PasswordID, + r#"SELECT ID, password FROM mcaptcha_users WHERE name = ($1)"#, + &username, + ) + .fetch_one(&data.db) + .await; + + match rec { + Ok(rec) => { + if Config::verify(&rec.password, &payload.password)? { + let payload = payload.into_inner(); + sqlx::query!( + "DELETE FROM mcaptcha_levels + WHERE config_id = ( + SELECT config_id FROM mcaptcha_config + WHERE key = $1 AND user_id = $2 + );", + &payload.key, + &rec.id, + ) + .execute(&data.db) + .await?; + + sqlx::query!( + "DELETE FROM mcaptcha_config WHERE key = ($1) AND user_id = $2;", + &payload.key, + &rec.id, + ) + .execute(&data.db) + .await?; + if let Err(err) = data.captcha.remove(RemoveCaptcha(payload.key)).await { + log::error!( + "Error while trying to remove captcha from cache {}", + err + ); + } + Ok(HttpResponse::Ok()) + } else { + Err(ServiceError::WrongPassword) + } + } + Err(RowNotFound) => Err(ServiceError::UsernameNotFound), + Err(_) => Err(ServiceError::InternalServerError), + } +} diff --git a/src/api/v1/mcaptcha/duration.rs b/src/api/v1/mcaptcha/duration.rs deleted file mode 100644 index b45c89f6..00000000 --- a/src/api/v1/mcaptcha/duration.rs +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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 serde::{Deserialize, Serialize}; - -use crate::api::v1::mcaptcha::captcha::MCaptchaDetails; -use crate::errors::*; -use crate::AppData; - -pub mod routes { - pub struct Duration { - pub update: &'static str, - pub get: &'static str, - } - impl Duration { - pub const fn new() -> Duration { - Duration { - update: "/api/v1/mcaptcha/domain/token/duration/update", - get: "/api/v1/mcaptcha/domain/token/duration/get", - } - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct UpdateDuration { - pub key: String, - pub duration: i32, -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.duration.update", - wrap = "crate::CheckLogin" -)] -async fn update_duration( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - - if payload.duration > 0 { - sqlx::query!( - "UPDATE mcaptcha_config set duration = $1 - WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)", - &payload.duration, - &payload.key, - &username, - ) - .execute(&data.db) - .await?; - - Ok(HttpResponse::Ok()) - } else { - // when mCaptcha/mCaptcha #2 is fixed, this wont be necessary - Err(ServiceError::CaptchaError( - libmcaptcha::errors::CaptchaError::CaptchaDurationZero, - )) - } -} - -#[derive(Deserialize, Serialize)] -pub struct GetDurationResp { - pub duration: i32, -} - -#[derive(Deserialize, Serialize)] -pub struct GetDuration { - pub token: String, -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.duration.get", - wrap = "crate::CheckLogin" -)] -async fn get_duration( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - - let duration = sqlx::query_as!( - GetDurationResp, - "SELECT duration FROM mcaptcha_config - WHERE key = $1 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)", - &payload.key, - &username, - ) - .fetch_one(&data.db) - .await?; - Ok(HttpResponse::Ok().json(duration)) -} - -pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(get_duration); - cfg.service(update_duration); -} - -#[cfg(test)] -mod tests { - use actix_web::http::StatusCode; - use actix_web::test; - - use super::*; - use crate::api::v1::ROUTES; - use crate::tests::*; - use crate::*; - - #[actix_rt::test] - async fn update_duration() { - const NAME: &str = "testuserduration"; - const PASSWORD: &str = "longpassworddomain"; - const EMAIL: &str = "testuserduration@a.com"; - - { - let data = Data::new().await; - delete_user(NAME, &data).await; - } - - register_and_signin(NAME, EMAIL, PASSWORD).await; - let (data, _, signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await; - let cookies = get_cookie!(signin_resp); - let app = get_app!(data).await; - - let update = UpdateDuration { - key: token_key.key.clone(), - duration: 40, - }; - - // check default - - let get_level_resp = test::call_service( - &app, - post_request!(&token_key, ROUTES.duration.get) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(get_level_resp.status(), StatusCode::OK); - let res_levels: GetDurationResp = test::read_body_json(get_level_resp).await; - assert_eq!(res_levels.duration, 30); - - // update and check changes - - let update_duration = test::call_service( - &app, - post_request!(&update, ROUTES.duration.update) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(update_duration.status(), StatusCode::OK); - let get_level_resp = test::call_service( - &app, - post_request!(&token_key, ROUTES.duration.get) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(get_level_resp.status(), StatusCode::OK); - let res_levels: GetDurationResp = test::read_body_json(get_level_resp).await; - assert_eq!(res_levels.duration, 40); - } -} diff --git a/src/api/v1/mcaptcha/captcha.rs b/src/api/v1/mcaptcha/easy.rs similarity index 60% rename from src/api/v1/mcaptcha/captcha.rs rename to src/api/v1/mcaptcha/easy.rs index ee8f8c85..4d8ed523 100644 --- a/src/api/v1/mcaptcha/captcha.rs +++ b/src/api/v1/mcaptcha/easy.rs @@ -14,213 +14,37 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -use std::borrow::Cow; - use actix_identity::Identity; use actix_web::{web, HttpResponse, Responder}; -use libmcaptcha::master::messages::{RemoveCaptcha, RenameBuilder}; use libmcaptcha::{defense::Level, defense::LevelBuilder}; use serde::{Deserialize, Serialize}; -use super::get_random; -use super::levels::{add_captcha_runner, update_level_runner, AddLevels, UpdateLevels}; +use super::create::{runner::create as create_runner, CreateCaptcha}; +use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha}; use crate::errors::*; use crate::settings::DefaultDifficultyStrategy; -use crate::stats::fetch::{Stats, StatsUnixTimestamp}; use crate::AppData; pub mod routes { - pub struct MCaptcha { - pub delete: &'static str, - pub update_key: &'static str, - pub stats: &'static str, + pub struct Easy { /// easy is using defaults - pub create_easy: &'static str, - pub update_easy: &'static str, + pub create: &'static str, + pub update: &'static str, } - impl MCaptcha { - pub const fn new() -> MCaptcha { - MCaptcha { - update_key: "/api/v1/mcaptcha/update/key", - delete: "/api/v1/mcaptcha/delete", - stats: "/api/v1/mcaptcha/stats", - create_easy: "/api/v1/mcaptcha/add/easy", - update_easy: "/api/v1/mcaptcha/update/easy", + impl Easy { + pub const fn new() -> Self { + Self { + create: "/api/v1/mcaptcha/add/easy", + update: "/api/v1/mcaptcha/update/easy", } } } } pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(update_token); - cfg.service(delete_mcaptcha); - cfg.service(get_stats); - cfg.service(create_easy); - cfg.service(update_easy); -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct MCaptchaID { - pub name: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct MCaptchaDetails { - pub name: String, - pub key: String, -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.mcaptcha.update_key", - wrap = "crate::CheckLogin" -)] -async fn update_token( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - let mut key; - - loop { - key = get_random(32); - let res = update_token_helper(&key, &payload.key, &username, &data).await; - if res.is_ok() { - break; - } else if let Err(sqlx::Error::Database(err)) = res { - if err.code() == Some(Cow::from("23505")) { - continue; - } else { - return Err(sqlx::Error::Database(err).into()); - } - }; - } - - let payload = payload.into_inner(); - let rename = RenameBuilder::default() - .name(payload.key) - .rename_to(key.clone()) - .build() - .unwrap(); - data.captcha.rename(rename).await?; - - let resp = MCaptchaDetails { - key, - name: payload.name, - }; - - Ok(HttpResponse::Ok().json(resp)) -} - -async fn update_token_helper( - key: &str, - old_key: &str, - username: &str, - data: &AppData, -) -> Result<(), sqlx::Error> { - sqlx::query!( - "UPDATE mcaptcha_config SET key = $1 - WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)", - &key, - &old_key, - &username, - ) - .execute(&data.db) - .await?; - Ok(()) -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DeleteCaptcha { - pub key: String, - pub password: String, -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.mcaptcha.delete", - wrap = "crate::CheckLogin" -)] -async fn delete_mcaptcha( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - use argon2_creds::Config; - use sqlx::Error::RowNotFound; - - let username = id.identity().unwrap(); - - struct PasswordID { - password: String, - id: i32, - } - - let rec = sqlx::query_as!( - PasswordID, - r#"SELECT ID, password FROM mcaptcha_users WHERE name = ($1)"#, - &username, - ) - .fetch_one(&data.db) - .await; - - match rec { - Ok(rec) => { - if Config::verify(&rec.password, &payload.password)? { - let payload = payload.into_inner(); - sqlx::query!( - "DELETE FROM mcaptcha_levels - WHERE config_id = ( - SELECT config_id FROM mcaptcha_config - WHERE key = $1 AND user_id = $2 - );", - &payload.key, - &rec.id, - ) - .execute(&data.db) - .await?; - - sqlx::query!( - "DELETE FROM mcaptcha_config WHERE key = ($1) AND user_id = $2;", - &payload.key, - &rec.id, - ) - .execute(&data.db) - .await?; - if let Err(err) = data.captcha.remove(RemoveCaptcha(payload.key)).await { - log::error!( - "Error while trying to remove captcha from cache {}", - err - ); - } - Ok(HttpResponse::Ok()) - } else { - Err(ServiceError::WrongPassword) - } - } - Err(RowNotFound) => Err(ServiceError::UsernameNotFound), - Err(_) => Err(ServiceError::InternalServerError), - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct StatsPayload { - pub key: String, -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.mcaptcha.stats", - wrap = "crate::CheckLogin" -)] -async fn get_stats( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - let stats = Stats::new(&username, &payload.key, &data.db).await?; - let stats = StatsUnixTimestamp::from_stats(&stats); - Ok(HttpResponse::Ok().json(&stats)) + cfg.service(update); + cfg.service(create); } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -271,10 +95,10 @@ impl TrafficPattern { } #[my_codegen::post( - path = "crate::V1_API_ROUTES.mcaptcha.create_easy", + path = "crate::V1_API_ROUTES.captcha.easy.create", wrap = "crate::CheckLogin" )] -async fn create_easy( +async fn create( payload: web::Json, data: AppData, id: Identity, @@ -283,7 +107,7 @@ async fn create_easy( let payload = payload.into_inner(); let levels = payload.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?; - let msg = AddLevels { + let msg = CreateCaptcha { levels, duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration, description: payload.description, @@ -294,7 +118,7 @@ async fn create_easy( None => None, }; - let mcaptcha_config = add_captcha_runner(&msg, &data, &username).await?; + let mcaptcha_config = create_runner(&msg, &data, &username).await?; sqlx::query!( "INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic ( config_id, @@ -327,10 +151,10 @@ pub struct UpdateTrafficPattern { } #[my_codegen::post( - path = "crate::V1_API_ROUTES.mcaptcha.update_easy", + path = "crate::V1_API_ROUTES.captcha.easy.update", wrap = "crate::CheckLogin" )] -async fn update_easy( +async fn update( payload: web::Json, data: AppData, id: Identity, @@ -341,14 +165,14 @@ async fn update_easy( .pattern .calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?; - let msg = UpdateLevels { + let msg = UpdateCaptcha { levels, duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration, description: payload.pattern.description, key: payload.key, }; - update_level_runner(&msg, &data, &username).await?; + update_captcha_runner(&msg, &data, &username).await?; sqlx::query!( "DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic @@ -403,63 +227,11 @@ mod tests { use actix_web::test; use super::*; + use crate::api::v1::mcaptcha::create::MCaptchaDetails; use crate::api::v1::ROUTES; use crate::tests::*; use crate::*; - #[actix_rt::test] - async fn update_and_get_mcaptcha_works() { - const NAME: &str = "updateusermcaptcha"; - const PASSWORD: &str = "longpassworddomain"; - const EMAIL: &str = "testupdateusermcaptcha@a.com"; - - { - let data = Data::new().await; - delete_user(NAME, &data).await; - } - - // 1. add mcaptcha token - register_and_signin(NAME, EMAIL, PASSWORD).await; - let (data, _, signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await; - let cookies = get_cookie!(signin_resp); - let app = get_app!(data).await; - - // 2. update token key - let update_token_resp = test::call_service( - &app, - post_request!(&token_key, ROUTES.mcaptcha.update_key) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(update_token_resp.status(), StatusCode::OK); - let updated_token: MCaptchaDetails = - test::read_body_json(update_token_resp).await; - - // get levels with udpated key - let get_token_resp = test::call_service( - &app, - post_request!(&updated_token, ROUTES.levels.get) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - // if updated key doesn't exist in databse, a non 200 result will bereturned - assert_eq!(get_token_resp.status(), StatusCode::OK); - - // get stats - let paylod = StatsPayload { key: token_key.key }; - let get_statis_resp = test::call_service( - &app, - post_request!(&paylod, ROUTES.mcaptcha.stats) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - // if updated key doesn't exist in databse, a non 200 result will bereturned - assert_eq!(get_statis_resp.status(), StatusCode::OK); - } - #[cfg(test)] mod isoloated_test { use super::{LevelBuilder, TrafficPattern}; @@ -566,7 +338,7 @@ mod tests { let add_token_resp = test::call_service( &app, - post_request!(&payload, ROUTES.mcaptcha.create_easy) + post_request!(&payload, ROUTES.captcha.easy.create) .cookie(cookies.clone()) .to_request(), ) @@ -576,7 +348,7 @@ mod tests { let get_level_resp = test::call_service( &app, - post_request!(&token_key, ROUTES.levels.get) + post_request!(&token_key, ROUTES.captcha.get) .cookie(cookies.clone()) .to_request(), ) @@ -606,7 +378,7 @@ mod tests { let update_token_resp = test::call_service( &app, - post_request!(&payload, ROUTES.mcaptcha.update_easy) + post_request!(&payload, ROUTES.captcha.easy.update) .cookie(cookies.clone()) .to_request(), ) @@ -615,7 +387,7 @@ mod tests { let get_level_resp = test::call_service( &app, - post_request!(&token_key, ROUTES.levels.get) + post_request!(&token_key, ROUTES.captcha.get) .cookie(cookies.clone()) .to_request(), ) diff --git a/src/api/v1/mcaptcha/get.rs b/src/api/v1/mcaptcha/get.rs new file mode 100644 index 00000000..3e92d20f --- /dev/null +++ b/src/api/v1/mcaptcha/get.rs @@ -0,0 +1,76 @@ +/* + * 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 serde::{Deserialize, Serialize}; + +use super::create::MCaptchaDetails; +use crate::errors::*; +use crate::AppData; + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.captcha.get", + wrap = "crate::CheckLogin" +)] +pub async fn get_captcha( + payload: web::Json, + data: AppData, + id: Identity, +) -> ServiceResult { + let username = id.identity().unwrap(); + let levels = runner::get_captcha(&payload.key, &username, &data).await?; + Ok(HttpResponse::Ok().json(levels)) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Levels { + levels: I32Levels, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct I32Levels { + pub difficulty_factor: i32, + pub visitor_threshold: i32, +} + +pub mod runner { + use super::*; + + // TODO get metadata from mcaptcha_config table + pub async fn get_captcha( + key: &str, + username: &str, + data: &AppData, + ) -> ServiceResult> { + let levels = sqlx::query_as!( + I32Levels, + "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE + config_id = ( + SELECT config_id FROM mcaptcha_config WHERE key = ($1) + AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2) + ) + ORDER BY difficulty_factor ASC;", + key, + &username + ) + .fetch_all(&data.db) + .await?; + + Ok(levels) + } +} diff --git a/src/api/v1/mcaptcha/levels.rs b/src/api/v1/mcaptcha/levels.rs deleted file mode 100644 index c267942d..00000000 --- a/src/api/v1/mcaptcha/levels.rs +++ /dev/null @@ -1,420 +0,0 @@ -/* - * 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 std::borrow::Cow; - -use actix_identity::Identity; -use actix_web::{web, HttpResponse, Responder}; -use futures::future::try_join_all; -use libmcaptcha::{defense::Level, master::messages::RemoveCaptcha, DefenseBuilder}; -use log::debug; -use serde::{Deserialize, Serialize}; - -use super::captcha::MCaptchaDetails; -use super::get_random; -use crate::errors::*; -use crate::AppData; - -pub mod routes { - - pub struct Levels { - pub add: &'static str, - pub get: &'static str, - pub update: &'static str, - } - - impl Levels { - pub const fn new() -> Levels { - let add = "/api/v1/mcaptcha/add"; - let update = "/api/v1/mcaptcha/update"; - let get = "/api/v1/mcaptcha/get"; - Levels { add, get, update } - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct AddLevels { - pub levels: Vec, - pub duration: u32, - pub description: String, -} - -pub fn services(cfg: &mut web::ServiceConfig) { - cfg.service(add_levels); - cfg.service(update_levels); - cfg.service(get_levels); -} - -// TODO redo mcaptcha table to include levels as json field -// so that the whole thing can be added/udpaed in a single stroke -#[my_codegen::post(path = "crate::V1_API_ROUTES.levels.add", wrap = "crate::CheckLogin")] -async fn add_levels( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - let mcaptcha_config = add_captcha_runner(&payload, &data, &username).await?; - Ok(HttpResponse::Ok().json(mcaptcha_config)) -} - -pub async fn add_captcha_runner( - payload: &AddLevels, - data: &AppData, - username: &str, -) -> ServiceResult { - let mut defense = DefenseBuilder::default(); - for level in payload.levels.iter() { - defense.add_level(*level)?; - } - - defense.build()?; - - debug!("creating config"); - let mcaptcha_config = - // add_mcaptcha_util(payload.duration, &payload.description, &data, username).await?; - - { - let mut key; - - let resp; - - loop { - key = get_random(32); - - let res = sqlx::query!( - "INSERT INTO mcaptcha_config - (key, user_id, duration, name) - VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)", - &key, - &username, - payload.duration as i32, - &payload.description, - ) - .execute(&data.db) - .await; - - match res { - Err(sqlx::Error::Database(err)) => { - if err.code() == Some(Cow::from("23505")) - && err.message().contains("mcaptcha_config_key_key") - { - continue; - } else { - return Err(sqlx::Error::Database(err).into()); - } - } - Err(e) => return Err(e.into()), - - Ok(_) => { - resp = MCaptchaDetails { - key, - name: payload.description.to_owned(), - }; - break; - } - } - } - resp - }; - - debug!("config created"); - - let mut futs = Vec::with_capacity(payload.levels.len()); - - for level in payload.levels.iter() { - let difficulty_factor = level.difficulty_factor as i32; - let visitor_threshold = level.visitor_threshold as i32; - let fut = sqlx::query!( - "INSERT INTO mcaptcha_levels ( - difficulty_factor, - visitor_threshold, - config_id) VALUES ( - $1, $2, ( - SELECT config_id FROM mcaptcha_config WHERE - key = ($3) AND user_id = ( - SELECT ID FROM mcaptcha_users WHERE name = $4 - )));", - difficulty_factor, - visitor_threshold, - &mcaptcha_config.key, - &username, - ) - .execute(&data.db); - futs.push(fut); - } - - try_join_all(futs).await?; - Ok(mcaptcha_config) -} - -#[derive(Serialize, Deserialize)] -pub struct UpdateLevels { - pub levels: Vec, - pub duration: u32, - pub description: String, - pub key: String, -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.levels.update", - wrap = "crate::CheckLogin" -)] -async fn update_levels( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - update_level_runner(&payload, &data, &username).await?; - Ok(HttpResponse::Ok()) -} - -pub async fn update_level_runner( - payload: &UpdateLevels, - data: &AppData, - username: &str, -) -> ServiceResult<()> { - let mut defense = DefenseBuilder::default(); - - for level in payload.levels.iter() { - defense.add_level(*level)?; - } - - // I feel this is necessary as both difficulty factor _and_ visitor threshold of a - // level could change so doing this would not require us to send level_id to client - // still, needs to be benchmarked - defense.build()?; - - let mut futs = Vec::with_capacity(payload.levels.len() + 2); - sqlx::query!( - "DELETE FROM mcaptcha_levels - WHERE config_id = ( - SELECT config_id FROM mcaptcha_config where key = ($1) - AND user_id = ( - SELECT ID from mcaptcha_users WHERE name = $2 - ) - )", - &payload.key, - &username - ) - .execute(&data.db) - .await?; - - let update_fut = sqlx::query!( - "UPDATE mcaptcha_config SET name = $1, duration = $2 - WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3) - AND key = $4", - &payload.description, - payload.duration as i32, - &username, - &payload.key, - ) - .execute(&data.db); //.await?; - - futs.push(update_fut); - - for level in payload.levels.iter() { - let difficulty_factor = level.difficulty_factor as i32; - let visitor_threshold = level.visitor_threshold as i32; - let fut = sqlx::query!( - "INSERT INTO mcaptcha_levels ( - difficulty_factor, - visitor_threshold, - config_id) VALUES ( - $1, $2, ( - SELECT config_id FROM mcaptcha_config WHERE key = ($3) AND - user_id = ( - SELECT ID from mcaptcha_users WHERE name = $4 - ) - ));", - difficulty_factor, - visitor_threshold, - &payload.key, - &username, - ) - .execute(&data.db); //.await?; - futs.push(fut); - } - - try_join_all(futs).await?; - if let Err(ServiceError::CaptchaError(e)) = data - .captcha - .remove(RemoveCaptcha(payload.key.clone())) - .await - { - log::error!( - "Deleting captcha key {} while updating it, error: {:?}", - &payload.key, - e - ); - } - Ok(()) -} - -#[my_codegen::post(path = "crate::V1_API_ROUTES.levels.get", wrap = "crate::CheckLogin")] -async fn get_levels( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - - let levels = get_levels_util(&payload.key, &username, &data).await?; - - Ok(HttpResponse::Ok().json(levels)) -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Levels { - levels: I32Levels, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct I32Levels { - pub difficulty_factor: i32, - pub visitor_threshold: i32, -} - -async fn get_levels_util( - key: &str, - username: &str, - data: &AppData, -) -> ServiceResult> { - let levels = sqlx::query_as!( - I32Levels, - "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE - config_id = ( - SELECT config_id FROM mcaptcha_config WHERE key = ($1) - AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2) - ) - ORDER BY difficulty_factor ASC;", - key, - &username - ) - .fetch_all(&data.db) - .await?; - - Ok(levels) -} - -#[cfg(test)] -mod tests { - use actix_web::http::StatusCode; - use actix_web::test; - - use super::*; - use crate::api::v1::mcaptcha::captcha::DeleteCaptcha; - use crate::api::v1::ROUTES; - use crate::data::Data; - use crate::tests::*; - use crate::*; - - const L1: Level = Level { - difficulty_factor: 100, - visitor_threshold: 10, - }; - const L2: Level = Level { - difficulty_factor: 1000, - visitor_threshold: 1000, - }; - - #[actix_rt::test] - async fn level_routes_work() { - const NAME: &str = "testuserlevelroutes"; - const PASSWORD: &str = "longpassworddomain"; - const EMAIL: &str = "testuserlevelrouts@a.com"; - - { - let data = Data::new().await; - delete_user(NAME, &data).await; - } - - register_and_signin(NAME, EMAIL, PASSWORD).await; - let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await; - let cookies = get_cookie!(signin_resp); - let app = get_app!(data).await; - - // 2. get level - let add_level = get_level_data(); - let get_level_resp = test::call_service( - &app, - post_request!(&key, ROUTES.levels.get) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(get_level_resp.status(), StatusCode::OK); - let res_levels: Vec = test::read_body_json(get_level_resp).await; - assert_eq!(res_levels, add_level.levels); - - // 3. update level - let levels = vec![L1, L2]; - let update_level = UpdateLevels { - key: key.key.clone(), - levels: levels.clone(), - description: add_level.description, - duration: add_level.duration, - }; - - let add_token_resp = test::call_service( - &app, - post_request!(&update_level, ROUTES.levels.update) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(add_token_resp.status(), StatusCode::OK); - - let get_level_resp = test::call_service( - &app, - post_request!(&key, ROUTES.levels.get) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(get_level_resp.status(), StatusCode::OK); - let res_levels: Vec = test::read_body_json(get_level_resp).await; - assert_eq!(res_levels, levels); - - // 4. delete captcha - let mut delete_payload = DeleteCaptcha { - key: key.key, - password: format!("worongpass{}", PASSWORD), - }; - - bad_post_req_test( - NAME, - PASSWORD, - ROUTES.mcaptcha.delete, - &delete_payload, - ServiceError::WrongPassword, - ) - .await; - - delete_payload.password = PASSWORD.into(); - - let del_resp = test::call_service( - &app, - post_request!(&delete_payload, ROUTES.mcaptcha.delete) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(del_resp.status(), StatusCode::OK); - } -} diff --git a/src/api/v1/mcaptcha/mod.rs b/src/api/v1/mcaptcha/mod.rs index 6c649565..05c59177 100644 --- a/src/api/v1/mcaptcha/mod.rs +++ b/src/api/v1/mcaptcha/mod.rs @@ -15,9 +15,14 @@ * along with this program. If not, see . */ -pub mod captcha; -pub mod duration; -pub mod levels; +pub mod create; +pub mod delete; +pub mod easy; +pub mod get; +pub mod stats; +#[cfg(test)] +pub mod test; +pub mod update; pub fn get_random(len: usize) -> String { use std::iter; @@ -34,7 +39,40 @@ pub fn get_random(len: usize) -> String { } pub fn services(cfg: &mut actix_web::web::ServiceConfig) { - duration::services(cfg); - levels::services(cfg); - captcha::services(cfg); + easy::services(cfg); + cfg.service(stats::get); + cfg.service(create::create); + cfg.service(get::get_captcha); + cfg.service(update::update_key); + cfg.service(update::update_captcha); + cfg.service(delete::delete); +} + +pub mod routes { + use super::easy::routes::Easy; + use super::stats::routes::Stats; + + pub struct Captcha { + pub create: &'static str, + pub update: &'static str, + pub get: &'static str, + pub delete: &'static str, + pub update_key: &'static str, + pub easy: Easy, + pub stats: Stats, + } + + impl Captcha { + pub const fn new() -> Self { + Self { + create: "/api/v1/mcaptcha/create", + update: "/api/v1/mcaptcha/update", + get: "/api/v1/mcaptcha/get", + update_key: "/api/v1/mcaptcha/update/key", + delete: "/api/v1/mcaptcha/delete", + easy: Easy::new(), + stats: Stats::new(), + } + } + } } diff --git a/src/api/v1/mcaptcha/stats.rs b/src/api/v1/mcaptcha/stats.rs new file mode 100644 index 00000000..7deca075 --- /dev/null +++ b/src/api/v1/mcaptcha/stats.rs @@ -0,0 +1,56 @@ +/* +* 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 serde::{Deserialize, Serialize}; + +use crate::errors::*; +use crate::stats::fetch::{Stats, StatsUnixTimestamp}; +use crate::AppData; + +pub mod routes { + pub struct Stats { + pub get: &'static str, + } + + impl Stats { + pub const fn new() -> Self { + Self { + get: "/api/v1/mcaptcha/stats", + } + } + } +} +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct StatsPayload { + pub key: String, +} + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.captcha.stats.get", + wrap = "crate::CheckLogin" +)] +pub async fn get( + payload: web::Json, + data: AppData, + id: Identity, +) -> ServiceResult { + let username = id.identity().unwrap(); + let stats = Stats::new(&username, &payload.key, &data.db).await?; + let stats = StatsUnixTimestamp::from_stats(&stats); + Ok(HttpResponse::Ok().json(&stats)) +} diff --git a/src/api/v1/mcaptcha/test.rs b/src/api/v1/mcaptcha/test.rs new file mode 100644 index 00000000..7e2d5561 --- /dev/null +++ b/src/api/v1/mcaptcha/test.rs @@ -0,0 +1,124 @@ +/* + * 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_web::http::StatusCode; +use actix_web::test; + +use crate::api::v1::mcaptcha::delete::DeleteCaptcha; +use libmcaptcha::defense::Level; + +use crate::api::v1::mcaptcha::update::UpdateCaptcha; +use crate::api::v1::ROUTES; +use crate::data::Data; +use crate::errors::*; +use crate::tests::*; +use crate::*; + +const L1: Level = Level { + difficulty_factor: 100, + visitor_threshold: 10, +}; +const L2: Level = Level { + difficulty_factor: 1000, + visitor_threshold: 1000, +}; + +#[actix_rt::test] +async fn level_routes_work() { + const NAME: &str = "testuserlevelroutes"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testuserlevelrouts@a.com"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + register_and_signin(NAME, EMAIL, PASSWORD).await; + // create captcha + let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(data).await; + + // 2. get captcha + let add_level = get_level_data(); + let get_level_resp = test::call_service( + &app, + post_request!(&key, ROUTES.captcha.get) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: Vec = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels, add_level.levels); + + // 3. update captcha + let levels = vec![L1, L2]; + let update_level = UpdateCaptcha { + key: key.key.clone(), + levels: levels.clone(), + description: add_level.description, + duration: add_level.duration, + }; + + let add_token_resp = test::call_service( + &app, + post_request!(&update_level, ROUTES.captcha.update) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_token_resp.status(), StatusCode::OK); + + let get_level_resp = test::call_service( + &app, + post_request!(&key, ROUTES.captcha.get) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: Vec = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels, levels); + + // 4. delete captcha + let mut delete_payload = DeleteCaptcha { + key: key.key, + password: format!("worongpass{}", PASSWORD), + }; + + bad_post_req_test( + NAME, + PASSWORD, + ROUTES.captcha.delete, + &delete_payload, + ServiceError::WrongPassword, + ) + .await; + + delete_payload.password = PASSWORD.into(); + + let del_resp = test::call_service( + &app, + post_request!(&delete_payload, ROUTES.captcha.delete) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(del_resp.status(), StatusCode::OK); +} diff --git a/src/api/v1/mcaptcha/update.rs b/src/api/v1/mcaptcha/update.rs new file mode 100644 index 00000000..e21356e9 --- /dev/null +++ b/src/api/v1/mcaptcha/update.rs @@ -0,0 +1,264 @@ +/* + * 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 std::borrow::Cow; + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use libmcaptcha::defense::Level; +use libmcaptcha::master::messages::RenameBuilder; +use serde::{Deserialize, Serialize}; + +use super::create::MCaptchaDetails; +use super::get_random; +use crate::errors::*; +use crate::AppData; + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.captcha.update_key", + wrap = "crate::CheckLogin" +)] +pub async fn update_key( + payload: web::Json, + data: AppData, + id: Identity, +) -> ServiceResult { + let username = id.identity().unwrap(); + let mut key; + + loop { + key = get_random(32); + let res = runner::update_key(&key, &payload.key, &username, &data).await; + if res.is_ok() { + break; + } else if let Err(sqlx::Error::Database(err)) = res { + if err.code() == Some(Cow::from("23505")) { + continue; + } else { + return Err(sqlx::Error::Database(err).into()); + } + }; + } + + let payload = payload.into_inner(); + let rename = RenameBuilder::default() + .name(payload.key) + .rename_to(key.clone()) + .build() + .unwrap(); + data.captcha.rename(rename).await?; + + let resp = MCaptchaDetails { + key, + name: payload.name, + }; + + Ok(HttpResponse::Ok().json(resp)) +} + +#[derive(Serialize, Deserialize)] +pub struct UpdateCaptcha { + pub levels: Vec, + pub duration: u32, + pub description: String, + pub key: String, +} + +#[my_codegen::post( + path = "crate::V1_API_ROUTES.captcha.update", + wrap = "crate::CheckLogin" +)] +pub async fn update_captcha( + payload: web::Json, + data: AppData, + id: Identity, +) -> ServiceResult { + let username = id.identity().unwrap(); + runner::update_captcha(&payload, &data, &username).await?; + Ok(HttpResponse::Ok()) +} + +pub mod runner { + use futures::future::try_join_all; + use libmcaptcha::{master::messages::RemoveCaptcha, DefenseBuilder}; + + use super::*; + + pub async fn update_key( + key: &str, + old_key: &str, + username: &str, + data: &AppData, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE mcaptcha_config SET key = $1 + WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)", + &key, + &old_key, + &username, + ) + .execute(&data.db) + .await?; + Ok(()) + } + + pub async fn update_captcha( + payload: &UpdateCaptcha, + data: &AppData, + username: &str, + ) -> ServiceResult<()> { + let mut defense = DefenseBuilder::default(); + + for level in payload.levels.iter() { + defense.add_level(*level)?; + } + + // I feel this is necessary as both difficulty factor _and_ visitor threshold of a + // level could change so doing this would not require us to send level_id to client + // still, needs to be benchmarked + defense.build()?; + + let mut futs = Vec::with_capacity(payload.levels.len() + 2); + sqlx::query!( + "DELETE FROM mcaptcha_levels + WHERE config_id = ( + SELECT config_id FROM mcaptcha_config where key = ($1) + AND user_id = ( + SELECT ID from mcaptcha_users WHERE name = $2 + ) + )", + &payload.key, + &username + ) + .execute(&data.db) + .await?; + + let update_fut = sqlx::query!( + "UPDATE mcaptcha_config SET name = $1, duration = $2 + WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3) + AND key = $4", + &payload.description, + payload.duration as i32, + &username, + &payload.key, + ) + .execute(&data.db); //.await?; + + futs.push(update_fut); + + for level in payload.levels.iter() { + let difficulty_factor = level.difficulty_factor as i32; + let visitor_threshold = level.visitor_threshold as i32; + let fut = sqlx::query!( + "INSERT INTO mcaptcha_levels ( + difficulty_factor, + visitor_threshold, + config_id) VALUES ( + $1, $2, ( + SELECT config_id FROM mcaptcha_config WHERE key = ($3) AND + user_id = ( + SELECT ID from mcaptcha_users WHERE name = $4 + ) + ));", + difficulty_factor, + visitor_threshold, + &payload.key, + &username, + ) + .execute(&data.db); //.await?; + futs.push(fut); + } + + try_join_all(futs).await?; + if let Err(ServiceError::CaptchaError(e)) = data + .captcha + .remove(RemoveCaptcha(payload.key.clone())) + .await + { + log::error!( + "Deleting captcha key {} while updating it, error: {:?}", + &payload.key, + e + ); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::test; + + use crate::api::v1::mcaptcha::create::MCaptchaDetails; + use crate::api::v1::mcaptcha::stats::StatsPayload; + use crate::api::v1::ROUTES; + use crate::tests::*; + use crate::*; + + #[actix_rt::test] + async fn update_and_get_mcaptcha_works() { + const NAME: &str = "updateusermcaptcha"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testupdateusermcaptcha@a.com"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + // 1. add mcaptcha token + register_and_signin(NAME, EMAIL, PASSWORD).await; + let (data, _, signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(data).await; + + // 2. update token key + let update_token_resp = test::call_service( + &app, + post_request!(&token_key, ROUTES.captcha.update_key) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(update_token_resp.status(), StatusCode::OK); + let updated_token: MCaptchaDetails = + test::read_body_json(update_token_resp).await; + + // get levels with udpated key + let get_token_resp = test::call_service( + &app, + post_request!(&updated_token, ROUTES.captcha.get) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + // if updated key doesn't exist in databse, a non 200 result will bereturned + assert_eq!(get_token_resp.status(), StatusCode::OK); + + // get stats + let paylod = StatsPayload { key: token_key.key }; + let get_statis_resp = test::call_service( + &app, + post_request!(&paylod, ROUTES.captcha.stats.get) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + // if updated key doesn't exist in databse, a non 200 result will bereturned + assert_eq!(get_statis_resp.status(), StatusCode::OK); + } +} diff --git a/src/api/v1/pow/get_config.rs b/src/api/v1/pow/get_config.rs index 338afff5..c8e7649c 100644 --- a/src/api/v1/pow/get_config.rs +++ b/src/api/v1/pow/get_config.rs @@ -23,7 +23,6 @@ use libmcaptcha::{ }; use serde::{Deserialize, Serialize}; -use super::GetDurationResp; use super::I32Levels; use crate::errors::*; use crate::stats::record::record_fetch; @@ -96,9 +95,13 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> { &key, ) .fetch_all(&data.db); + + struct DurationResp { + duration: i32, + } // get duration let duration_fut = sqlx::query_as!( - GetDurationResp, + DurationResp, "SELECT duration FROM mcaptcha_config WHERE key = $1", &key, diff --git a/src/api/v1/pow/mod.rs b/src/api/v1/pow/mod.rs index f14ad509..814f16b3 100644 --- a/src/api/v1/pow/mod.rs +++ b/src/api/v1/pow/mod.rs @@ -21,8 +21,7 @@ pub mod get_config; pub mod verify_pow; pub mod verify_token; -pub use super::mcaptcha::duration::GetDurationResp; -pub use super::mcaptcha::levels::I32Levels; +pub use super::mcaptcha::get::I32Levels; pub fn services(cfg: &mut web::ServiceConfig) { let cors = actix_cors::Cors::default() diff --git a/src/api/v1/routes.rs b/src/api/v1/routes.rs index 797affc7..008ae15f 100644 --- a/src/api/v1/routes.rs +++ b/src/api/v1/routes.rs @@ -17,9 +17,7 @@ use super::account::routes::Account; use super::auth::routes::Auth; -use super::mcaptcha::captcha::routes::MCaptcha; -use super::mcaptcha::duration::routes::Duration; -use super::mcaptcha::levels::routes::Levels; +use super::mcaptcha::routes::Captcha; use super::meta::routes::Meta; use super::notifications::routes::Notifications; use super::pow::routes::PoW; @@ -29,9 +27,7 @@ pub const ROUTES: Routes = Routes::new(); pub struct Routes { pub auth: Auth, pub account: Account, - pub levels: Levels, - pub mcaptcha: MCaptcha, - pub duration: Duration, + pub captcha: Captcha, pub meta: Meta, pub pow: PoW, pub notifications: Notifications, @@ -42,9 +38,7 @@ impl Routes { Routes { auth: Auth::new(), account: Account::new(), - levels: Levels::new(), - mcaptcha: MCaptcha::new(), - duration: Duration::new(), + captcha: Captcha::new(), meta: Meta::new(), pow: PoW::new(), notifications: Notifications::new(), diff --git a/src/demo.rs b/src/demo.rs index 06a7c682..e1965276 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -120,7 +120,7 @@ mod tests { let resp = test::call_service( &app, - post_request!(&token_key, crate::V1_API_ROUTES.levels.get) + post_request!(&token_key, crate::V1_API_ROUTES.captcha.get) .cookie(cookies.clone()) .to_request(), ) @@ -133,7 +133,7 @@ mod tests { let resp = test::call_service( &app, - post_request!(&token_key, crate::V1_API_ROUTES.levels.get) + post_request!(&token_key, crate::V1_API_ROUTES.captcha.create) .cookie(cookies) .to_request(), ) diff --git a/src/pages/panel/sitekey/delete.rs b/src/pages/panel/sitekey/delete.rs index 7f7fa7c4..65165a2d 100644 --- a/src/pages/panel/sitekey/delete.rs +++ b/src/pages/panel/sitekey/delete.rs @@ -26,7 +26,7 @@ use crate::{PAGES, V1_API_ROUTES}; pub async fn delete_sitekey(path: web::Path) -> impl Responder { let key = path.into_inner(); let data = vec![("sitekey", key)]; - let page = SudoPage::new(V1_API_ROUTES.mcaptcha.delete, Some(data)) + let page = SudoPage::new(V1_API_ROUTES.captcha.delete, Some(data)) .render_once() .unwrap(); HttpResponse::Ok() diff --git a/src/pages/panel/sitekey/list.rs b/src/pages/panel/sitekey/list.rs index 29f6dbbe..2ac11a70 100644 --- a/src/pages/panel/sitekey/list.rs +++ b/src/pages/panel/sitekey/list.rs @@ -19,7 +19,7 @@ use actix_identity::Identity; use actix_web::{HttpResponse, Responder}; use sailfish::TemplateOnce; -use crate::api::v1::mcaptcha::captcha::MCaptchaDetails; +use crate::api::v1::mcaptcha::create::MCaptchaDetails; use crate::errors::*; use crate::AppData; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 80dc10b1..2e112e57 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -10,8 +10,8 @@ use serde::Serialize; use super::*; use crate::api::v1::auth::runners::{Login, Register}; -use crate::api::v1::mcaptcha::captcha::MCaptchaDetails; -use crate::api::v1::mcaptcha::levels::AddLevels; +use crate::api::v1::mcaptcha::create::CreateCaptcha; +use crate::api::v1::mcaptcha::create::MCaptchaDetails; use crate::api::v1::ROUTES; use crate::data::Data; use crate::errors::*; @@ -159,10 +159,10 @@ pub const L2: Level = Level { visitor_threshold: 500, }; -pub fn get_level_data() -> AddLevels { +pub fn get_level_data() -> CreateCaptcha { let levels = vec![L1, L2]; - AddLevels { + CreateCaptcha { levels, duration: 30, description: "dummy".into(), @@ -182,7 +182,7 @@ pub async fn add_levels_util( // 1. add level let add_token_resp = test::call_service( &app, - post_request!(&add_level, ROUTES.levels.add) + post_request!(&add_level, ROUTES.captcha.create) .cookie(cookies.clone()) .to_request(), ) diff --git a/templates/panel/settings/index.html b/templates/panel/settings/index.html index 0db278ae..e93b1f6d 100644 --- a/templates/panel/settings/index.html +++ b/templates/panel/settings/index.html @@ -13,7 +13,7 @@ <. include!("../help-banner/index.html"); .>
-
+

<.= PAGE .>

diff --git a/templates/panel/sitekey/add/advance/form.html b/templates/panel/sitekey/add/advance/form.html index 899afeb9..5ddf8d75 100644 --- a/templates/panel/sitekey/add/advance/form.html +++ b/templates/panel/sitekey/add/advance/form.html @@ -1,4 +1,4 @@ -
+

<.= form_title .>

diff --git a/templates/panel/sitekey/edit/index.html b/templates/panel/sitekey/edit/index.html index 14d709a9..0a000915 100644 --- a/templates/panel/sitekey/edit/index.html +++ b/templates/panel/sitekey/edit/index.html @@ -1,4 +1,4 @@ -<. const URL: &str = crate::V1_API_ROUTES.levels.update; .> +<. const URL: &str = crate::V1_API_ROUTES.captcha.update; .> <. const READONLY: bool = false; .> <. include!("../view/__form-top.html"); .> <. for (count, level) in levels.iter().enumerate() { .> diff --git a/templates/panel/sitekey/view/index.html b/templates/panel/sitekey/view/index.html index 8bac99db..bf0b71aa 100644 --- a/templates/panel/sitekey/view/index.html +++ b/templates/panel/sitekey/view/index.html @@ -1,4 +1,4 @@ -<. const URL: &str = crate::V1_API_ROUTES.levels.add; .> +<. const URL: &str = crate::V1_API_ROUTES.captcha.create; .> <. const READONLY: bool = true; .> <. include!("./__form-top.html"); .> <. for (count, level) in levels.iter().enumerate() { .>