Implement easy edit view

When user tries to visit this view without an easy configuration
available, i.e, user had created the CAPTCHA using advance view and no
TrafficPattern is available in database, the user will be automatically
redirected to the advance edit page.

But the default edit link everywhere is to the easy edit view.
This commit is contained in:
realaravinth 2021-12-18 21:01:19 +05:30
parent ebde9775fc
commit c46b3f4f4c
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
9 changed files with 332 additions and 9 deletions

View file

@ -219,6 +219,7 @@ async fn update(
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use actix_web::web::Bytes;
use super::*;
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
@ -392,5 +393,33 @@ mod tests {
assert_ne!(res_levels, default_levels);
assert_eq!(res_levels, updated_default_values);
// END update_easy
// test easy edit page
let easy_url = PAGES.panel.sitekey.get_edit_easy(&token_key.key);
let easy_edit_page = test::call_service(
&app,
test::TestRequest::get()
.uri(&easy_url)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(easy_edit_page.status(), StatusCode::OK);
let body: Bytes = test::read_body(easy_edit_page).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains(&token_key.name));
assert!(body.contains(
&payload
.pattern
.broke_my_site_traffic
.as_ref()
.unwrap()
.to_string()
));
assert!(body.contains(&payload.pattern.avg_traffic.to_string()));
assert!(body.contains(&payload.pattern.peak_sustainable_traffic.to_string()));
}
}

View file

@ -15,13 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use actix_web::{http, web, HttpResponse, Responder};
use sailfish::TemplateOnce;
use sqlx::Error::RowNotFound;
use crate::api::v1::mcaptcha::easy::TrafficPattern;
use crate::errors::*;
use crate::AppData;
const PAGE: &str = "SiteKeys";
const PAGE: &str = "Edit Sitekey";
#[derive(Clone)]
struct McaptchaConfig {
@ -100,9 +102,118 @@ pub async fn advance(
.body(body))
}
#[derive(TemplateOnce, Clone)]
#[template(path = "panel/sitekey/edit/easy/index.html")]
pub struct EasyEditPage<'a> {
pub form_title: &'a str,
pub pattern: TrafficPattern,
pub key: String,
}
impl<'a> EasyEditPage<'a> {
pub fn new(key: String, pattern: TrafficPattern) -> Self {
Self {
form_title: PAGE,
pattern,
key,
}
}
}
/// route handler that renders individual views for sitekeys
#[my_codegen::get(
path = "crate::PAGES.panel.sitekey.edit_easy",
wrap = "crate::CheckLogin"
)]
pub async fn easy(
path: web::Path<String>,
data: AppData,
id: Identity,
) -> PageResult<impl Responder> {
let username = id.identity().unwrap();
let key = path.into_inner();
struct Traffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
match sqlx::query_as!(
Traffic,
"SELECT
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
FROM
mcaptcha_sitekey_user_provided_avg_traffic
WHERE
config_id = (
SELECT
config_id
FROM
mcaptcha_config
WHERE
KEY = $1
AND user_id = (
SELECT
id
FROM
mcaptcha_users
WHERE
NAME = $2
)
)
",
&key,
&username
)
.fetch_one(&data.db)
.await
{
Ok(c) => {
struct Description {
name: String,
}
let description = sqlx::query_as!(
Description,
"SELECT name FROM mcaptcha_config
WHERE key = $1
AND user_id = (
SELECT user_id FROM mcaptcha_users WHERE NAME = $2)",
&key,
&username
)
.fetch_one(&data.db)
.await?;
let pattern = TrafficPattern {
peak_sustainable_traffic: c.peak_sustainable_traffic as u32,
avg_traffic: c.avg_traffic as u32,
broke_my_site_traffic: c.broke_my_site_traffic.map(|n| n as u32),
description: description.name,
};
let page = EasyEditPage::new(key, pattern).render_once().unwrap();
return Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(page));
}
Err(RowNotFound) => {
return Ok(HttpResponse::Found()
.insert_header((
http::header::LOCATION,
crate::PAGES.panel.sitekey.get_edit_advance(&key),
))
.finish());
}
Err(e) => Err(e.into()),
}
}
#[cfg(test)]
mod test {
use actix_web::http::StatusCode;
use actix_web::http::{header, StatusCode};
use actix_web::test;
use actix_web::web::Bytes;
@ -148,5 +259,19 @@ mod test {
assert!(body.contains(&L1.difficulty_factor.to_string()));
assert!(body.contains(&L2.difficulty_factor.to_string()));
assert!(body.contains(&L2.visitor_threshold.to_string()));
let easy_url = PAGES.panel.sitekey.get_edit_easy(&key.key);
let redirect_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(&easy_url)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(redirect_resp.status(), StatusCode::FOUND);
let headers = redirect_resp.headers();
assert_eq!(headers.get(header::LOCATION).unwrap(), &url);
}
}

View file

@ -73,6 +73,7 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(list::list_sitekeys);
cfg.service(view::view_sitekey);
cfg.service(edit::advance);
cfg.service(edit::easy);
cfg.service(delete::delete_sitekey);
}

View file

