add site key form

This commit is contained in:
realaravinth 2021-05-03 20:24:03 +05:30
parent 0531a26274
commit 812b0ff2c9
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
49 changed files with 1253 additions and 597 deletions

View file

@ -1,8 +1,8 @@
# WIP # WIP
default: build-frontend default: frontend
cargo build cargo build
run: build-frontend-dev run: frontend-dev
cargo run cargo run
dev-env: dev-env:
@ -12,10 +12,10 @@ dev-env:
docs: docs:
cargo doc --no-deps --workspace --all-features cargo doc --no-deps --workspace --all-features
build-frontend-dev: frontend-dev:
yarn start yarn start
build-frontend: frontend:
yarn build yarn build
test: migrate test: migrate
@ -27,7 +27,7 @@ xml-test-coverage: migrate
coverage: migrate coverage: migrate
cargo tarpaulin -t 1200 --out Html cargo tarpaulin -t 1200 --out Html
release: build-frontend release: frontend
cargo build --release cargo build --release
clean: clean:
@ -38,11 +38,13 @@ migrate:
cargo run --bin tests-migrate cargo run --bin tests-migrate
help: help:
@echo ' docs - build documentation'
@echo ' run - run developer instance' @echo ' run - run developer instance'
@echo ' test - run unit and integration tests' @echo ' test - run unit and integration tests'
@echo ' frontend-dev - build static assets in dev mode'
@echo ' frontend - build static assets in prod mode'
@echo ' migrate - run database migrations' @echo ' migrate - run database migrations'
@echo ' dev-env - download dependencies' @echo ' dev-env - download dependencies'
@echo ' docs - build documentation'
@echo ' clean - drop builds and environments' @echo ' clean - drop builds and environments'
@echo ' coverage - build test coverage in HTML format' @echo ' coverage - build test coverage in HTML format'
@echo ' xml-coverage - build test coverage in XML for upload to codecov' @echo ' xml-coverage - build test coverage in XML for upload to codecov'

View file

@ -177,7 +177,8 @@ async fn signout(id: Identity) -> impl Responder {
if let Some(_) = id.identity() { if let Some(_) = id.identity() {
id.forget(); id.forget();
} }
HttpResponse::Ok() HttpResponse::Found()
.set_header(header::LOCATION, "/login") .header(header::LOCATION, "/login")
.body("") .finish()
.into_body()
} }

View file

@ -20,6 +20,7 @@ use actix_web::{web, HttpResponse, Responder};
use m_captcha::{defense::Level, DefenseBuilder}; use m_captcha::{defense::Level, DefenseBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::mcaptcha::add_mcaptcha_util;
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails; use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
use crate::errors::*; use crate::errors::*;
use crate::Data; use crate::Data;
@ -52,8 +53,6 @@ pub mod routes {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct AddLevels { pub struct AddLevels {
pub levels: Vec<Level>, pub levels: Vec<Level>,
/// name is config_name
pub key: String,
} }
pub fn services(cfg: &mut web::ServiceConfig) { pub fn services(cfg: &mut web::ServiceConfig) {
@ -104,6 +103,8 @@ async fn add_levels(
defense.build()?; defense.build()?;
let mcaptcha_config = add_mcaptcha_util(&data, &id).await?;
for level in payload.levels.iter() { for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32; let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32; let visitor_threshold = level.visitor_threshold as i32;
@ -119,18 +120,25 @@ async fn add_levels(
)));", )));",
difficulty_factor, difficulty_factor,
visitor_threshold, visitor_threshold,
&payload.key, &mcaptcha_config.key,
&username, &username,
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
} }
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok().json(mcaptcha_config))
}
#[derive(Serialize, Deserialize)]
pub struct UpdateLevels {
pub levels: Vec<Level>,
/// name is config_name
pub key: String,
} }
async fn update_levels( async fn update_levels(
payload: web::Json<AddLevels>, payload: web::Json<UpdateLevels>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
@ -187,7 +195,7 @@ async fn update_levels(
} }
async fn delete_levels( async fn delete_levels(
payload: web::Json<AddLevels>, payload: web::Json<UpdateLevels>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
@ -304,7 +312,7 @@ mod tests {
visitor_threshold: 5000, visitor_threshold: 5000,
}; };
let levels = vec![l1, l2]; let levels = vec![l1, l2];
let add_level = AddLevels { let add_level = UpdateLevels {
levels: levels.clone(), levels: levels.clone(),
key: key.key.clone(), key: key.key.clone(),
}; };
@ -337,7 +345,7 @@ mod tests {
visitor_threshold: 5000, visitor_threshold: 5000,
}; };
let levels = vec![l1, l2]; let levels = vec![l1, l2];
let add_level = AddLevels { let add_level = UpdateLevels {
levels: levels.clone(), levels: levels.clone(),
key: key.key.clone(), key: key.key.clone(),
}; };

View file

@ -88,7 +88,7 @@ pub struct MCaptchaDetails {
} }
// this should be called from within add levels // this should be called from within add levels
async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl Responder> { pub async fn add_mcaptcha_util(data: &Data, id: &Identity) -> ServiceResult<MCaptchaDetails> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let mut key; let mut key;
@ -125,7 +125,12 @@ async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl
} }
} }
} }
Ok(resp)
}
// this should be called from within add levels
async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl Responder> {
let resp = add_mcaptcha_util(&data, &id).await?;
Ok(HttpResponse::Ok().json(resp)) Ok(HttpResponse::Ok().json(resp))
} }

View file

@ -123,7 +123,7 @@ async fn auth_works() {
.to_request(), .to_request(),
) )
.await; .await;
assert_eq!(signout_resp.status(), StatusCode::OK); assert_eq!(signout_resp.status(), StatusCode::FOUND);
let headers = signout_resp.headers(); let headers = signout_resp.headers();
assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login); assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login);
} }

View file

@ -70,6 +70,10 @@ async fn protected_routes_work() {
) )
.await; .await;
if url == &V1_API_ROUTES.auth.logout {
assert_eq!(authenticated_resp.status(), StatusCode::FOUND);
} else {
assert_eq!(authenticated_resp.status(), StatusCode::OK); assert_eq!(authenticated_resp.status(), StatusCode::OK);
} }
}
} }

View file

