refactor captcha.rs and levels.rs and rm duration routes

This commit is contained in:
realaravinth 2021-12-16 20:46:50 +05:30
parent cf4a0f9b73
commit 73ce2d1cb1
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
22 changed files with 860 additions and 886 deletions

View file

@ -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<Password>,
data: AppData,

View file

@ -0,0 +1,153 @@
/*
* 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 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<Level>,
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<CreateCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<MCaptchaDetails> {
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)
}
}

View file

@ -0,0 +1,95 @@
/*
* 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::{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<DeleteCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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),
}
}

View file

@ -1,180 +0,0 @@
/*
* 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::{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<UpdateDuration>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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);
}
}

View file

@ -14,213 +14,37 @@
* 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 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<String>,
}
#[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<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<DeleteCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<StatsPayload>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<TrafficPattern>,
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<UpdateTrafficPattern>,
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(),
)

View file

@ -0,0 +1,76 @@
/*
* 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::{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<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<Vec<I32Levels>> {
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)
}
}

View file

@ -1,420 +0,0 @@
/*
* 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 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<Level>,
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<AddLevels>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<MCaptchaDetails> {
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<Level>,
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<UpdateLevels>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<Vec<I32Levels>> {
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<Level> = 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<Level> = 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);
}
}

View file

@ -15,9 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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(),
}
}
}
}

View file

@ -0,0 +1,56 @@
/*
* 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::{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<StatsPayload>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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))
}

124
src/api/v1/mcaptcha/test.rs Normal file
View file

@ -0,0 +1,124 @@
/*
* 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_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<Level> = 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<Level> = 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);
}

View file

@ -0,0 +1,264 @@
/*
* 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 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<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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<Level>,
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<UpdateCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
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);
}
}

View file

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

View file

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

View file

@ -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(),

View file

@ -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(),
)

View file

@ -26,7 +26,7 @@ use crate::{PAGES, V1_API_ROUTES};
pub async fn delete_sitekey(path: web::Path<String>) -> 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()

View file

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

View file

@ -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(),
)

View file

@ -13,7 +13,7 @@
<. include!("../help-banner/index.html"); .>
<!-- Main content container -->
<div class="inner-container">
<div class="sitekey-form" action="<.= crate::V1_API_ROUTES.levels.add .>" method="post">
<div class="sitekey-form" action="<.= crate::V1_API_ROUTES.captcha.create .>" method="post">
<h1 class="form__title">
<.= PAGE .>
</h1>

View file

@ -1,4 +1,4 @@
<form class="sitekey-form" action="<.= crate::V1_API_ROUTES.levels.add .>" method="post">
<form class="sitekey-form" action="<.= crate::V1_API_ROUTES.captcha.create .>" method="post">
<h1 class="form__title">
<.= form_title .>
</h1>

View file

@ -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() { .>

View file

@ -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() { .>