mirror of
https://github.com/mCaptcha/mCaptcha.git
synced 2025-11-24 06:25:46 +00:00
refactor captcha.rs and levels.rs and rm duration routes
This commit is contained in:
parent
cf4a0f9b73
commit
73ce2d1cb1
@ -26,7 +26,7 @@ use crate::AppData;
|
|||||||
path = "crate::V1_API_ROUTES.account.delete",
|
path = "crate::V1_API_ROUTES.account.delete",
|
||||||
wrap = "crate::CheckLogin"
|
wrap = "crate::CheckLogin"
|
||||||
)]
|
)]
|
||||||
async fn delete_account(
|
pub async fn delete_account(
|
||||||
id: Identity,
|
id: Identity,
|
||||||
payload: web::Json<Password>,
|
payload: web::Json<Password>,
|
||||||
data: AppData,
|
data: AppData,
|
||||||
|
|||||||
153
src/api/v1/mcaptcha/create.rs
Normal file
153
src/api/v1/mcaptcha/create.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/api/v1/mcaptcha/delete.rs
Normal file
95
src/api/v1/mcaptcha/delete.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,213 +14,37 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{web, HttpResponse, Responder};
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
use libmcaptcha::master::messages::{RemoveCaptcha, RenameBuilder};
|
|
||||||
use libmcaptcha::{defense::Level, defense::LevelBuilder};
|
use libmcaptcha::{defense::Level, defense::LevelBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::get_random;
|
use super::create::{runner::create as create_runner, CreateCaptcha};
|
||||||
use super::levels::{add_captcha_runner, update_level_runner, AddLevels, UpdateLevels};
|
use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::settings::DefaultDifficultyStrategy;
|
use crate::settings::DefaultDifficultyStrategy;
|
||||||
use crate::stats::fetch::{Stats, StatsUnixTimestamp};
|
|
||||||
use crate::AppData;
|
use crate::AppData;
|
||||||
|
|
||||||
pub mod routes {
|
pub mod routes {
|
||||||
pub struct MCaptcha {
|
pub struct Easy {
|
||||||
pub delete: &'static str,
|
|
||||||
pub update_key: &'static str,
|
|
||||||
pub stats: &'static str,
|
|
||||||
/// easy is using defaults
|
/// easy is using defaults
|
||||||
pub create_easy: &'static str,
|
pub create: &'static str,
|
||||||
pub update_easy: &'static str,
|
pub update: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MCaptcha {
|
impl Easy {
|
||||||
pub const fn new() -> MCaptcha {
|
pub const fn new() -> Self {
|
||||||
MCaptcha {
|
Self {
|
||||||
update_key: "/api/v1/mcaptcha/update/key",
|
create: "/api/v1/mcaptcha/add/easy",
|
||||||
delete: "/api/v1/mcaptcha/delete",
|
update: "/api/v1/mcaptcha/update/easy",
|
||||||
stats: "/api/v1/mcaptcha/stats",
|
|
||||||
create_easy: "/api/v1/mcaptcha/add/easy",
|
|
||||||
update_easy: "/api/v1/mcaptcha/update/easy",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(update_token);
|
cfg.service(update);
|
||||||
cfg.service(delete_mcaptcha);
|
cfg.service(create);
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@ -271,10 +95,10 @@ impl TrafficPattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[my_codegen::post(
|
#[my_codegen::post(
|
||||||
path = "crate::V1_API_ROUTES.mcaptcha.create_easy",
|
path = "crate::V1_API_ROUTES.captcha.easy.create",
|
||||||
wrap = "crate::CheckLogin"
|
wrap = "crate::CheckLogin"
|
||||||
)]
|
)]
|
||||||
async fn create_easy(
|
async fn create(
|
||||||
payload: web::Json<TrafficPattern>,
|
payload: web::Json<TrafficPattern>,
|
||||||
data: AppData,
|
data: AppData,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
@ -283,7 +107,7 @@ async fn create_easy(
|
|||||||
let payload = payload.into_inner();
|
let payload = payload.into_inner();
|
||||||
let levels =
|
let levels =
|
||||||
payload.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
|
payload.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
|
||||||
let msg = AddLevels {
|
let msg = CreateCaptcha {
|
||||||
levels,
|
levels,
|
||||||
duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
|
duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
@ -294,7 +118,7 @@ async fn create_easy(
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mcaptcha_config = add_captcha_runner(&msg, &data, &username).await?;
|
let mcaptcha_config = create_runner(&msg, &data, &username).await?;
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
|
"INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
|
||||||
config_id,
|
config_id,
|
||||||
@ -327,10 +151,10 @@ pub struct UpdateTrafficPattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[my_codegen::post(
|
#[my_codegen::post(
|
||||||
path = "crate::V1_API_ROUTES.mcaptcha.update_easy",
|
path = "crate::V1_API_ROUTES.captcha.easy.update",
|
||||||
wrap = "crate::CheckLogin"
|
wrap = "crate::CheckLogin"
|
||||||
)]
|
)]
|
||||||
async fn update_easy(
|
async fn update(
|
||||||
payload: web::Json<UpdateTrafficPattern>,
|
payload: web::Json<UpdateTrafficPattern>,
|
||||||
data: AppData,
|
data: AppData,
|
||||||
id: Identity,
|
id: Identity,
|
||||||
@ -341,14 +165,14 @@ async fn update_easy(
|
|||||||
.pattern
|
.pattern
|
||||||
.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
|
.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
|
||||||
|
|
||||||
let msg = UpdateLevels {
|
let msg = UpdateCaptcha {
|
||||||
levels,
|
levels,
|
||||||
duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
|
duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
|
||||||
description: payload.pattern.description,
|
description: payload.pattern.description,
|
||||||
key: payload.key,
|
key: payload.key,
|
||||||
};
|
};
|
||||||
|
|
||||||
update_level_runner(&msg, &data, &username).await?;
|
update_captcha_runner(&msg, &data, &username).await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic
|
"DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic
|
||||||
@ -403,63 +227,11 @@ mod tests {
|
|||||||
use actix_web::test;
|
use actix_web::test;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
|
||||||
use crate::api::v1::ROUTES;
|
use crate::api::v1::ROUTES;
|
||||||
use crate::tests::*;
|
use crate::tests::*;
|
||||||
use crate::*;
|
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)]
|
#[cfg(test)]
|
||||||
mod isoloated_test {
|
mod isoloated_test {
|
||||||
use super::{LevelBuilder, TrafficPattern};
|
use super::{LevelBuilder, TrafficPattern};
|
||||||
@ -566,7 +338,7 @@ mod tests {
|
|||||||
|
|
||||||
let add_token_resp = test::call_service(
|
let add_token_resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&payload, ROUTES.mcaptcha.create_easy)
|
post_request!(&payload, ROUTES.captcha.easy.create)
|
||||||
.cookie(cookies.clone())
|
.cookie(cookies.clone())
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
@ -576,7 +348,7 @@ mod tests {
|
|||||||
|
|
||||||
let get_level_resp = test::call_service(
|
let get_level_resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&token_key, ROUTES.levels.get)
|
post_request!(&token_key, ROUTES.captcha.get)
|
||||||
.cookie(cookies.clone())
|
.cookie(cookies.clone())
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
@ -606,7 +378,7 @@ mod tests {
|
|||||||
|
|
||||||
let update_token_resp = test::call_service(
|
let update_token_resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&payload, ROUTES.mcaptcha.update_easy)
|
post_request!(&payload, ROUTES.captcha.easy.update)
|
||||||
.cookie(cookies.clone())
|
.cookie(cookies.clone())
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
@ -615,7 +387,7 @@ mod tests {
|
|||||||
|
|
||||||
let get_level_resp = test::call_service(
|
let get_level_resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&token_key, ROUTES.levels.get)
|
post_request!(&token_key, ROUTES.captcha.get)
|
||||||
.cookie(cookies.clone())
|
.cookie(cookies.clone())
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
76
src/api/v1/mcaptcha/get.rs
Normal file
76
src/api/v1/mcaptcha/get.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,9 +15,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub mod captcha;
|
pub mod create;
|
||||||
pub mod duration;
|
pub mod delete;
|
||||||
pub mod levels;
|
pub mod easy;
|
||||||
|
pub mod get;
|
||||||
|
pub mod stats;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test;
|
||||||
|
pub mod update;
|
||||||
|
|
||||||
pub fn get_random(len: usize) -> String {
|
pub fn get_random(len: usize) -> String {
|
||||||
use std::iter;
|
use std::iter;
|
||||||
@ -34,7 +39,40 @@ pub fn get_random(len: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
duration::services(cfg);
|
easy::services(cfg);
|
||||||
levels::services(cfg);
|
cfg.service(stats::get);
|
||||||
captcha::services(cfg);
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/api/v1/mcaptcha/stats.rs
Normal file
56
src/api/v1/mcaptcha/stats.rs
Normal 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
124
src/api/v1/mcaptcha/test.rs
Normal 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);
|
||||||
|
}
|
||||||
264
src/api/v1/mcaptcha/update.rs
Normal file
264
src/api/v1/mcaptcha/update.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,7 +23,6 @@ use libmcaptcha::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::GetDurationResp;
|
|
||||||
use super::I32Levels;
|
use super::I32Levels;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::stats::record::record_fetch;
|
use crate::stats::record::record_fetch;
|
||||||
@ -96,9 +95,13 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
|
|||||||
&key,
|
&key,
|
||||||
)
|
)
|
||||||
.fetch_all(&data.db);
|
.fetch_all(&data.db);
|
||||||
|
|
||||||
|
struct DurationResp {
|
||||||
|
duration: i32,
|
||||||
|
}
|
||||||
// get duration
|
// get duration
|
||||||
let duration_fut = sqlx::query_as!(
|
let duration_fut = sqlx::query_as!(
|
||||||
GetDurationResp,
|
DurationResp,
|
||||||
"SELECT duration FROM mcaptcha_config
|
"SELECT duration FROM mcaptcha_config
|
||||||
WHERE key = $1",
|
WHERE key = $1",
|
||||||
&key,
|
&key,
|
||||||
|
|||||||
@ -21,8 +21,7 @@ pub mod get_config;
|
|||||||
pub mod verify_pow;
|
pub mod verify_pow;
|
||||||
pub mod verify_token;
|
pub mod verify_token;
|
||||||
|
|
||||||
pub use super::mcaptcha::duration::GetDurationResp;
|
pub use super::mcaptcha::get::I32Levels;
|
||||||
pub use super::mcaptcha::levels::I32Levels;
|
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
let cors = actix_cors::Cors::default()
|
let cors = actix_cors::Cors::default()
|
||||||
|
|||||||
@ -17,9 +17,7 @@
|
|||||||
|
|
||||||
use super::account::routes::Account;
|
use super::account::routes::Account;
|
||||||
use super::auth::routes::Auth;
|
use super::auth::routes::Auth;
|
||||||
use super::mcaptcha::captcha::routes::MCaptcha;
|
use super::mcaptcha::routes::Captcha;
|
||||||
use super::mcaptcha::duration::routes::Duration;
|
|
||||||
use super::mcaptcha::levels::routes::Levels;
|
|
||||||
use super::meta::routes::Meta;
|
use super::meta::routes::Meta;
|
||||||
use super::notifications::routes::Notifications;
|
use super::notifications::routes::Notifications;
|
||||||
use super::pow::routes::PoW;
|
use super::pow::routes::PoW;
|
||||||
@ -29,9 +27,7 @@ pub const ROUTES: Routes = Routes::new();
|
|||||||
pub struct Routes {
|
pub struct Routes {
|
||||||
pub auth: Auth,
|
pub auth: Auth,
|
||||||
pub account: Account,
|
pub account: Account,
|
||||||
pub levels: Levels,
|
pub captcha: Captcha,
|
||||||
pub mcaptcha: MCaptcha,
|
|
||||||
pub duration: Duration,
|
|
||||||
pub meta: Meta,
|
pub meta: Meta,
|
||||||
pub pow: PoW,
|
pub pow: PoW,
|
||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
@ -42,9 +38,7 @@ impl Routes {
|
|||||||
Routes {
|
Routes {
|
||||||
auth: Auth::new(),
|
auth: Auth::new(),
|
||||||
account: Account::new(),
|
account: Account::new(),
|
||||||
levels: Levels::new(),
|
captcha: Captcha::new(),
|
||||||
mcaptcha: MCaptcha::new(),
|
|
||||||
duration: Duration::new(),
|
|
||||||
meta: Meta::new(),
|
meta: Meta::new(),
|
||||||
pow: PoW::new(),
|
pow: PoW::new(),
|
||||||
notifications: Notifications::new(),
|
notifications: Notifications::new(),
|
||||||
|
|||||||
@ -120,7 +120,7 @@ mod tests {
|
|||||||
|
|
||||||
let resp = test::call_service(
|
let resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&token_key, crate::V1_API_ROUTES.levels.get)
|
post_request!(&token_key, crate::V1_API_ROUTES.captcha.get)
|
||||||
.cookie(cookies.clone())
|
.cookie(cookies.clone())
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
@ -133,7 +133,7 @@ mod tests {
|
|||||||
|
|
||||||
let resp = test::call_service(
|
let resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&token_key, crate::V1_API_ROUTES.levels.get)
|
post_request!(&token_key, crate::V1_API_ROUTES.captcha.create)
|
||||||
.cookie(cookies)
|
.cookie(cookies)
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -26,7 +26,7 @@ use crate::{PAGES, V1_API_ROUTES};
|
|||||||
pub async fn delete_sitekey(path: web::Path<String>) -> impl Responder {
|
pub async fn delete_sitekey(path: web::Path<String>) -> impl Responder {
|
||||||
let key = path.into_inner();
|
let key = path.into_inner();
|
||||||
let data = vec![("sitekey", key)];
|
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()
|
.render_once()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
|
|||||||
@ -19,7 +19,7 @@ use actix_identity::Identity;
|
|||||||
use actix_web::{HttpResponse, Responder};
|
use actix_web::{HttpResponse, Responder};
|
||||||
use sailfish::TemplateOnce;
|
use sailfish::TemplateOnce;
|
||||||
|
|
||||||
use crate::api::v1::mcaptcha::captcha::MCaptchaDetails;
|
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::AppData;
|
use crate::AppData;
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,8 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::api::v1::auth::runners::{Login, Register};
|
use crate::api::v1::auth::runners::{Login, Register};
|
||||||
use crate::api::v1::mcaptcha::captcha::MCaptchaDetails;
|
use crate::api::v1::mcaptcha::create::CreateCaptcha;
|
||||||
use crate::api::v1::mcaptcha::levels::AddLevels;
|
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
|
||||||
use crate::api::v1::ROUTES;
|
use crate::api::v1::ROUTES;
|
||||||
use crate::data::Data;
|
use crate::data::Data;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -159,10 +159,10 @@ pub const L2: Level = Level {
|
|||||||
visitor_threshold: 500,
|
visitor_threshold: 500,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn get_level_data() -> AddLevels {
|
pub fn get_level_data() -> CreateCaptcha {
|
||||||
let levels = vec![L1, L2];
|
let levels = vec![L1, L2];
|
||||||
|
|
||||||
AddLevels {
|
CreateCaptcha {
|
||||||
levels,
|
levels,
|
||||||
duration: 30,
|
duration: 30,
|
||||||
description: "dummy".into(),
|
description: "dummy".into(),
|
||||||
@ -182,7 +182,7 @@ pub async fn add_levels_util(
|
|||||||
// 1. add level
|
// 1. add level
|
||||||
let add_token_resp = test::call_service(
|
let add_token_resp = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
post_request!(&add_level, ROUTES.levels.add)
|
post_request!(&add_level, ROUTES.captcha.create)
|
||||||
.cookie(cookies.clone())
|
.cookie(cookies.clone())
|
||||||
.to_request(),
|
.to_request(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
<. include!("../help-banner/index.html"); .>
|
<. include!("../help-banner/index.html"); .>
|
||||||
<!-- Main content container -->
|
<!-- Main content container -->
|
||||||
<div class="inner-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">
|
<h1 class="form__title">
|
||||||
<.= PAGE .>
|
<.= PAGE .>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -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">
|
<h1 class="form__title">
|
||||||
<.= form_title .>
|
<.= form_title .>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -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; .>
|
<. const READONLY: bool = false; .>
|
||||||
<. include!("../view/__form-top.html"); .>
|
<. include!("../view/__form-top.html"); .>
|
||||||
<. for (count, level) in levels.iter().enumerate() { .>
|
<. for (count, level) in levels.iter().enumerate() { .>
|
||||||
|
|||||||
@ -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; .>
|
<. const READONLY: bool = true; .>
|
||||||
<. include!("./__form-top.html"); .>
|
<. include!("./__form-top.html"); .>
|
||||||
<. for (count, level) in levels.iter().enumerate() { .>
|
<. for (count, level) in levels.iter().enumerate() { .>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user