@ -25,7 +25,8 @@ import * as deleteAccount from "./panel/settings/account/delete";
import * as updateSecret from "./panel/settings/secret/update";
import * as addSiteKeyAdvance from "./panel/sitekey/add/advance/ts";
import * as addSiteKeyEasy from "./panel/sitekey/add/novice/ts";
import * as editSitekey from "./panel/sitekey/edit/";
import * as editSitekeyAdvance from "./panel/sitekey/edit/";
import * as editSitekeyEasy from "./panel/sitekey/edit/easy/";
import * as deleteSitekey from "./panel/sitekey/delete/";
import * as listSitekeys from "./panel/sitekey/list/ts";
import * as notidications from "./panel/notifications/ts";
@ -48,7 +49,8 @@ router.register(VIEWS.notifications, notidications.index);
router.register(VIEWS.listSitekey, listSitekeys.index);
router.register(VIEWS.addSiteKeyAdvance,addSiteKeyAdvance.index);
router.register(VIEWS.addSiteKeyEasy, addSiteKeyEasy.index);
router.register(VIEWS.editSitekeyAdvance("[A-Z),a-z,0-9]+"), editSitekey.index);
router.register(VIEWS.editSitekeyAdvance("[A-Z),a-z,0-9]+"), editSitekeyAdvance.index);
router.register(VIEWS.editSitekeyEasy("[A-Z),a-z,0-9]+"), editSitekeyEasy.index);
router.register(VIEWS.deleteSitekey("[A-Z),a-z,0-9]+"), deleteSitekey.index);
try {

View file

@ -45,9 +45,14 @@ export const break_my_site_name = "traffic that broke your website";
export const avg_traffic_name = "average";
export const peak_traffic_name = "maximum traffic your website can handle";
const submit = async (e: Event) => {
e.preventDefault();
type TrafficPattern = {
avg_traffic: number;
peak_sustainable_traffic: number;
broke_my_site_traffic?: number;
description: string;
};
export const validate = (e: Event): TrafficPattern => {
const description = validateDescription(e);
let broke_is_set = false;
@ -89,8 +94,6 @@ const submit = async (e: Event) => {
}
}
const formUrl = getFormUrl(FORM);
const payload = {
avg_traffic,
peak_sustainable_traffic,
@ -98,6 +101,14 @@ const submit = async (e: Event) => {
description,
};
return payload;
};
const submit = async (e: Event) => {
e.preventDefault();
const formUrl = getFormUrl(FORM);
const payload = validate(e);
console.debug(`[form submition] json payload: ${JSON.stringify(payload)}`);
const res = await fetch(formUrl, genJsonPayload(payload));

View file

@ -0,0 +1,67 @@
<form class="sitekey-form" action="<.= crate::V1_API_ROUTES.captcha.easy.update .>" method="post">
<div class="sitekey-form__advance-options-container">
<h1 class="sitekey-form__advance-options-form-title">
<.= form_title .>
</h1>
<a
class="sitekey-form__advance-options-link"
href="<.= crate::PAGES.panel.sitekey.get_edit_advance(&key) .>">
Advance Options
</a>
</div>
<label class="sitekey-form__label" for="description">
Description
<input
class="sitekey-form__input"
type="text"
name="description"
id="description"
required
value="<.= pattern.description .>"
/>
</label>
<label class="sitekey-form__label" for="avg_traffic">
Average Traffic of your website
<input
class="sitekey-form__input"
type="number"
name="avg_traffic"
id="avg_traffic"
required
value="<.= pattern.avg_traffic .>"
/>
</label>
<label class="sitekey-form__label" for="avg_traffic">
Maximum traffic that your website can handle
<input
class="sitekey-form__input"
type="number"
name="peak_sustainable_traffic"
id="peak_sustainable_traffic"
required
value="<.= pattern.peak_sustainable_traffic .>"
/>
</label>
<label class="sitekey-form__label" for="avg_traffic">
Traffic that broke your website(Optional)
<input
class="sitekey-form__input"
type="number"
name="broke_my_site_traffic"
id="broke_my_site_traffic"
<. if let Some(broke_my_site_traffic) = pattern.broke_my_site_traffic { .>
value="<.= broke_my_site_traffic .>"
<. } .>
/>
</label>
<button data-sitekey="<.= key .>" class="sitekey-form__submit" type="submit">
Submit
</button>
</form>

View file

@ -0,0 +1,51 @@
/*
* 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/>.
*/
import getFormUrl from "../../../../utils/getFormUrl";
import genJsonPayload from "../../../../utils/genJsonPayload";
import createError from "../../../../components/error";
import VIEWS from "../../../../views/v1/routes";
import { validate, FORM } from "../../add/novice/ts/form";
const SUBMIT_BTN = <HTMLButtonElement>(
document.querySelector(".sitekey-form__submit")
);
const key = SUBMIT_BTN.dataset.sitekey;
const submit = async (e: Event) => {
e.preventDefault();
const formUrl = getFormUrl(FORM);
const payload = {
pattern: validate(e),
key,
};
console.debug(`[form submition] json payload: ${JSON.stringify(payload)}`);
const res = await fetch(formUrl, genJsonPayload(payload));
if (res.ok) {
window.location.assign(VIEWS.viewSitekey(key));
} else {
const err = await res.json();
createError(err.error);
}
};
const addSubmitEventListener = (): void =>
FORM.addEventListener("submit", submit, true);
export default addSubmitEventListener;

View file

@ -0,0 +1,15 @@
<. include!("../../../../components/headers/index.html"); .>
<. include!("../../../navbar/index.html"); .>
<div class="tmp-layout">
<. include!("../../../header/index.html"); .>
<main class="panel-main">
<. include!("../../../help-banner/index.html"); .>
<!-- Main content container -->
<div class="inner-container">
<!-- Main menu/ important actions roaster -->
<. include!("./form.html"); .>
</div>
<!-- end of container -->
<. include!("../../../../components/footers.html"); .>

View file

@ -0,0 +1,22 @@
/*
* 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/>.
*/
import addSubmitEventListener from "./form";
export const index = (): void => {
addSubmitEventListener();
};