@ -18,6 +18,8 @@
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use crate::pages::TITLE;
#[derive(Clone, TemplateOnce)] #[derive(Clone, TemplateOnce)]
#[template(path = "auth/login/index.html")] #[template(path = "auth/login/index.html")]
struct IndexPage<'a> { struct IndexPage<'a> {
@ -28,7 +30,7 @@ struct IndexPage<'a> {
impl<'a> Default for IndexPage<'a> { impl<'a> Default for IndexPage<'a> {
fn default() -> Self { fn default() -> Self {
IndexPage { IndexPage {
name: "mCaptcha", name: TITLE,
title: "Login", title: "Login",
} }
} }

View file

@ -18,6 +18,8 @@
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use crate::pages::TITLE;
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
#[template(path = "auth/register/index.html")] #[template(path = "auth/register/index.html")]
struct IndexPage<'a> { struct IndexPage<'a> {
@ -28,7 +30,7 @@ struct IndexPage<'a> {
impl<'a> Default for IndexPage<'a> { impl<'a> Default for IndexPage<'a> {
fn default() -> Self { fn default() -> Self {
IndexPage { IndexPage {
name: "mCaptcha", name: TITLE,
title: "Join", title: "Join",
} }
} }

View file

@ -21,6 +21,8 @@ mod auth;
mod panel; mod panel;
pub mod routes; pub mod routes;
pub const TITLE: &str = "mCaptcha";
pub fn services(cfg: &mut ServiceConfig) { pub fn services(cfg: &mut ServiceConfig) {
auth::services(cfg); auth::services(cfg);
panel::services(cfg); panel::services(cfg);
@ -59,7 +61,11 @@ mod tests {
) )
.await; .await;
let urls = vec![PAGES.home, PAGES.panel.sitekey.add]; let urls = vec![
PAGES.home,
PAGES.panel.sitekey.add,
PAGES.panel.sitekey.list,
];
for url in urls.iter() { for url in urls.iter() {
let resp = let resp =

View file

@ -18,6 +18,8 @@
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use crate::pages::TITLE;
pub mod sitekey; pub mod sitekey;
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
@ -27,13 +29,13 @@ pub struct IndexPage<'a> {
pub title: &'a str, pub title: &'a str,
} }
const TITLE: &str = "Dashboard"; const COMPONENT: &str = "Dashboard";
impl<'a> Default for IndexPage<'a> { impl<'a> Default for IndexPage<'a> {
fn default() -> Self { fn default() -> Self {
IndexPage { IndexPage {
name: "mCaptcha", name: TITLE,
title: TITLE, title: COMPONENT,
} }
} }
} }

View file

@ -18,6 +18,8 @@
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use crate::pages::TITLE;
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
#[template(path = "panel/add-site-key/index.html")] #[template(path = "panel/add-site-key/index.html")]
pub struct IndexPage<'a> { pub struct IndexPage<'a> {
@ -28,13 +30,13 @@ pub struct IndexPage<'a> {
pub form_description: &'a str, pub form_description: &'a str,
} }
const TITLE: &str = "Add Site Key"; const COMPONENT: &str = "Add Site Key";
impl<'a> Default for IndexPage<'a> { impl<'a> Default for IndexPage<'a> {
fn default() -> Self { fn default() -> Self {
IndexPage { IndexPage {
name: "mCaptcha", name: TITLE,
title: TITLE, title: COMPONENT,
levels: 1, levels: 1,
form_description: "", form_description: "",
form_title: "Add Site Key", form_title: "Add Site Key",

View file

@ -0,0 +1,44 @@
/*
* 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::{HttpResponse, Responder};
use sailfish::TemplateOnce;
#[derive(TemplateOnce, Clone)]
#[template(path = "panel/site-keys/index.html")]
pub struct IndexPage<'a> {
pub name: &'a str,
pub title: &'a str,
}
const TITLE: &str = "Add Site Key";
impl<'a> Default for IndexPage<'a> {
fn default() -> Self {
IndexPage {
name: "mCaptcha",
title: TITLE,
}
}
}
pub async fn list_sitekeys() -> impl Responder {
let body = IndexPage::default().render_once().unwrap();
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(body)
}

View file

@ -16,6 +16,7 @@
*/ */
mod add; mod add;
mod list;
pub mod routes { pub mod routes {
pub struct Sitekey { pub struct Sitekey {
@ -43,4 +44,11 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
Methods::ProtectGet, Methods::ProtectGet,
add::add_sitekey add::add_sitekey
); );
define_resource!(
cfg,
PAGES.panel.sitekey.list,
Methods::ProtectGet,
list::list_sitekeys
);
} }

View file

@ -180,7 +180,7 @@ pub async fn add_levels_util(
name: &str, name: &str,
password: &str, password: &str,
) -> (data::Data, Login, ServiceResponse, MCaptchaDetails) { ) -> (data::Data, Login, ServiceResponse, MCaptchaDetails) {
let (data, creds, signin_resp, token_key) = add_token_util(name, password).await; let (data, creds, signin_resp) = signin(name, password).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
@ -188,7 +188,6 @@ pub async fn add_levels_util(
let add_level = AddLevels { let add_level = AddLevels {
levels: levels.clone(), levels: levels.clone(),
key: token_key.key.clone(),
}; };
// 1. add level // 1. add level
@ -200,6 +199,7 @@ pub async fn add_levels_util(
) )
.await; .await;
assert_eq!(add_token_resp.status(), StatusCode::OK); assert_eq!(add_token_resp.status(), StatusCode::OK);
let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
(data, creds, signin_resp, token_key) (data, creds, signin_resp, token_key)
} }

View file

@ -1,6 +1,24 @@
/*
* 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/>.
*/
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: Arial, Helvetica, sans-serif;
} }
a { a {

View file

@ -1,6 +1,26 @@
/*
* 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/>.
*/
$green: #5cad66; $green: #5cad66;
$violet: #800080; $violet: #800080;
$light-violet: #993299;
$backdrop: #f0f0f0; $backdrop: #f0f0f0;
$light-text: rgba(255, 255, 255, 0.87); $light-text: rgba(255, 255, 255, 0.87);
$secondary-backdrop: #2b2c30; $secondary-backdrop: #2b2c30;
$light-grey: rgba(0, 0, 0, 0.125); $light-grey: rgba(0, 0, 0, 0.125);
$white: #fff;
$form-content-width: 90%;

View file

@ -1,3 +1,20 @@
/*
* 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 '../reset'; @import '../reset';
@import '../vars'; @import '../vars';

View file

@ -12,6 +12,7 @@
type="text" type="text"
name="username" name="username"
required="" required=""
autofocus="true"
/> />
</label> </label>
@ -30,9 +31,7 @@
> >
--> -->
</label> </label>
<button class="form__submit-button" type="submit"> <input type="submit" class="form__submit-button" value="Sign in" />
Submit
</button>
</form> </form>
<div class="form__secondary-action"> <div class="form__secondary-action">
<p class="form__secondary-action__banner"> <p class="form__secondary-action__banner">

View file

@ -15,51 +15,51 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ROUTES from '../../api/v1/routes';
import VIEWS from '../../views/v1/routes'; import VIEWS from '../../views/v1/routes';
import isBlankString from '../../utils/isBlankString'; import isBlankString from '../../utils/isBlankString';
import genJsonPayload from '../../utils/genJsonPayload'; import genJsonPayload from '../../utils/genJsonPayload';
import getFormUrl from '../../utils/getFormUrl';
//import '../forms.scss'; //import '../forms.scss';
const login = (e: Event) => { const login = async (e: Event) => {
e.preventDefault(); e.preventDefault();
InputEvent const usernameElement = <HTMLInputElement>document.getElementById('username');
const usernameElement: HTMLInputElement = <HTMLInputElement>document.getElementById('username');
if (usernameElement === null) { if (usernameElement === null) {
console.debug("Username element is null"); console.debug('Username element is null');
return; return;
} }
let username = usernameElement.value; const username = usernameElement.value;
isBlankString(e, username, 'username'); isBlankString(username, 'username', e);
// isBlankString(e);//, username, 'username');
const passwordElement: HTMLInputElement = <HTMLInputElement>document.getElementById('password'); const passwordElement = <HTMLInputElement>document.getElementById('password');
if (passwordElement === null) { if (passwordElement === null) {
console.debug("Password is null"); console.debug('Password is null');
return; return;
} }
let password = passwordElement.value; const password = passwordElement.value;
let payload = { const payload = {
username, username,
password, password,
}; };
fetch(ROUTES.loginUser, genJsonPayload(payload)).then(res => { const formUrl = getFormUrl(null);
const res = await fetch(formUrl, genJsonPayload(payload));
if (res.ok) { if (res.ok) {
alert('success'); alert('success');
window.location.assign(VIEWS.panelHome); window.location.assign(VIEWS.panelHome);
} else { } else {
res.json().then(err => alert(`error: ${err.error}`)); const err = await res.json();
alert(`error: ${err.error}`);
} }
});
}; };
export const index = () => { export const index = () => {
let form = <HTMLFontElement>document.getElementById('form'); const form = <HTMLFontElement>document.getElementById('form');
form.addEventListener('submit', login, true); form.addEventListener('submit', login, true);
}; };

View file

@ -3,7 +3,7 @@
<img src="<.= crate::FILES.get("./static-assets/img/icon-trans.png").unwrap().>" class="form__logo" alt="" /> <img src="<.= crate::FILES.get("./static-assets/img/icon-trans.png").unwrap().>" class="form__logo" alt="" />
<h2 class="form__brand">Join mCaptcha</h2> <h2 class="form__brand">Join mCaptcha</h2>
<form method="POST" action="<.= crate::V1_API_ROUTES.auth.register .> class="form__box" id="form"> <form method="POST" action="<.= crate::V1_API_ROUTES.auth.register .>" class="form__box" id="form">
<label class="form__in-group" for="username" <label class="form__in-group" for="username"
>Username >Username
<input <input
@ -13,6 +13,7 @@
name="username" name="username"
id="username" id="username"
required required
autofocus="true"
/> />
</label> </label>
@ -50,9 +51,7 @@
required required
/> />
</label> </label>
<button class="form__submit-button" type="submit"> <input type="submit" class="form__submit-button" value="Sign up" />
Submit
</button>
</form> </form>
<div class="form__secondary-action"> <div class="form__secondary-action">
<p class="form__secondary-action__banner"> <p class="form__secondary-action__banner">

View file

@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ROUTES from '../../api/v1/routes';
import VIEWS from '../../views/v1/routes'; import VIEWS from '../../views/v1/routes';
import isBlankString from '../../utils/isBlankString'; import isBlankString from '../../utils/isBlankString';
@ -23,6 +22,7 @@ import genJsonPayload from '../../utils/genJsonPayload';
import userExists from './userExists'; import userExists from './userExists';
import {checkEmailExists} from './emailExists'; import {checkEmailExists} from './emailExists';
import getFormUrl from '../../utils/getFormUrl';
//import '../forms.scss'; //import '../forms.scss';
@ -33,15 +33,15 @@ const passwordElement = <HTMLInputElement>document.getElementById('password');
const registerUser = async (e: Event) => { const registerUser = async (e: Event) => {
e.preventDefault(); e.preventDefault();
let username = usernameElement.value; const username = usernameElement.value;
isBlankString(e, username, 'username'); isBlankString(username, 'username', e);
//isBlankString(e);//, username, 'username'); //isBlankString(e);//, username, 'username');
let password = passwordElement.value; const password = passwordElement.value;
const passwordCheckElement = <HTMLInputElement>( const passwordCheckElement = <HTMLInputElement>(
document.getElementById('password-check') document.getElementById('password-check')
); );
let passwordCheck = passwordCheckElement.value; const passwordCheck = passwordCheckElement.value;
if (password != passwordCheck) { if (password != passwordCheck) {
return alert("passwords don't match, check again!"); return alert("passwords don't match, check again!");
} }
@ -61,25 +61,26 @@ const registerUser = async (e: Event) => {
} }
} }
let payload = { const payload = {
username, username,
password, password,
confirm_password: passwordCheck, confirm_password: passwordCheck,
email, email,
}; };
const formUrl = getFormUrl(null);
let res = await fetch(ROUTES.registerUser, genJsonPayload(payload)); const res = await fetch(formUrl, genJsonPayload(payload));
if (res.ok) { if (res.ok) {
alert('success'); alert('success');
window.location.assign(VIEWS.loginUser); window.location.assign(VIEWS.loginUser);
} else { } else {
let err = await res.json(); const err = await res.json();
alert(`error: ${err.error}`); alert(`error: ${err.error}`);
} }
}; };
export const index = () => { export const index = () => {
let form = <HTMLFontElement>document.getElementById('form'); const form = <HTMLFontElement>document.getElementById('form');
form.addEventListener('submit', registerUser, true); form.addEventListener('submit', registerUser, true);
usernameElement.addEventListener('input', userExists, false); usernameElement.addEventListener('input', userExists, false);
}; };

View file

@ -0,0 +1,33 @@
/*
* 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/>.
*/
@mixin violet-button-hover {
background-color: $light-violet;
cursor: grab;
transform: translateY(-5px);
}
@mixin violet-button {
background-color: $violet;
color: $light-text;
border-radius: 5px;
border: 1px $light-grey solid;
padding: 5px;
font-size: 20px;
min-height: 45px;
width: 125px;
}

View file

@ -0,0 +1,33 @@
/*
* 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 '../vars';
@mixin form-label {
margin: 10px 0;
box-sizing: border-box;
width: $form-content-width;
justify-self: left;
}
@mixin form-input {
position: relative;
margin-top: 5px;
box-sizing: border-box;
height: 40px;
width: 90%;
}

View file

@ -21,9 +21,17 @@ import * as login from './auth/login';
import * as register from './auth/register'; import * as register from './auth/register';
import * as panel from './panel/index'; import * as panel from './panel/index';
import * as addSiteKey from './panel/add-site-key/'; import * as addSiteKey from './panel/add-site-key/';
import VIEWS from './views/v1/routes';
import './auth/forms.scss'; import './auth/forms.scss';
import './panel/main.scss'; import './panel/main.scss';
import VIEWS from './views/v1/routes'; import './panel/header/sidebar/main.scss';
import './panel/taskbar/main.scss';
import './panel/help-banner/main.scss';
import './panel/add-site-key/main.scss';
const router = new Router(); const router = new Router();

View file

@ -1,17 +1,31 @@
<div class="sitekey-form__add-level-flex-container"> <fieldset class="sitekey-form__level">
<label class="sitekey-form__label" for="level2">Level <.= level .></label> <legend>Level <.= level .></legend>
</div> <label class="sitekey-form__level-fieldname" for="visitor<.= level .>"
>Visitor
<div class="sitekey-form__add-level-flex-container">
<input <input
class="sitekey-form__input--add-level" class="sitekey-form__number-filed"
type="number" type="number"
id="level<.= level .>" name=""
name="level<.= level .>"
<. if level == 1 { .>
<.= "required" .>
<. } .>
value="" value=""
id="visitor<.= level .>"
/> />
<button class="sitekey-form__add-level-button">Add Level</button> </label>
</div>
<label class="sitekey-form__level-fieldname" for="difficulty<.= level .>">
Difficulty
<input
type="number"
name="difficulty"
class="sitekey-form__number-filed"
value=""
id="difficulty<.= level .>"
/>
</label>
<input
class="sitekey-form__add-level-button"
type="button"
name="add"
id="add"
value="Add"
/>
</fieldset>

View file

@ -14,124 +14,91 @@
* 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/>.
*/ */
import VALIDATE_LEVELS from './levels'; import getNumLevels from './levels/getNumLevels';
import isBlankString from '../../utils/isBlankString'; import validateLevel from './levels/validateLevel';
import isNumber from '../../utils/isNumber'; import * as UpdateLevel from './levels/updateLevel';
const LABEL_CONTAINER_CLASS = 'sitekey-form__add-level-flex-container'; const ADD_LEVEL_BUTTON = 'sitekey-form__level-add-level-button';
const LABEL_CLASS = 'sitekey-form__label';
export const LABEL_INNER_TEXT_WITHOUT_LEVEL = 'Level ';
export const INPUT_ID_WITHOUT_LEVEL = 'level'; /**
const INPUT_CLASS = 'sitekey-form__input--add-level'; * Gets executed when 'Add' Button is clicked to add levels
* Used to validate levels per m_captcha::defense::Defense's
* specifications
*/
const addLevel = (e: Event) => {
const eventTarget = <HTMLElement>e.target;
const PARENT = <HTMLElement>eventTarget.parentElement;
const FIELDSET = <HTMLElement>PARENT.parentElement;
const numLevels = getNumLevels();
const ADD_LEVEL_BUTTON_INNER_TEXT = 'Add Level'; const isValid = validateLevel(numLevels);
const ADD_LEVEL_BUTTON = 'sitekey-form__add-level-button';
export const getNumLevels = () => {
let numLevels = 0;
document.querySelectorAll(`.${LABEL_CLASS}`).forEach(_ => numLevels++);
return numLevels;
};
const validateLevel = (numLevels: number) => {
numLevels = numLevels - 1;
let inputID = INPUT_ID_WITHOUT_LEVEL + numLevels.toString();
let filed = LABEL_INNER_TEXT_WITHOUT_LEVEL + numLevels;
let inputElement = <HTMLInputElement>document.getElementById(inputID);
let val = inputElement.value;
if (!isNumber(val)) {
return false;
}
let level = parseInt(val);
if (Number.isNaN(level)) {
alert('Level can contain nubers only');
return false;
}
let e = null;
console.log(level);
isBlankString(e, val, filed);
let isValid = VALIDATE_LEVELS.add(level);
return isValid;
};
const addLevelButtonEventHandler = (e: Event) => {
let eventTarget = <HTMLElement>e.target;
// if (!eventTarget) {
// return;
// }
const PREV_LEVEL_CONTAINER = <HTMLElement>eventTarget.parentElement;
let numLevels: string | number = getNumLevels();
let isValid = validateLevel(numLevels);
console.log(`[addLevelButton] isValid: ${isValid}`); console.log(`[addLevelButton] isValid: ${isValid}`);
if (!isValid) { if (!isValid) {
return console.log('Aborting level addition'); return console.error('Aborting level addition');
} }
eventTarget.remove(); PARENT.remove();
numLevels = numLevels.toString(); const newLevelHTML = getHtml(numLevels + 1);
FIELDSET.insertAdjacentHTML('afterend', newLevelHTML);
let labelContainer = document.createElement('div'); UpdateLevel.register(numLevels);
labelContainer.className = LABEL_CONTAINER_CLASS;
let inputID = INPUT_ID_WITHOUT_LEVEL + numLevels;
let label = document.createElement('label');
label.className = LABEL_CLASS;
label.htmlFor = inputID;
label.innerText = LABEL_INNER_TEXT_WITHOUT_LEVEL + numLevels;
labelContainer.appendChild(label);
PREV_LEVEL_CONTAINER.insertAdjacentElement('afterend', labelContainer);
let inputContainer = document.createElement('div');
inputContainer.className = LABEL_CONTAINER_CLASS;
let input = document.createElement('input');
input.id = inputID;
input.name = inputID;
input.type = 'text';
input.className = INPUT_CLASS;
inputContainer.appendChild(input);
let button = document.createElement('button');
button.className = ADD_LEVEL_BUTTON;
button.innerText = ADD_LEVEL_BUTTON_INNER_TEXT;
inputContainer.appendChild(button);
labelContainer.insertAdjacentElement('afterend', inputContainer);
addLevelButtonAddEventListener(); addLevelButtonAddEventListener();
}; };
export const addLevelButtonAddEventListener = () => { /** adds onclick event listener */
const addLevelButtonAddEventListener = () => {
let addLevelButton = <HTMLElement>( let addLevelButton = <HTMLElement>(
document.querySelector(`.${ADD_LEVEL_BUTTON}`) document.querySelector(`.${ADD_LEVEL_BUTTON}`)
); );
addLevelButton.addEventListener('click', addLevelButtonEventHandler); addLevelButton.addEventListener('click', addLevel);
}; };
/* /**
<div class="sitekey-form__add-level-flex-container"> * Generate HTML to be added when 'Add Level' button is clicked
<label class="sitekey-form__label" for="level2">Level 2</label> * Check if './add-level.html` to see if this is up to date
</div> */
const getHtml = (level: number) => {
<div class="sitekey-form__add-level-flex-container"> console.debug(`[generating HTML getHtml]level: ${level}`);
<input const HTML = `
class="sitekey-form__input--add-level" <fieldset class="sitekey__level-container">
type="text" <legend class="sitekey__level-title">
name="level2" Level ${level}
id="level2" </legend>
<label class="sitekey-form__level-label" for="visitor${level}"
>Visitor
<input
class="sitekey-form__level-input"
type="number"
name=""
value="" value=""
/> id="visitor${level}"
<button class="sitekey-form__add-level-button">Add Level</button> />
</div> </label>
*/
<label class="sitekey-form__level-label" for="difficulty${level}">
Difficulty
<input
type="number"
name="difficulty"
class="sitekey-form__level-input"
value=""
id="difficulty${level}"
/>
</label>
<label class="sitekey-form__level-label--hidden" for="add">
Add level
<input
class="sitekey-form__level-add-level-button"
type="button"
name="add"
id="add"
value="Add"
/>
</label>
</fieldset>
`;
return HTML;
};
export default addLevelButtonAddEventListener;

View file

@ -0,0 +1,35 @@
/*
* 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/>.
*/
const LABEL_INNER_TEXT_WITHOUT_LEVEL = 'Level ';
const INPUT_ID_WITHOUT_LEVEL = 'level';
const LABEL_CLASS = 'sitekey-form__label';
const VISITOR_WITHOUT_LEVEL = 'visitor';
const DIFFICULTY_WITHOUT_LEVEL = 'difficulty';
const LEVEL_CONTAINER_CLASS = "sitekey__level-container";
const CONST = {
LABEL_CLASS,
INPUT_ID_WITHOUT_LEVEL,
LABEL_INNER_TEXT_WITHOUT_LEVEL,
VISITOR_WITHOUT_LEVEL,
DIFFICULTY_WITHOUT_LEVEL,
LEVEL_CONTAINER_CLASS,
}
export default CONST;

View file

@ -1,6 +1,6 @@
<div class="sitekey-form__add-level-flex-container"> <div class="sitekey-form__add-level-flex-container">
<!-- insert Javascript for adding more levels as needed --> <!-- insert Javascript for adding more levels as needed -->
<label class="sitekey-form__label" for="level1">Level <.= level .></label> <label class="sitekey-form__label" for="level<.= level .>">Level <.= level .></label>
</div> </div>
<input <input

View file

@ -1,10 +1,9 @@
<form class="sitekey-form" action="/something" method="post"> <form class="sitekey-form" action="<.= crate::V1_API_ROUTES.levels.add .>" method="post">
<div class="sitekey-form__title-flex-container"> <h1 class="form__title">
<b class="sitekey-form__title"><.= form_title .></b> <.= form_title .>
</div> </h1>
<div class="sitekey-form__add-level-flex-container"> <label class="sitekey-form__label" for="description">
<label class="sitekey-form__label" for="description">Description</label> Description
</div>
<input <input
class="sitekey-form__input" class="sitekey-form__input"
type="text" type="text"
@ -13,10 +12,11 @@
required required
value="<.= form_description .>" value="<.= form_description .>"
/> />
</label>
<. for level in 1..=levels { .> <. for level in 1..=levels { .>
<. if level == levels { .> <. if level == levels { .>
<. include!("./add-level.html"); .> <. include!("./new-add-level.html"); .>
<. } else { .> <. } else { .>
<. include!("./existing-level.html"); .> <. include!("./existing-level.html"); .>
<. } .> <. } .>

View file

@ -15,43 +15,71 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import CONST from './const';
import getNumLevels from './levels/getNumLevels';
import isBlankString from '../../utils/isBlankString';
import getFormUrl from '../../utils/getFormUrl';
import genJsonPayload from '../../utils/genJsonPayload';
import {LEVELS} from './levels';
import VIEWS from '../../views/v1/routes';
const SITE_KEY_FORM_CLASS = 'sitekey-form'; const SITE_KEY_FORM_CLASS = 'sitekey-form';
const FORM = <HTMLFormElement>document.querySelector(`.${SITE_KEY_FORM_CLASS}`); const FORM = <HTMLFormElement>document.querySelector(`.${SITE_KEY_FORM_CLASS}`);
//const FORM_SUBMIT_BUTTON_CLASS = "sitekey-form__submit";
//const FORM_SUBMIT_BUTTON = <HTMLButtonElement>document.querySelector(`.${FORM_SUBMIT_BUTTON_CLASS}`);
import * as addLevelButton from './addLevelButton'; const addSubmitEventListener = () => {
import isBlankString from '../../utils/isBlankString';
export const addSubmitEventListener = () => {
FORM.addEventListener('submit', submit, true); FORM.addEventListener('submit', submit, true);
}; };
const validateLevels = (e: Event) => { //const validateLevels = (e: Event) => {
let numLevels = addLevelButton.getNumLevels(); // const numLevels = getNumLevels();
// check if levels are unique and are in increasing order; // // check if levels are unique and are in increasing order;
// also if they are positive // // also if they are positive
// also if level input field is accompanied by a "Add Level" button, // // also if level input field is accompanied by a "Add Level" button,
// it shouldn't be used for validation // // it shouldn't be used for validation
for (let levelNum = 1; levelNum < numLevels; levelNum++) { // for (let levelNum = 1; levelNum < numLevels; levelNum++) {
let inputID = addLevelButton.INPUT_ID_WITHOUT_LEVEL + levelNum; // const inputID = CONST.INPUT_ID_WITHOUT_LEVEL + levelNum;
let inputElement = <HTMLInputElement>document.getElementById(inputID); // const inputElement = <HTMLInputElement>document.getElementById(inputID);
let val = inputElement.value; // const val = inputElement.value;
let filed = addLevelButton.LABEL_INNER_TEXT_WITHOUT_LEVEL + levelNum; // const filed = CONST.LABEL_INNER_TEXT_WITHOUT_LEVEL + levelNum;
isBlankString(e, val, filed); // isBlankString(val, filed, e);
} // }
} //};
const validateDescription = (e: Event) => { const validateDescription = (e: Event) => {
let inputElement = <HTMLInputElement>document.getElementById("description"); const inputElement = <HTMLInputElement>document.getElementById('description');
let val = inputElement.value; const val = inputElement.value;
let filed = "Description"; const filed = 'Description';
isBlankString(e, val, filed); isBlankString(val, filed, e);
} };
const submit = async (e: Event) => { const submit = async (e: Event) => {
e.preventDefault();
validateDescription(e); validateDescription(e);
validateLevels(e); // validateLevels(e);
// get values
// check validate levels const formUrl = getFormUrl(FORM);
// submit
// handle erros const levels = LEVELS.getLevels();
console.debug(`[form submition]: levels: ${levels}`);
const payload = {
levels: levels,
};
console.debug(`[form submition] json payload: ${JSON.stringify(payload)}`);
const res = await fetch(formUrl, genJsonPayload(payload));
if (res.ok) {
alert('success');
window.location.assign(VIEWS.sitekey);
} else {
const err = await res.json();
alert(`error: ${err.error}`);
}
}; };
export default addSubmitEventListener;

View file

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as addLevelButton from './addLevelButton'; import addLevelButtonAddEventListener from './addLevelButton';
import * as addLevelForm from './form'; import addSubmitEventListener from './form';
export const index = () => { export const index = () => {
addLevelButton.addLevelButtonAddEventListener(); addLevelButtonAddEventListener();
addLevelForm.addSubmitEventListener(); addSubmitEventListener();
}; };

View file

@ -1,61 +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/>.
*/
const VALIDATE_LEVELS = (function() {
const levels: Array<number> = [];
const checkAscendingOrder = (newLevel: number) => {
if (levels.length == 0) {
return true;
}
let isValid = true;
levels.find(level => {
if (level > newLevel) {
alert(
`Level: ${newLevel} has to greater than previous levels ${level}`,
);
isValid = false;
return true;
}
return false;
});
return isValid;
};
return {
add: function(newLevel: number) {
console.log(`[levels.js]levels: ${levels} newLevel: ${newLevel}`);
if (levels.find(level => level == newLevel)) {
alert(`Level: ${newLevel} has to be unique`);
return false;
}
let isValid = checkAscendingOrder(newLevel);
if (isValid) {
levels.push(newLevel);
return true;
}
console.log(
`Ascending arder failure. Levels: ${levels}, levels length: ${levels.length}`,
);
return false;
},
}; })();
export default VALIDATE_LEVELS;

View file

@ -0,0 +1,57 @@
/*
* 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 {Level} from './index';
import CONST from '../const';
import isNumber from '../../../utils/isNumber';
/** Fetches level from DOM using the ID passesd and validates */
const getLevelFields = (id: number) => {
console.log(`[getLevelFields]: id: ${id}`);
const visitorID = CONST.VISITOR_WITHOUT_LEVEL + id.toString();
const difficultyID = CONST.DIFFICULTY_WITHOUT_LEVEL + id.toString();
const visitorElement = <HTMLInputElement>document.getElementById(visitorID);
const difficultyElement = <HTMLInputElement>(
document.getElementById(difficultyID)
);
const visitor_threshold = parseInt(visitorElement.value);
const difficulty_factor = parseInt(difficultyElement.value);
if (!isNumber(visitor_threshold) || Number.isNaN(visitor_threshold)) {
throw new Error('visitor can contain nubers only');
}
if (!isNumber(difficulty_factor) || Number.isNaN(difficulty_factor)) {
throw new Error('difficulty can contain nubers only');
}
const level: Level = {
difficulty_factor,
visitor_threshold,
};
console.debug(
`[getLevelFields.ts] visitor: ${visitor_threshold} difficulty: ${difficulty_factor}`,
);
return level;
};
export default getLevelFields;

View file

@ -0,0 +1,29 @@
/*
* 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 CONST from '../const';
/** returns number of level input fields currently in DOM */
const getNumLevels = () => {
let numLevels = 0;
document.querySelectorAll(`.${CONST.LEVEL_CONTAINER_CLASS}`).forEach(_ => numLevels++);
console.debug(`[getNumLevels]: numLevels: ${numLevels}`);
return numLevels;
};
export default getNumLevels;

View file

@ -0,0 +1,105 @@
/*
* 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/>.
*/
/** Datatype represenging an mCaptcha level */
export type Level = {
difficulty_factor: number;
visitor_threshold: number;
};
/** Datatype representing a collection of mCaptcha levels */
class Levels {
levels: Array<Level>;
constructor() {
this.levels = [];
}
add = (newLevel: Level) => {
console.debug(`[levels/index.ts] levels lenght: ${this.levels.length}`);
if (newLevel.difficulty_factor <= 0) {
throw new Error('Difficulty must be greater than zero');
}
if (newLevel.visitor_threshold <= 0) {
throw new Error('Visitors must be graeter than zero');
}
if (this.levels.length == 0) {
this.levels.push(newLevel);
return true;
}
let msg;
let count = 1;
const validate = (level: Level, newLevel: Level) => {
if (level.visitor_threshold >= newLevel.visitor_threshold) {
msg = `Level: ${newLevel} visitor count has to greater than previous levels. See ${count}`;
return true;
}
if (level.difficulty_factor >= newLevel.difficulty_factor) {
msg = `Level ${this.levels.length} difficulty has to greater than previous levels See ${count}`;
return true;
}
count++;
return false;
};
if (this.levels.find(level => validate(level, newLevel))) {
alert(msg);
throw new Error(msg);
} else {
this.levels.push(newLevel);
}
};
get = () => this.levels;
}
/** Singleton that does manipulations on Levels object */
export const LEVELS = (function() {
const levels = new Levels();
return {
/** get levels */
getLevels: () => levels.get(),
/** add new level */
add: (newLevel: Level) => levels.add(newLevel),
/** update levels */
update: (updateLevel: Level, id: number) => {
const tmpLevel = new Levels();
id -= 1;
try {
for (let i = 0; i < levels.levels.length; i++) {
if (id == i) {
tmpLevel.add(updateLevel);
} else {
tmpLevel.add(levels.levels[i]);
}
}
return true;
} catch (e) {
return false;
}
},
};
})();

View file

@ -0,0 +1,64 @@
/*
* 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 CONST from '../const';
import getLevelFields from './getLevelFields';
import {LEVELS} from './index';
/** on-change event handler to update level */
const updateLevel = (e: Event) => {
const target = <HTMLInputElement>e.target;
const id = target.id;
let level;
if (id.includes(CONST.VISITOR_WITHOUT_LEVEL)) {
level = id.slice(CONST.VISITOR_WITHOUT_LEVEL.length);
} else if (id.includes(CONST.DIFFICULTY_WITHOUT_LEVEL)) {
level = id.slice(CONST.DIFFICULTY_WITHOUT_LEVEL.length);
} else {
throw new Error(
'update event was triggered by some element other than difficulty or visitor',
);
}
level = parseInt(level);
if (Number.isNaN(level)) {
console.error(`[updateLevel.ts] level # computed is not correct, got NaN`);
}
try {
const updatedLevel = getLevelFields(level);
LEVELS.update(updatedLevel, level);
} catch (e) {
alert(e);
}
};
/** registers on-change event handlers to update levels */
export const register = (id: number) => {
const visitorID = CONST.VISITOR_WITHOUT_LEVEL + id.toString();
const difficultyID = CONST.DIFFICULTY_WITHOUT_LEVEL + id.toString();
const visitorElement = <HTMLInputElement>document.getElementById(visitorID);
const difficultyElement = <HTMLInputElement>(
document.getElementById(difficultyID)
);
visitorElement.addEventListener('input', updateLevel, false);
difficultyElement.addEventListener('input', updateLevel, false);
};

View file

@ -0,0 +1,40 @@
/*
* 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 {LEVELS} from './index';
import getLevelFields from './getLevelFields';
/**
* Fetches level from DOM using the ID passesd and validates
* its contents
* */
const validateLevel = (id: number) => {
const level = getLevelFields(id);
if (level === null) {
return false;
}
try {
LEVELS.add(level);
return true;
} catch (e) {
return false;
}
};
export default validateLevel;

View file

@ -0,0 +1,113 @@
/*
* 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 '../../reset';
@import '../../vars';
@import '../../components/button';
@import '../../components/forms';
.sitekey-form {
display: flex;
flex-direction: column;
width: 90%;
justify-content: center;
align-items: center;
box-sizing: content-box;
background-color: $white;
margin: auto;
padding-bottom: 30px;
}
.form__title-flex-container {
display: flex;
width: 100%;
border-bottom: 0.1px solid $light-grey;
}
.form__title {
padding-left: 10px;
font-size: 1rem;
padding: 0.75rem 1.25rem;
box-sizing: border-box;
text-align: left;
width: 100%;
border-bottom: 0.1px solid $light-grey;
}
.sitekey-form__label {
@include form-label;
}
.sitekey-form__input {
@include form-input;
width: 100%;
}
// level styling
.sitekey__level-container {
width: $form-content-width;
box-sizing: border-box;
display: flex;
}
.sitekey__level-title {
margin-bottom: 10px;
margin-top: 5px;
}
.sitekey-form__level-label {
@include form-label;
font-size: 0.9rem;
}
.sitekey-form__level-name {
@include form-label;
display: block;
}
.sitekey-form__level-input {
@include form-input;
flex: 2;
}
.sitekey-form__level-add-level-button {
@include violet-button;
}
.sitekey-form__level-add-level-button:hover {
@include violet-button-hover;
}
.sitekey-form__level-label--hidden {
@include form-label;
color: $white;
flex: 1;
}
// level styling ends
.sitekey-form__submit {
@include violet-button;
display: block;
margin-top: 50px;
width: $form-content-width;
width: 90%;
}
.sitekey-form__submit:hover {
@include violet-button-hover;
}

View file

@ -0,0 +1,36 @@
<fieldset class="sitekey__level-container">
<legend class="sitekey__level-title">
Level <.= level .>
</legend>
<label class="sitekey-form__level-label" for="visitor<.= level .>"
>Visitor
<input
class="sitekey-form__level-input"
type="number"
name=""
value=""
id="visitor<.= level .>"
/>
</label>
<label class="sitekey-form__level-label" for="difficulty<.= level .>">
Difficulty
<input
type="number"
name="difficulty"
class="sitekey-form__level-input"
value=""
id="difficulty<.= level .>"
/>
</label>
<label class="sitekey-form__level-label--hidden" for="add">
Add level
<input
class="sitekey-form__level-add-level-button"
type="button"
name="add"
id="add"
value="Add"
/>
</label>
</fieldset>

View file

@ -0,0 +1,17 @@
/*
* 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 './main.scss';

View file

@ -0,0 +1,88 @@
/*
* 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 '../../../reset';
@import '../../../vars';
.secondary-menu {
position: fixed;
width: 14%;
left: 0;
bottom: 0;
top: 0;
right: 0;
height: 100%;
overflow: auto;
background-color: $secondary-backdrop;
color: $light-text;
}
.secondary-menu__heading {
margin: auto;
padding: 20px 5px;
display: flex;
}
.secondary-menu__heading:hover {
color: $green;
cursor: grab;
}
.secondary-menu__logo {
width: 70px;
display: inline-block;
}
.secondary-menu__brand-name {
display: inline-block;
margin: auto;
font-size: 1.5rem;
}
.secondary-menu__item {
margin: auto;
padding: 20px 25px;
display: flex;
}
.secondary-menu__icon {
filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(93deg) brightness(103%)
contrast(103%);
opacity: 0.8;
width: 1rem;
margin: auto;
margin-right: 10px;
}
.secondary-menu__item-name {
display: inline-block;
margin: auto;
font-size: 1rem;
}
.secondary-menu__item-link:hover {
color: $green;
cursor: grab;
filter: invert(58%) sepia(60%) saturate(331%) hue-rotate(76deg)
brightness(91%) contrast(92%);
}
.secondary-menu__item-link {
color: inherit;
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,50 @@
/*
* 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 '../../reset';
@import '../../vars';
.help-text {
border-radius: 4px;
box-shadow: $secondary-backdrop 0px 2px 6px 0px;
min-width: 70%;
max-width: 80%;
min-height: 70px;
display: flex;
margin-left: 15px;
margin-top: 40px;
}
.help-text__serial-num {
display: inline-flex;
background-color: $violet;
color: $light-text;
width: 30px;
height: 30px;
border-radius: 50%;
align-items: center;
justify-content: center;
}
.help-text__instructions {
display: inline-block;
list-style: none;
font-size: 19px;
font-weight: 500;
padding: 10px;
margin: auto;
}

View file

@ -1,74 +1,23 @@
/*
* 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 '../reset'; @import '../reset';
@import '../vars'; @import '../vars';
@import '../components/button';
.secondary-menu {
position: fixed;
width: 14%;
left: 0;
bottom: 0;
top: 0;
right: 0;
height: 100%;
overflow: auto;
background-color: $secondary-backdrop;
color: $light-text;
}
.secondary-menu__heading {
margin: auto;
padding: 20px 5px;
display: flex;
}
.secondary-menu__heading:hover {
color: $green;
cursor: grab;
}
.secondary-menu__logo {
width: 70px;
display: inline-block;
}
.secondary-menu__brand-name {
display: inline-block;
margin: auto;
font-size: 1.5rem;
}
.secondary-menu__item {
margin: auto;
padding: 20px 25px;
display: flex;
}
.secondary-menu__icon {
filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(93deg) brightness(103%)
contrast(103%);
opacity: 0.8;
width: 1rem;
margin: auto;
margin-right: 10px;
}
.secondary-menu__item-name {
display: inline-block;
margin: auto;
font-size: 1rem;
}
.secondary-menu__item-link:hover {
color: $green;
cursor: grab;
filter: invert(58%) sepia(60%) saturate(331%) hue-rotate(76deg)
brightness(91%) contrast(92%);
}
.secondary-menu__item-link {
color: inherit;
width: 100%;
height: 100%;
}
main { main {
margin-left: 14%; margin-left: 14%;
@ -77,130 +26,6 @@ main {
padding-bottom: 20px; padding-bottom: 20px;
} }
.task-bar {
display: flex;
padding: 0px;
margin: 0px;
background-color: #fff;
}
.task-bar__action {
display: inline-block;
padding: 10px 0px;
margin: auto;
}
.task-bar__spacer {
min-width: 250px;
flex: 6;
}
.task-bar__icon {
opacity: 0.8;
width: 1.5rem;
margin: auto 20px;
}
.task-bar__icon {
color: $light-text;
}
.task-bar__icon:hover {
cursor: grab;
}
.main-menu {
display: flex;
flex-grow: 0;
padding-top: 20px;
padding-left: 10px;
}
.main-menu__item {
list-style: none;
background-color: #c3c3c3;
flex: 2;
text-align: center;
vertical-align: middle;
margin: auto 20px;
padding: 10px 0;
}
.main-menu__item--spacer {
list-style: none;
flex: 3;
text-align: center;
}
.main-menu__item:hover {
background-color: grey;
cursor: grab;
}
.main-menu__item:last-child {
padding: 0;
display: flex;
flex: 2;
border: none;
background-color: unset;
}
.main-menu__item:last-child:hover {
cursor: unset;
background-color: unset;
}
.main-menu__add-site {
display: inline-block;
background-color: $violet;
color: white;
font-weight: 500;
font-size: 16px;
padding: 10px 15px;
border-radius: 5px;
border: 1px grey solid;
min-height: 45px;
margin: auto;
}
.main-menu__add-site:hover {
background-color: $violet;
cursor: grab;
transform: translateY(-5px);
}
.help-text {
border-radius: 4px;
box-shadow: $secondary-backdrop 0px 2px 6px 0px;
min-width: 70%;
max-width: 80%;
min-height: 70px;
display: flex;
margin-left: 15px;
margin-top: 40px;
}
.help-text__serial-num {
display: inline-flex;
background-color: $violet;
color: $light-text;
width: 30px;
height: 30px;
border-radius: 50%;
align-items: center;
justify-content: center;
}
.help-text__instructions {
display: inline-block;
list-style: none;
font-size: 19px;
font-weight: 500;
padding: 10px;
margin: auto;
}
.inner-container { .inner-container {
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
@ -209,86 +34,3 @@ main {
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
} }
.sitekey-form {
display: flex;
flex-direction: column;
width: 90%;
justify-content: center;
align-items: center;
box-sizing: content-box;
background-color: #fff;
margin: auto;
padding-bottom: 30px;
}
.sitekey-form__title-flex-container {
display: flex;
width: 100%;
border-bottom: 0.1px solid $light-grey;
}
.sitekey-form__title {
padding-left: 10px;
font-size: 1rem;
padding: 0.75rem 1.25rem;
}
.sitekey-form__label {
display: block;
margin: 10px 0;
box-sizing: inherit;
justify-self: left;
}
.sitekey-form__input {
position: relative;
margin-top: 5px;
box-sizing: border-box;
height: 40px;
width: 90%;
}
.sitekey-form__input--add-level {
position: relative;
margin-top: 5px;
box-sizing: inherit;
flex: 3;
height: 40px;
margin-right: 20px;
}
.sitekey-form__add-level-flex-container {
display: flex;
box-sizing: border-box;
width: 90%;
margin-top: 10px;
}
.sitekey-form__add-level-button {
background-color: $violet;
color: white;
padding: 5px;
font-size: 16px;
border-radius: 5px;
border: 1px $light-grey solid;
height: 40px;
min-width: 125px;
margin: auto;
}
.sitekey-form__submit {
margin-top: 50px;
display: block;
background-color: $violet;
color: white;
padding: 5px;
font-size: 20px;
border-radius: 5px;
border: 1px $light-grey solid;
min-height: 45px;
width: 125px;
width: 90%;
}

View file

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

View file

@ -1,29 +1,29 @@
<ul class="task-bar"> <ul class="taskbar">
<!-- <!--
<li class="task-bar__action">Brand Name</li> <li class="taskbar__action">Brand Name</li>
--> -->
<li class="task-bar__spacer"></li> <li class="taskbar__spacer"></li>
<li class="task-bar__action"> <li class="taskbar__action">
<a class="task-bar__link" href="<.= crate::PAGES.panel.sitekey.add .>"> <a class="taskbar__link" href="<.= crate::PAGES.panel.sitekey.add .>">
<button class="main-menu__add-site"> <button class="taskbar__add-site">
+ New Site + New Site
</button> </button>
</a> </a>
</li> </li>
<li class="task-bar__action"> <li class="taskbar__action">
<img class="task-bar__icon" src="<.= <img class="taskbar__icon" src="<.=
crate::FILES.get("./static-assets/img/svg/moon.svg").unwrap() .>" alt="Profile" /> crate::FILES.get("./static-assets/img/svg/moon.svg").unwrap() .>" alt="Profile" />
</li> </li>
<li class="task-bar__action"> <li class="taskbar__action">
<img class="task-bar__icon" src="<.= <img class="taskbar__icon" src="<.=
crate::FILES.get("./static-assets/img/svg/bell.svg").unwrap() .>" crate::FILES.get("./static-assets/img/svg/bell.svg").unwrap() .>"
alt="Notifications" /> alt="Notifications" />
</li> </li>
<li class="task-bar__action"> <li class="taskbar__action">
<a href="<.= crate::V1_API_ROUTES.auth.logout .>"> <a href="<.= crate::V1_API_ROUTES.auth.logout .>">
<img class="task-bar__icon" src="<.= <img class="taskbar__icon" src="<.=
crate::FILES.get("./static-assets/img/svg/log-out.svg").unwrap() .>" alt="Profile" crate::FILES.get("./static-assets/img/svg/log-out.svg").unwrap() .>" alt="Profile"
/></a> /></a>
</li> </li>

View file

@ -0,0 +1,61 @@
/*
* 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 '../../reset';
@import '../../vars';
@import '../../components/button';
.taskbar {
display: flex;
padding: 0px;
margin: 0px;
background-color: $white;
}
.taskbar__action {
display: inline-block;
padding: 10px 0px;
margin: auto;
}
.taskbar__spacer {
min-width: 250px;
flex: 6;
}
.taskbar__icon {
opacity: 0.8;
width: 1.5rem;
margin: auto 20px;
}
.taskbar__icon {
color: $light-text;
}
.taskbar__icon:hover {
cursor: grab;
}
.taskbar__add-site {
display: inline-block;
@include violet-button;
}
.taskbar__add-site:hover {
@include violet-button-hover;
}

View file

@ -0,0 +1,43 @@
/*
* 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/>.
*/
/**
* querySelector is the the selector that will be
* used to fetch elements.
* So when using class-names, pass in ".whatever-classname"
* and for ID, "#id".
* */
const getFormUrl = (querySelector: null|string|HTMLFormElement) => {
let form;
if (querySelector === null) {
form = <HTMLFormElement>document.querySelector("form");
}
if (querySelector === "string" || querySelector instanceof String) {
form = <HTMLFormElement>document.querySelector(querySelector.toString());
}
if (querySelector instanceof HTMLFormElement) {
form = querySelector;
}
if ( form !== undefined) {
return form.action
} else {
throw new Error("Can't find form");
}
};
export default getFormUrl;

View file

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
const isBlankString = (event: Event|null, value: string|number, field: string) => { const isBlankString = (value: string|number, field: string, event?: Event) => {
value = value.toString(); value = value.toString();
if (!value.replace(/\s/g, '').length) { if (!value.replace(/\s/g, '').length) {
if (event) { if (event !== undefined) {
event.preventDefault(); event.preventDefault();
} }
alert(`${field} can't be empty`); alert(`${field} can't be empty`);

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
const isNumber = (value: string) => { const isNumber = (value: string|number) => {
value = value.toString(); value = value.toString();
return /^\d+$/.test(value); return /^\d+$/.test(value);
}; };