diff --git a/sqlx-data.json b/sqlx-data.json index b5bd189d..9bfea1c0 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -26,6 +26,21 @@ "nullable": [] } }, + "238569a64d7dbd252e3b27204f207e8a8548109717b89495ddf8f9a870c7c75d": { + "query": "UPDATE mcaptcha_config SET name = $1, duration = $2 \n WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)\n AND key = $4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int4", + "Text", + "Text" + ] + }, + "nullable": [] + } + }, "2b319a202bb983d5f28979d1e371f399125da1122fbda36a5a55b75b9c743451": { "query": "-- mark a notification as read\nUPDATE mcaptcha_notifications\n SET read = TRUE\nWHERE \n mcaptcha_notifications.id = $1\nAND\n mcaptcha_notifications.rx = (\n SELECT\n id\n FROM\n mcaptcha_users\n WHERE\n name = $2\n );\n", "describe": { @@ -162,6 +177,32 @@ ] } }, + "4c3a9fe30a4c6bd49ab1cb8883c4495993aa05f2991483b4f04913b2e5043a63": { + "query": "SELECT \n difficulty_factor, visitor_threshold \n FROM \n mcaptcha_levels \n WHERE config_id = $1 ORDER BY difficulty_factor ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "difficulty_factor", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "visitor_threshold", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false + ] + } + }, "4dc1b6d8ae3b92ebff45f683951c087244f9614ed0e95b75578f0d1346887224": { "query": "SELECT fetched_at FROM mcaptcha_pow_fetched_stats WHERE config_id = \n (SELECT config_id FROM mcaptcha_config where key = $1)", "describe": { @@ -272,6 +313,33 @@ ] } }, + "717771c42737feb3f4ca13f2ab11361073ea17b55562a103f660149bf049c5c6": { + "query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)\n )\n ORDER BY difficulty_factor ASC;", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "difficulty_factor", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "visitor_threshold", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false + ] + } + }, "73b9bddce90d59193430c5ec38b77ad7bc0e94bac74dcde7ab9949a86cfbddca": { "query": "INSERT INTO mcaptcha_pow_solved_stats \n (config_id) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1))", "describe": { @@ -364,8 +432,8 @@ "nullable": [] } }, - "a1f1e5693dad5c04b85f97d1de9c68b584a1ca99436e61f7c93f2a5acf8fb55f": { - "query": "SELECT \n difficulty_factor, visitor_threshold \n FROM \n mcaptcha_levels \n WHERE config_id = $1", + "9753721856a47438c5e72f28fd9d149db10c48e677b4613bf3f1e8487908aac8": { + "query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n ) ORDER BY difficulty_factor ASC;", "describe": { "columns": [ { @@ -381,33 +449,6 @@ ], "parameters": { "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - false - ] - } - }, - "aa9a21fd88c106fe6c4b75a724b202b7bdda66eb9c5fd91780113e2c3ea82719": { - "query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)\n );", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "difficulty_factor", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "visitor_threshold", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text", "Text" ] }, @@ -521,32 +562,6 @@ "nullable": [] } }, - "d9a097cba4552c17b410fcb8745dd9b2eae5146f7b710006a50ae6aa2add54fa": { - "query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n );", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "difficulty_factor", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "visitor_threshold", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false - ] - } - }, "daebbef26cf04fdc46226304d028528e121a9847c07139d7d3a56a0e7c165879": { "query": "SELECT solved_at FROM mcaptcha_pow_solved_stats WHERE config_id = \n (SELECT config_id FROM mcaptcha_config where key = $1)", "describe": { @@ -665,19 +680,5 @@ false ] } - }, - "fb19fbff4265cc59450d64a8d945f0ae2ad337b97e6192837881e8b6b4c397ee": { - "query": "DELETE FROM mcaptcha_levels WHERE \n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = $1 AND\n user_id = (SELECT ID from mcaptcha_users WHERE name = $3)\n ) AND difficulty_factor = ($2);", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int4", - "Text" - ] - }, - "nullable": [] - } } } \ No newline at end of file diff --git a/src/api/v1/mcaptcha/levels.rs b/src/api/v1/mcaptcha/levels.rs index 4b59b774..5955a9d1 100644 --- a/src/api/v1/mcaptcha/levels.rs +++ b/src/api/v1/mcaptcha/levels.rs @@ -16,6 +16,7 @@ */ use actix_identity::Identity; use actix_web::{web, HttpResponse, Responder}; +use futures::future::try_join_all; use libmcaptcha::{defense::Level, DefenseBuilder}; use log::debug; use serde::{Deserialize, Serialize}; @@ -29,7 +30,7 @@ pub mod routes { pub struct Levels { pub add: &'static str, - pub delete: &'static str, + // pub delete: &'static str, pub get: &'static str, pub update: &'static str, } @@ -38,11 +39,11 @@ pub mod routes { pub const fn new() -> Levels { let add = "/api/v1/mcaptcha/levels/add"; let update = "/api/v1/mcaptcha/levels/update"; - let delete = "/api/v1/mcaptcha/levels/delete"; + // let delete = "/api/v1/mcaptcha/levels/delete"; let get = "/api/v1/mcaptcha/levels/get"; Levels { add, - delete, + // delete, get, update, } @@ -60,7 +61,6 @@ pub struct AddLevels { pub fn services(cfg: &mut web::ServiceConfig) { cfg.service(add_levels); cfg.service(update_levels); - cfg.service(delete_levels); cfg.service(get_levels); } @@ -87,10 +87,12 @@ async fn add_levels( 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; - sqlx::query!( + let fut = sqlx::query!( "INSERT INTO mcaptcha_levels ( difficulty_factor, visitor_threshold, @@ -105,17 +107,20 @@ async fn add_levels( &mcaptcha_config.key, &username, ) - .execute(&data.db) - .await?; + .execute(&data.db); + futs.push(fut); } + try_join_all(futs).await?; + Ok(HttpResponse::Ok().json(mcaptcha_config)) } #[derive(Serialize, Deserialize)] pub struct UpdateLevels { pub levels: Vec, - /// name is config_name + pub duration: u32, + pub description: String, pub key: String, } @@ -140,7 +145,8 @@ async fn update_levels( // still, needs to be benchmarked defense.build()?; - sqlx::query!( + let mut futs = Vec::with_capacity(payload.levels.len() + 2); + let del_fut = sqlx::query!( "DELETE FROM mcaptcha_levels WHERE config_id = ( SELECT config_id FROM mcaptcha_config where key = ($1) @@ -151,13 +157,26 @@ async fn update_levels( &payload.key, &username ) - .execute(&data.db) - .await?; + .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(del_fut); + 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; - sqlx::query!( + let fut = sqlx::query!( "INSERT INTO mcaptcha_levels ( difficulty_factor, visitor_threshold, @@ -173,40 +192,11 @@ async fn update_levels( &payload.key, &username, ) - .execute(&data.db) - .await?; - } - - Ok(HttpResponse::Ok()) -} - -#[my_codegen::post( - path = "crate::V1_API_ROUTES.levels.delete", - wrap = "crate::CheckLogin" -)] -async fn delete_levels( - payload: web::Json, - data: AppData, - id: Identity, -) -> ServiceResult { - let username = id.identity().unwrap(); - - for level in payload.levels.iter() { - let difficulty_factor = level.difficulty_factor as i32; - 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 = $3) - ) AND difficulty_factor = ($2);", - &payload.key, - difficulty_factor, - &username - ) - .execute(&data.db) - .await?; + .execute(&data.db); //.await?; + futs.push(fut); } + try_join_all(futs).await?; Ok(HttpResponse::Ok()) } @@ -245,7 +235,8 @@ async fn get_levels_util( 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 ) @@ -266,6 +257,15 @@ mod tests { 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"; @@ -284,8 +284,7 @@ mod tests { // 2. get level - let levels = vec![L1, L2]; - + let add_level = get_level_data(); let get_level_resp = test::call_service( &app, post_request!(&key, ROUTES.levels.get) @@ -295,31 +294,28 @@ mod tests { .await; assert_eq!(get_level_resp.status(), StatusCode::OK); let res_levels: Vec = test::read_body_json(get_level_resp).await; - assert_eq!(res_levels, levels); + assert_eq!(res_levels, add_level.levels); // 3. update level - let l1 = Level { - difficulty_factor: 10, - visitor_threshold: 10, - }; - let l2 = Level { - difficulty_factor: 5000, - visitor_threshold: 5000, - }; - let levels = vec![l1, l2]; - let add_level = UpdateLevels { - levels: levels.clone(), + 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!(&add_level, ROUTES.levels.update) + 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) @@ -330,38 +326,5 @@ mod tests { assert_eq!(get_level_resp.status(), StatusCode::OK); let res_levels: Vec = test::read_body_json(get_level_resp).await; assert_eq!(res_levels, levels); - - // 4. delete level - let l1 = Level { - difficulty_factor: 10, - visitor_threshold: 10, - }; - let l2 = Level { - difficulty_factor: 5000, - visitor_threshold: 5000, - }; - let levels = vec![l1, l2]; - let add_level = UpdateLevels { - levels: levels.clone(), - key: key.key.clone(), - }; - let add_token_resp = test::call_service( - &app, - post_request!(&add_level, ROUTES.levels.delete) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(add_token_resp.status(), StatusCode::OK); - let get_level_resp = test::call_service( - &app, - post_request!(&key, ROUTES.levels.get) - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(get_level_resp.status(), StatusCode::OK); - let res_levels: Vec = test::read_body_json(get_level_resp).await; - assert_eq!(res_levels, Vec::new()); } } diff --git a/src/api/v1/pow/get_config.rs b/src/api/v1/pow/get_config.rs index 99f82ead..974a4d44 100644 --- a/src/api/v1/pow/get_config.rs +++ b/src/api/v1/pow/get_config.rs @@ -96,7 +96,7 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> { "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE config_id = ( SELECT config_id FROM mcaptcha_config WHERE key = ($1) - );", + ) ORDER BY difficulty_factor ASC;", &key, ) .fetch_all(&data.db); diff --git a/src/pages/panel/sitekey/edit.rs b/src/pages/panel/sitekey/edit.rs new file mode 100644 index 00000000..282ee8b4 --- /dev/null +++ b/src/pages/panel/sitekey/edit.rs @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use futures::{future::TryFutureExt, try_join}; +use sailfish::TemplateOnce; + +use crate::errors::*; +use crate::stats::fetch::Stats; +use crate::AppData; + +const PAGE: &str = "SiteKeys"; + +#[derive(Clone)] +struct McaptchaConfig { + config_id: i32, + duration: i32, + name: String, +} + +#[derive(Clone)] +struct Level { + difficulty_factor: i32, + visitor_threshold: i32, +} + +#[derive(TemplateOnce, Clone)] +#[template(path = "panel/sitekey/edit/index.html")] +struct IndexPage { + duration: u32, + name: String, + key: String, + levels: Vec, +} + +impl IndexPage { + fn new(config: McaptchaConfig, levels: Vec, key: String) -> Self { + IndexPage { + duration: config.duration as u32, + name: config.name, + levels, + key, + } + } +} + +/// route handler that renders individual views for sitekeys +#[my_codegen::get(path = "crate::PAGES.panel.sitekey.edit", wrap = "crate::CheckLogin")] +pub async fn edit_sitekey( + path: web::Path, + data: AppData, + id: Identity, +) -> PageResult { + let username = id.identity().unwrap(); + let key = path.into_inner(); + + let config = sqlx::query_as!( + McaptchaConfig, + "SELECT config_id, duration, name from mcaptcha_config WHERE + key = $1 AND + user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ", + &key, + &username, + ) + .fetch_one(&data.db) + .await?; + + let levels_fut = sqlx::query_as!( + Level, + "SELECT + difficulty_factor, visitor_threshold + FROM + mcaptcha_levels + WHERE config_id = $1 ORDER BY difficulty_factor ASC", + &config.config_id + ) + .fetch_all(&data.db) + .err_into(); + + let (_stats, levels) = try_join!(Stats::new(&key, &data.db), levels_fut)?; + + let body = IndexPage::new(config, levels, key).render_once().unwrap(); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(body)) +} + +#[cfg(test)] +mod test { + use actix_web::http::StatusCode; + use actix_web::test; + use actix_web::web::Bytes; + + use crate::tests::*; + use crate::*; + + #[actix_rt::test] + async fn view_sitekey_work() { + const NAME: &str = "editsitekeyuser"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "editsitekeyuser@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; + + let url = format!("/sitekey/{}/edit", &key.key); + + let list_sitekey_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(&url) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(list_sitekey_resp.status(), StatusCode::OK); + + let body: Bytes = test::read_body(list_sitekey_resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + + assert!(body.contains(&key.name)); + + assert!(body.contains(&L1.visitor_threshold.to_string())); + assert!(body.contains(&L1.difficulty_factor.to_string())); + assert!(body.contains(&L2.difficulty_factor.to_string())); + assert!(body.contains(&L2.visitor_threshold.to_string())); + } +} diff --git a/src/pages/panel/sitekey/mod.rs b/src/pages/panel/sitekey/mod.rs index 66748801..68c1f606 100644 --- a/src/pages/panel/sitekey/mod.rs +++ b/src/pages/panel/sitekey/mod.rs @@ -16,6 +16,7 @@ */ mod add; +mod edit; pub mod list; mod view; @@ -24,6 +25,7 @@ pub mod routes { pub list: &'static str, pub add: &'static str, pub view: &'static str, + pub edit: &'static str, } impl Sitekey { @@ -32,6 +34,7 @@ pub mod routes { list: "/sitekeys", add: "/sitekeys/add", view: "/sitekey/{key}", + edit: "/sitekey/{key}/edit", } } } @@ -41,4 +44,5 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(add::add_sitekey); cfg.service(list::list_sitekeys); cfg.service(view::view_sitekey); + cfg.service(edit::edit_sitekey); } diff --git a/src/pages/panel/sitekey/view.rs b/src/pages/panel/sitekey/view.rs index f1cc6a83..789709b7 100644 --- a/src/pages/panel/sitekey/view.rs +++ b/src/pages/panel/sitekey/view.rs @@ -1,19 +1,19 @@ /* -* Copyright (C) 2021 Aravinth Manivannan -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see . -*/ + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ use actix_identity::Identity; use actix_web::{web, HttpResponse, Responder}; @@ -85,7 +85,7 @@ pub async fn view_sitekey( difficulty_factor, visitor_threshold FROM mcaptcha_levels - WHERE config_id = $1", + WHERE config_id = $1 ORDER BY difficulty_factor ASC", &config.config_id ) .fetch_all(&data.db) diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 55e5bbbc..926ff4b5 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -171,6 +171,16 @@ pub const L2: Level = Level { visitor_threshold: 500, }; +pub fn get_level_data() -> AddLevels { + let levels = vec![L1, L2]; + + AddLevels { + levels, + duration: 30, + description: "dummy".into(), + } +} + pub async fn add_levels_util( name: &str, password: &str, @@ -179,13 +189,7 @@ pub async fn add_levels_util( let cookies = get_cookie!(signin_resp); let app = get_app!(data).await; - let levels = vec![L1, L2]; - - let add_level = AddLevels { - levels: levels.clone(), - duration: 30, - description: "dummy".into(), - }; + let add_level = get_level_data(); // 1. add level let add_token_resp = test::call_service( diff --git a/templates/index.ts b/templates/index.ts index 33c6b3d2..8d1db03c 100644 --- a/templates/index.ts +++ b/templates/index.ts @@ -21,6 +21,7 @@ import * as login from './auth/login/ts/'; import * as register from './auth/register/ts/'; import * as panel from './panel/ts/index'; import * as addSiteKey from './panel/sitekey/add/ts'; +import * as editSitekey from './panel/sitekey/edit/'; import * as listSitekeys from './panel/sitekey/list/ts'; import {MODE} from './logger'; import log from './logger'; @@ -51,6 +52,7 @@ router.register(VIEWS.registerUser, register.index); router.register(VIEWS.loginUser, login.index); router.register(VIEWS.listSitekey, listSitekeys.index); router.register(VIEWS.addSiteKey, addSiteKey.index); +router.register(VIEWS.editSitekey("[A-Z,a-z,0-9]+"), editSitekey.index); try { router.route(); diff --git a/templates/panel/sitekey/add/index.html b/templates/panel/sitekey/add/index.html index 3dab97f5..47a16e61 100644 --- a/templates/panel/sitekey/add/index.html +++ b/templates/panel/sitekey/add/index.html @@ -9,7 +9,7 @@
- <. include!("../add/form.html"); .> + <. include!("./form.html"); .>
<. include!("../../../components/footers.html"); .> diff --git a/templates/panel/sitekey/add/ts/addLevelButton.test.ts b/templates/panel/sitekey/add/ts/addLevelButton.test.ts index 82e3bf1e..5af3b58e 100644 --- a/templates/panel/sitekey/add/ts/addLevelButton.test.ts +++ b/templates/panel/sitekey/add/ts/addLevelButton.test.ts @@ -17,8 +17,12 @@ import getNumLevels from './levels/getNumLevels'; import {getAddForm, trim, addLevel} from './setupTests'; +import setup from '../../../../components/error/setUpTests'; document.body.innerHTML = getAddForm(); +document.body.appendChild(setup()); + +jest.useFakeTimers(); it('addLevelButton works', () => { expect(getNumLevels()).toBe(1); @@ -26,23 +30,25 @@ it('addLevelButton works', () => { addLevel(2, 4); expect(getNumLevels()).toBe(2); - // try to add duplicate level - addLevel(2, 4); - expect(getNumLevels()).toBe(2); - - // try to add negative parameters - addLevel(-4, -9); - expect(getNumLevels()).toBe(2); + // add second level addLevel(4, 9); expect(getNumLevels()).toBe(3); let a = document.body.innerHTML; - expect(trim(a)).toBe(trim(finalHtml())); -}); + expect(trim(a)).toBe(trim(finalHtml())); + + // try to add duplicate level + addLevel(2, 4); + expect(getNumLevels()).toBe(3); + + // try to add negative parameters + addLevel(-4, -9); + expect(getNumLevels()).toBe(3); +}); const finalHtml = () => { return ` @@ -186,5 +192,7 @@ const finalHtml = () => { +
+
`; }; diff --git a/templates/panel/sitekey/add/ts/addLevelButton.ts b/templates/panel/sitekey/add/ts/addLevelButton.ts index e2db8913..39ff75f5 100644 --- a/templates/panel/sitekey/add/ts/addLevelButton.ts +++ b/templates/panel/sitekey/add/ts/addLevelButton.ts @@ -39,17 +39,12 @@ const addLevel = (e: Event) => { const isValid = validateLevel(onScreenLevel); log.debug(`[addLevelButton] isValid: ${isValid}`); if (!isValid) { - return log.error('Aborting level addition'); + let error = `Aborting level ${onScreenLevel} addition`; + return log.error(error); } - // eventTarget.remove(); FIELDSET.replaceChild(getRemoveButtonHTML(onScreenLevel), PARENT); - // PARENT.appendChild( PARENT.htmlFor = `${CONST.REMOVE_LEVEL_BUTTON_ID_WITHOUT_LEVEL}${onScreenLevel}`; - //FIELDSET.innerHTML += getRemoveButtonHTML(numLevels); - - //PARENT.remove(); - const newLevelElement = getHtml(onScreenLevel + 1); FIELDSET.insertAdjacentElement('afterend', newLevelElement); UpdateLevel.register(onScreenLevel); diff --git a/templates/panel/sitekey/add/ts/form/index.ts b/templates/panel/sitekey/add/ts/form/index.ts index 5dc9e1af..0fd1ce85 100644 --- a/templates/panel/sitekey/add/ts/form/index.ts +++ b/templates/panel/sitekey/add/ts/form/index.ts @@ -27,10 +27,10 @@ import validateDuration from './validateDuration'; import createError from '../../../../../components/error'; -const SITE_KEY_FORM_CLASS = 'sitekey-form'; -const FORM = document.querySelector(`.${SITE_KEY_FORM_CLASS}`); +export const SITE_KEY_FORM_CLASS = 'sitekey-form'; +export const FORM = document.querySelector(`.${SITE_KEY_FORM_CLASS}`); -const addSubmitEventListener = () => { +export const addSubmitEventListener = () => { FORM.addEventListener('submit', submit, true); }; diff --git a/templates/panel/sitekey/add/ts/levels/index.ts b/templates/panel/sitekey/add/ts/levels/index.ts index 34b23891..a502c623 100644 --- a/templates/panel/sitekey/add/ts/levels/index.ts +++ b/templates/panel/sitekey/add/ts/levels/index.ts @@ -34,11 +34,15 @@ class Levels { add = (newLevel: Level) => { log.debug(`[levels/index.ts] levels lenght: ${this.levels.length}`); if (newLevel.difficulty_factor <= 0) { - throw new Error('Difficulty must be greater than zero'); + throw new Error( + `Level ${this.levels.length}'s difficulty must be greater than zero`, + ); } if (newLevel.visitor_threshold <= 0) { - throw new Error('Visitors must be greater than zero'); + throw new Error( + `Level ${this.levels.length}'s visitors must be greater than zero`, + ); } if (this.levels.length == 0) { @@ -50,10 +54,10 @@ class Levels { this.levels.forEach(level => { if (level.visitor_threshold >= newLevel.visitor_threshold) { - const msg = `Level: ${newLevel} visitor count has to greater than previous levels. See ${count}`; + const msg = `Level ${this.levels.length}'s visitor count should be greater than previous levels(Level ${count} is greater)`; throw new Error(msg); } else if (level.difficulty_factor >= newLevel.difficulty_factor) { - const msg = `Level ${this.levels.length} difficulty has to greater than previous levels See ${count}`; + const msg = `Level ${this.levels.length} difficulty should be greater than previous levels(Level ${count} is greater)`; throw new Error(msg); } else { count++; diff --git a/templates/panel/sitekey/add/ts/levels/levels.test.ts b/templates/panel/sitekey/add/ts/levels/levels.test.ts index e5881da0..1b446ae1 100644 --- a/templates/panel/sitekey/add/ts/levels/levels.test.ts +++ b/templates/panel/sitekey/add/ts/levels/levels.test.ts @@ -18,11 +18,11 @@ import {LEVELS, Level} from './index'; import {level1, level1visErr, level1diffErr, level2} from '../setupTests'; -const visitorErr = 'visitor count has to greater than previous levels'; -const difficultyErr = 'difficulty has to greater than previous levels'; +const visitorErr = 'visitor count should be greater than previous levels'; +const difficultyErr = 'difficulty should be greater than previous levels'; -const zeroVisError = 'Visitors must be greater than zero'; -const zeroDiffError = 'Difficulty must be greater than zero'; +const zeroVisError = 'visitors must be greater than zero'; +const zeroDiffError = 'difficulty must be greater than zero'; const zeroVis: Level = { difficulty_factor: 10, @@ -71,12 +71,12 @@ it('LEVELS works', () => { try { LEVELS.add(zeroVis); } catch (e) { - expect(e.message).toEqual(zeroVisError); + expect(e.message).toContain(zeroVisError); } // difficulty is 0 try { LEVELS.add(zeroDiff); } catch (e) { - expect(e.message).toEqual(zeroDiffError); + expect(e.message).toContain(zeroDiffError); } }); diff --git a/templates/panel/sitekey/add/ts/levels/updateLevel.ts b/templates/panel/sitekey/add/ts/levels/updateLevel.ts index 8fc99a26..fa9d9fc9 100644 --- a/templates/panel/sitekey/add/ts/levels/updateLevel.ts +++ b/templates/panel/sitekey/add/ts/levels/updateLevel.ts @@ -43,7 +43,7 @@ const updateLevel = (e: Event) => { const updatedLevel = getLevelFields(level); LEVELS.update(updatedLevel, level); } catch (e) { - createError(e); + createError(e.message); } }; diff --git a/templates/panel/sitekey/add/ts/levels/validateLevel.test.ts b/templates/panel/sitekey/add/ts/levels/validateLevel.test.ts index 3e25de05..adc1d034 100644 --- a/templates/panel/sitekey/add/ts/levels/validateLevel.test.ts +++ b/templates/panel/sitekey/add/ts/levels/validateLevel.test.ts @@ -17,9 +17,12 @@ import validateLevel from './validateLevel'; import {getAddForm, level1, fillAddLevel} from '../setupTests'; +import setup from '../../../../../components/error/setUpTests'; document.body.innerHTML = getAddForm(); +document.body.appendChild(setup()); + it('validate levels fields works', () => { // null error expect(validateLevel(1)).toEqual(false); diff --git a/templates/panel/sitekey/add/ts/levels/validateLevel.ts b/templates/panel/sitekey/add/ts/levels/validateLevel.ts index e99c3164..75cad2a0 100644 --- a/templates/panel/sitekey/add/ts/levels/validateLevel.ts +++ b/templates/panel/sitekey/add/ts/levels/validateLevel.ts @@ -17,6 +17,7 @@ import {LEVELS} from './index'; import getLevelFields from './getLevelFields'; +import createError from '../../../../../components/error/'; /** * Fetches level from DOM using the ID passesd and validates @@ -28,6 +29,7 @@ const validateLevel = (id: number) => { LEVELS.add(level); return true; } catch (e) { + createError(e.message); return false; } }; diff --git a/templates/panel/sitekey/add/ts/t.html b/templates/panel/sitekey/add/ts/t.html deleted file mode 100644 index ef35c36e..00000000 --- a/templates/panel/sitekey/add/ts/t.html +++ /dev/null @@ -1,248 +0,0 @@ -
-

- Add Sitekey -

- - - - -
- - Level 1 - - - - - -
-
- - Level 2 - - - - - -
-
- - Level 3 - - - - - -
-
- - Level 4 - - - - - -
-
- - Level 5 - - - - - -
-
- - Level 6 - - - - - -
- - -
diff --git a/templates/panel/sitekey/edit/edit.test.ts b/templates/panel/sitekey/edit/edit.test.ts new file mode 100644 index 00000000..5ac25bde --- /dev/null +++ b/templates/panel/sitekey/edit/edit.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import getNumLevels from '../add/ts/levels/getNumLevels'; +import {addLevel} from '../add/ts/setupTests'; +import setup from '../../../components/error/setUpTests'; +import * as SETUP from './setupTest'; + +document.body.innerHTML = SETUP.EDIT_FORM; +document.body.appendChild(setup()); + +jest.useFakeTimers(); + +it('edit sitekey works', () => { + expect(getNumLevels()).toBe(2); + // add a level + addLevel(5, 6); + expect(getNumLevels()).toBe(3); + + // add second level + addLevel(8, 9); + expect(getNumLevels()).toBe(4); + + jest.runAllTimers(); + + // expect(trim(a)).toBe(trim(finalHtml())); + + // try to add negative parameters + addLevel(-4, -9); + expect(getNumLevels()).toBe(4); + + // try to add duplicate level + addLevel(6, 7); + expect(getNumLevels()).toBe(4); + +}); diff --git a/templates/panel/sitekey/edit/existing-level.html b/templates/panel/sitekey/edit/existing-level.html new file mode 100644 index 00000000..7020ea48 --- /dev/null +++ b/templates/panel/sitekey/edit/existing-level.html @@ -0,0 +1,36 @@ +<. let num = count + 1; .> +
+ + Level <.= num .> + + + + + +
diff --git a/templates/panel/sitekey/edit/index.html b/templates/panel/sitekey/edit/index.html new file mode 100644 index 00000000..e7f46610 --- /dev/null +++ b/templates/panel/sitekey/edit/index.html @@ -0,0 +1,12 @@ +<. const URL: &str = crate::V1_API_ROUTES.levels.update; .> +<. const READONLY: bool = false; .> +<. include!("../view/__form-top.html"); .> + <. for (count, level) in levels.iter().enumerate() { .> + <. include!("./existing-level.html"); .> + <. } .> + <. let level = levels.len() + 1; .> + <. include!("../add/add-level.html"); .> + +<. include!("../view/__form-bottom.html"); .> diff --git a/templates/panel/sitekey/edit/index.ts b/templates/panel/sitekey/edit/index.ts new file mode 100644 index 00000000..30510ff6 --- /dev/null +++ b/templates/panel/sitekey/edit/index.ts @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import * as Add from '../add/ts/form/'; +import addLevelButtonAddEventListener from '../add/ts/addLevelButton'; +import {addRemoveLevelButtonEventListenerAll} from '../add/ts/removeLevelButton'; +import getNumLevels from '../add/ts/levels/getNumLevels'; +import validateLevel from '../add/ts/levels/validateLevel'; +import * as UpdateLevel from '../add/ts/levels/updateLevel'; +import validateDescription from '../add/ts/form/validateDescription'; +import validateDuration from '../add/ts/form/validateDuration'; +import {LEVELS} from '../add/ts/levels'; + +import getFormUrl from '../../../utils/getFormUrl'; +import genJsonPayload from '../../../utils/genJsonPayload'; +import createError from '../../../components/error'; + +import VIEWS from '../../../views/v1/routes'; + +const BTN = document.querySelector('.sitekey-form__submit'); +const key = BTN.dataset.sitekey; + +const submit = async (e: Event) => { + e.preventDefault(); + + const description = validateDescription(e); + const duration = validateDuration(e); + + const formUrl = getFormUrl(Add.FORM); + + const levels = LEVELS.getLevels(); + console.debug(`[form submition]: levels: ${levels}`); + + const payload = { + levels, + duration, + description, + 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 = () => { + Add.FORM.addEventListener('submit', submit, true); +}; + +const bootstrapLevels = () => { + const levels = getNumLevels(); + addRemoveLevelButtonEventListenerAll(); + for (let i = 1; i <= levels - 1; i++) { + validateLevel(i); + UpdateLevel.register(i); + } +}; + +export const index = () => { + addSubmitEventListener(); + addLevelButtonAddEventListener(); + bootstrapLevels(); +}; diff --git a/templates/panel/sitekey/edit/setupTest.ts b/templates/panel/sitekey/edit/setupTest.ts new file mode 100644 index 00000000..fdcf0f64 --- /dev/null +++ b/templates/panel/sitekey/edit/setupTest.ts @@ -0,0 +1,107 @@ +export const EDIT_FORM = ` +
+

+ Sitekey: test +

+ + + +
+ + Level 1 + + + + + +
+ +
+ + Level 2 + + + + + +
+ +
+
+`; diff --git a/templates/panel/sitekey/view/__edit-sitekey-icon.html b/templates/panel/sitekey/view/__edit-sitekey-icon.html new file mode 100644 index 00000000..5f0ab3af --- /dev/null +++ b/templates/panel/sitekey/view/__edit-sitekey-icon.html @@ -0,0 +1,5 @@ + + " alt="Edit + sitekey" /> + diff --git a/templates/panel/sitekey/view/__form-bottom.html b/templates/panel/sitekey/view/__form-bottom.html new file mode 100644 index 00000000..1d078c0c --- /dev/null +++ b/templates/panel/sitekey/view/__form-bottom.html @@ -0,0 +1,4 @@ + + + +<. include!("../../../components/footers.html"); .> diff --git a/templates/panel/sitekey/view/__form-top.html b/templates/panel/sitekey/view/__form-top.html new file mode 100644 index 00000000..cc897bab --- /dev/null +++ b/templates/panel/sitekey/view/__form-top.html @@ -0,0 +1,56 @@ +<. include!("../../../components/headers/widget-headers.html"); .> + +<. include!("../../navbar/index.html"); .> +
+<. include!("../../header/index.html"); .> +
+ <. include!("../../help-banner/index.html"); .> + +
+ +
+

Sitekey: <.= name .> + View deployment + " + alt="View widget deployment" + /> + + <. if READONLY { .> + <. include!("./__edit-sitekey-icon.html"); .> + <. } .> +

+ + diff --git a/templates/panel/sitekey/view/index.html b/templates/panel/sitekey/view/index.html index 7311f17d..66c1173b 100644 --- a/templates/panel/sitekey/view/index.html +++ b/templates/panel/sitekey/view/index.html @@ -1,58 +1,7 @@ -<. include!("../../../components/headers/widget-headers.html"); .> - -<. include!("../../navbar/index.html"); .> -
-<. include!("../../header/index.html"); .> -
- <. include!("../../help-banner/index.html"); .> - -
- - -

Sitekey: <.= name .> - View widget - " - alt="View widget deployment" - /> - -

- - - +<. const URL: &str = crate::V1_API_ROUTES.levels.add; .> +<. const READONLY: bool = true; .> +<. include!("./__form-top.html"); .> <. for (count, level) in levels.iter().enumerate() { .> <. include!("./existing-level.html"); .> <. } .> - - -
- -<. include!("../../../components/footers.html"); .> +<. include!("./__form-bottom.html"); .> diff --git a/templates/panel/sitekey/view/ts/index.ts b/templates/panel/sitekey/view/ts/index.ts index 661a4391..458b1d19 100644 --- a/templates/panel/sitekey/view/ts/index.ts +++ b/templates/panel/sitekey/view/ts/index.ts @@ -14,5 +14,4 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - export const index = () => {}; diff --git a/templates/router.test.ts b/templates/router.test.ts index 0e03facd..a2dfef22 100644 --- a/templates/router.test.ts +++ b/templates/router.test.ts @@ -31,6 +31,11 @@ const settingsRoute = '/settings/'; const settingsResult = 'hello from settings'; const settings = () => (result.result = settingsResult); +const patternRoute = '/sitekey/[A-Z,a-z,0-9,_]+/'; +const examplePatternRoute = '/sitekey/alksdjakdjadajkhdjahrjke234/'; +const patterResult = 'hello from pattern route'; +const pattern = () => (result.result = patterResult); + const UriExistsErr = 'URI exists'; const emptyUriErr = 'uri is empty'; const unregisteredRouteErr = "Route isn't registered"; @@ -38,8 +43,21 @@ const unregisteredRouteErr = "Route isn't registered"; const router = new Router(); router.register(panelRoute, panel); router.register(settingsRoute, settings); +router.register(patternRoute, pattern); it('checks if Router works', () => { + window.history.pushState({}, '', examplePatternRoute); + router.route(); + expect(result.result).toBe(patterResult); + + window.history.pushState( + {}, + '', + examplePatternRoute.slice(0, examplePatternRoute.length - 1), + ); + router.route(); + expect(result.result).toBe(patterResult); + window.history.pushState({}, 'Settings', settingsRoute); router.route(); expect(result.result).toBe(settingsResult); @@ -68,4 +86,12 @@ it('checks if Router works', () => { } catch (e) { expect(e.message).toBe(unregisteredRouteErr); } + + // routing to unregistered route + try { + window.history.pushState({}, `Page Doesn't Exist`, `/sitekey/;asd;lasdj`); + router.route(); + } catch (e) { + expect(e.message).toBe(unregisteredRouteErr); + } }); diff --git a/templates/router.ts b/templates/router.ts index 52a68327..5251f926 100644 --- a/templates/router.ts +++ b/templates/router.ts @@ -31,7 +31,7 @@ const normalizeUri = (uri: string) => { /** URI<-> Fn mapping type */ type routeTuple = { - uri: string; + pattern: RegExp; fn: () => void; }; @@ -54,10 +54,15 @@ export class Router { register(uri: string, fn: () => void) { uri = normalizeUri(uri); + let pattern = new RegExp(`^${uri}(.*)`); + + let patterString = pattern.toString(); if ( this.routes.find(route => { - if (route.uri == uri) { + if (route.pattern.toString() == patterString) { return true; + } else { + return false; } }) ) { @@ -65,7 +70,7 @@ export class Router { } const route: routeTuple = { - uri, + pattern, fn, }; this.routes.push(route); @@ -81,8 +86,7 @@ export class Router { let fn: () => void | undefined; this.routes.forEach(route => { - const pattern = new RegExp(`^${route.uri}$`); - if (path.match(pattern)) { + if (path.match(route.pattern)) { fn = route.fn; } }); diff --git a/templates/views/v1/routes.ts b/templates/views/v1/routes.ts index 799cb116..8fb33d5a 100644 --- a/templates/views/v1/routes.ts +++ b/templates/views/v1/routes.ts @@ -23,6 +23,7 @@ const ROUTES = { docsHome: '/docs/', listSitekey: '/sitekeys/', viewSitekey: (key: string) => `/sitekey/${key}/`, + editSitekey: (key: string) => `/sitekey/${key}/edit/`, addSiteKey: '/sitekeys/add', };