Commit 9b30774e authored by H.M.C. Nadunithara Wijerathne's avatar H.M.C. Nadunithara Wijerathne

Merge branch 'it19243122' into 'master'

keystroke authentication ui front end

See merge request !1
parents b4557e66 fa5e865d
This diff is collapsed.
......@@ -10,11 +10,16 @@
"@types/node": "^16.18.8",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"axios": "^1.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.5",
"react-scripts": "5.0.1",
"recharts": "^2.2.0",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.2",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
},
......@@ -44,5 +49,6 @@
},
"devDependencies": {
"sass": "^1.56.2"
}
},
"proxy": "http://localhost:5000"
}
import React from "react";
import { Routes, BrowserRouter as Router, Route } from "react-router-dom";
import SignUp from "./components/SignUp";
import Login from "./components/Login";
import Landing from "./views/Landing.view";
import Home from "./views/Home.view";
import Settings from "./views/Settings.view";
import AppState from "./components/AppState";
import "./app.scss";
import "./components.scss";
import ProtectedRoutes from "./components/ProtectedRoute";
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<SignUp />} />
<Route path="/login" element={<Login />} />
<Route path="/" element={<Landing />} />
<Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
<AppState />
</Router>
);
}
......
......@@ -5,3 +5,28 @@ body {
p {
margin: 0;
}
.landing {
background: rgb(131, 58, 180);
background: linear-gradient(
160deg,
rgba(131, 58, 180, 1) 0%,
rgba(143, 244, 241, 1) 0%,
rgba(119, 122, 255, 1) 100%
);
}
.navbar {
background-color: #5b8ddf;
.collapse {
img {
height: 35px;
width: 35px;
border-radius: 35px;
}
.dropdown-toggle::after {
display: none;
}
}
}
import { ACTIONS } from ".";
import { ControlsType, Reducers, UpdatePasswordPayload } from "../types";
export const setAppState = (payload: Reducers["auth"]["appState"]) => ({
type: ACTIONS.SET_APP_STATE,
payload,
});
export const updateKeystrokeSettings = (payload: ControlsType) => ({
type: ACTIONS.UPDATE_KEYSTROKE_SETTINGS,
payload,
});
export const changePassword = (payload: UpdatePasswordPayload) => ({
type: ACTIONS.CHANGE_PASSWORD,
payload,
});
export enum ACTIONS {
SET_AUTH = "SET_AUTH",
SIGN_OUT_USER = "SIGN_OUT_USER",
UPDATE_KEYSTROKE_SETTINGS = "UPDATE_KEYSTROKE_SETTINGS",
CHANGE_PASSWORD = "CHANGE_PASSWORD",
SET_APP_STATE = "SET_APP_STATE",
}
import { request } from "../lib/api";
import {
SignUpPayload,
SignInPayload,
ControlsType,
UpdatePasswordPayload,
} from "../types";
export default class AuthAPI {
static signup = (payload: SignUpPayload) =>
request("<BASE_URL>/auth/signup", "POST", payload, true);
static signin = (payload: SignInPayload) =>
request("<BASE_URL>/auth/login", "POST", payload, true);
static updateControls = (payload: ControlsType) =>
request("<BASE_URL>/auth/update", "POST", payload);
static updatePassword = (payload: UpdatePasswordPayload) =>
request("<BASE_URL>/auth/change-pwd", "POST", payload);
}
export const BASE_URL = "http://localhost:5000";
export const DEFAULT_CONTROLS = {
standard: {
sd: 1.5,
threshold: 65,
use: true,
},
fullStandard: {
threshold: 1,
use: true,
},
};
import axios, { AxiosError, AxiosResponse, AxiosRequestConfig } from "axios";
import { BASE_URL } from "../config";
import { logger } from "./util";
import { store } from "../../store";
const getToken = () => {
const token = store.getState().auth.token;
return token;
};
axios.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
return Promise.reject(error);
}
);
export const request = (
url: AxiosRequestConfig["url"],
method: AxiosRequestConfig["method"],
requestData?: AxiosRequestConfig["data"] | AxiosRequestConfig["params"],
isGuest?: boolean,
contentType?: string
) =>
new Promise(async (resolve, reject) => {
const endpoint = url?.replace?.("<BASE_URL>", BASE_URL);
const params = method === "GET" ? requestData : null;
const data = method === "GET" ? null : requestData;
const auth_token = !isGuest ? await getToken() : null;
const headers = {
auth_token,
"Content-Type": contentType || "application/json",
};
logger("REQUEST: ", method, endpoint, headers, requestData);
axios({
url: endpoint,
method,
data,
params,
headers,
timeout: 30000,
})
.then(async (response: AxiosResponse) => {
logger("RESPONSE: ", response);
resolve(response.data);
})
.catch(async (error: AxiosError) => {
if (error?.response) {
logger("ERROR RESPONSE: ", error?.response);
return reject(error?.response?.data);
} else if (error?.request) {
logger("NO RESPONSE: ", error?.request);
} else {
logger("SETUP ISSUE: ", error?.message);
}
reject(error);
});
});
export const logger = (log: any, ...optionalparams: any[]) => {
console.log(log, ...optionalparams);
};
// 16 - Shift
// 17 - Ctrl
// 18 - Alt
......
import { ACTIONS } from "../actions/index";
import { AuthReducer, USER_TYPE } from "../types";
const INITIAL_STATE: AuthReducer = {
token: null,
candidate: {},
organization: {},
userType: USER_TYPE.CANDIDATE,
userId: "",
appState: null,
};
const authReducer = (
state = INITIAL_STATE,
{ type, payload }: { type: ACTIONS; payload: any }
): AuthReducer => {
switch (type) {
case ACTIONS.SET_AUTH:
return { ...state, ...(payload as AuthReducer) };
case ACTIONS.SET_APP_STATE:
return { ...state, appState: payload };
case ACTIONS.SIGN_OUT_USER:
return { ...state, token: null };
default:
return state;
}
};
export default authReducer;
import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import authReducer from "../reducers/auth";
const AuthPersistConfig = {
storage,
key: "auth",
};
export default combineReducers({
auth: persistReducer(AuthPersistConfig, authReducer),
});
import { takeLeading, call, put, select } from "redux-saga/effects";
import { ACTIONS } from "../actions";
import AuthAPI from "../apis/auth";
import {
APP_STATE,
ControlsType,
KeystrokeResultType,
Reducers,
UpdatePasswordPayload,
} from "../types";
function* updateKeystrokeSettings({
payload,
}: {
type: typeof ACTIONS.UPDATE_KEYSTROKE_SETTINGS;
payload: ControlsType;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
yield call(AuthAPI.updateControls, payload);
const keystrokeResult: KeystrokeResultType = yield select(
(state: Reducers) => state.auth.keystrokeResult
);
yield put({
type: ACTIONS.SET_AUTH,
payload: { keystrokeResult: { ...keystrokeResult, controls: payload } },
});
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED, msg: error },
});
}
}
function* changePassword({
payload,
}: {
type: typeof ACTIONS.CHANGE_PASSWORD;
payload: UpdatePasswordPayload;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
yield call(AuthAPI.updatePassword, payload);
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED, msg: error },
});
}
}
export default function* authSaga() {
yield takeLeading(ACTIONS.UPDATE_KEYSTROKE_SETTINGS, updateKeystrokeSettings);
yield takeLeading(ACTIONS.CHANGE_PASSWORD, changePassword);
}
import { all } from "redux-saga/effects";
import Auth from "./auth";
export default function* rootSaga() {
yield all([Auth()]);
}
export enum USER_TYPE {
ORGANIZATION = "ORGANIZATION",
CANDIDATE = "CANDIDATE",
}
export enum APP_STATE {
LOADING = "loading",
FAILED = "failed",
SUCCESS = "success",
}
export type KeyDetails = {
key: any;
code: any;
......@@ -55,9 +66,100 @@ export type KeystrokeType = {
full: number[];
};
export type Result = {
export type KeystrokeResultType = {
attempt: KeystrokeType;
db: KeystrokeType;
filteredDb: KeystrokeType;
result: Stat | null;
controls: ControlsType;
};
export type AddressType = {
addressLine: string;
city: string;
country: string;
};
export type CandidateType = {
_id?: string;
name: string;
bio: string;
contacts: {
email: string;
phone: string;
address: AddressType;
residentialAddress?: AddressType;
};
dateOfBirth: string;
jobIds: string[];
profilePicture: string;
};
export type OrganizationType = {
_id?: string;
name: string;
description: string;
contacts: {
email: string;
phone: string[];
address: AddressType;
website: string;
};
profilePicture: string;
};
export type ControlsType = {
standard: {
sd: number;
threshold: number;
use: boolean;
};
fullStandard: {
threshold: number;
use: boolean;
};
};
//PAYLOADS
export type SignUpPayload = {
passwords: string[];
keydown: KeyDetails[][];
keyup: KeyDetails[][];
email: string;
userType: USER_TYPE;
candidate?: CandidateType | {};
organization?: OrganizationType | {};
};
export type SignInPayload = {
password: string;
keydown: KeyDetails[];
keyup: KeyDetails[];
email: string;
userType: USER_TYPE;
};
export type UpdatePasswordPayload = {
passwords: string[];
keydown: KeyDetails[][];
keyup: KeyDetails[][];
oldPassword: string;
};
//REDUCERS
export type AuthReducer = {
token: string | null;
candidate: CandidateType | {};
organization: OrganizationType | {};
userType: USER_TYPE;
userId: string;
keystrokeResult?: KeystrokeResultType;
appState?: {
state: APP_STATE;
msg?: string;
} | null;
};
export type Reducers = {
auth: AuthReducer;
};
.card {
border: none;
box-shadow: 0 0 20px 0 rgb(86 153 196 / 15%);
border-radius: 15px;
}
.onboard {
width: 400px;
float: right;
margin-top: 100px;
.usertype-selector {
margin-top: -10px;
margin-bottom: 15px;
.btn {
border-radius: 0;
border-width: 0 0 2px 0;
background-color: white;
font-weight: 500;
color: #0d6efd;
border-bottom-color: #0d6efd;
&.deactive {
color: #d9d9d9;
border-bottom-color: #d9d9d9;
}
}
}
section {
margin-top: 10px;
.btn {
padding: 0;
margin-left: 10px;
font-size: 16px;
margin-bottom: 4px;
}
}
}
.avatar {
background-color: #8eb4f2;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin: auto;
&.lg {
height: 100px;
width: 100px;
border-radius: 100px;
font-size: 60px;
}
&.sm {
height: 35px;
width: 35px;
border-radius: 35px;
font-size: 20px;
}
}
.alert {
button {
&:active,
&:focus {
border: none;
outline: none;
box-shadow: none;
}
}
}
.keystrokes {
.keystrokes-settings {
margin-bottom: 10px;
.form-switch {
margin-top: 5px;
margin-bottom: 5px;
.form-check-label {
font-weight: 500;
}
}
}
}
.appstate {
padding: 10px 20px;
background-color: red;
max-width: 350px;
border-radius: 15px;
box-shadow: 0 0 10px 0 rgba(83, 83, 83, 0.662);
position: fixed;
width: 350px;
bottom: 30px;
right: 30px;
section {
p {
font-weight: bold;
}
}
&.failed {
background: rgb(131, 58, 180);
background: linear-gradient(
153deg,
rgba(131, 58, 180, 1) 0%,
rgba(255, 71, 71, 1) 0%,
rgba(255, 155, 119, 1) 100%
);
}
&.success {
background: rgb(131, 58, 180);
background: linear-gradient(
153deg,
rgba(131, 58, 180, 1) 0%,
rgba(46, 197, 198, 1) 0%,
rgba(216, 255, 134, 1) 100%
);
}
&.loading {
background: rgb(131, 58, 180);
background: linear-gradient(
153deg,
rgba(131, 58, 180, 1) 0%,
rgba(189, 189, 189, 1) 0%,
rgba(134, 188, 255, 1) 100%
);
}
}
import React from "react";
type OwnProps = {
alert?: string | null;
setAlert: (value: string | null) => void;
};
const Alert = ({ alert, setAlert }: OwnProps) => {
const onClose = () => setAlert(null);
if (!alert) return null;
return (
<div className="alert alert-warning alert-dismissible fade show mt-3">
{alert}
<button type="button" className="btn-close" onClick={onClose}></button>
</div>
);
};
export default Alert;
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setAppState } from "../common/actions/auth";
import { APP_STATE, Reducers } from "../common/types";
const AppState = () => {
const dispatch = useDispatch();
const appState = useSelector((state: Reducers) => state.auth.appState);
useEffect(() => {
if (appState?.state !== APP_STATE.LOADING) {
setTimeout(() => {
dispatch(setAppState(null));
}, 5000);
}
}, [appState?.state, dispatch]);
if (!appState) return null;
const state =
appState.state === APP_STATE.LOADING
? "Loading..."
: appState.state === APP_STATE.FAILED
? "Failed"
: "Success";
return (
<div className={`appstate ${appState?.state}`}>
<section>
<p>{state}</p>
</section>
<p>{appState?.msg}</p>
</div>
);
};
export default AppState;
import React from "react";
const Avatar = ({
url,
name,
size = "sm",
}: {
url?: string;
name: string;
size?: "sm" | "md" | "lg";
}) => {
if (url) return <img src={url} alt={name} className={`avatar img ${size}`} />;
return <div className={`avatar ${size}`}>{name[0]}</div>;
};
export default Avatar;
import React, { useRef, useEffect, useState } from "react";
import { KeyDetails, UpdatePasswordPayload } from "../common/types";
import Alert from "../components/Alert";
import { keyEvent } from "../common/lib/util";
import { useDispatch } from "react-redux";
import { changePassword } from "../common/actions/auth";
const ChangePassword = () => {
const dispatch = useDispatch();
const keydownArray = useRef<KeyDetails[][]>([]);
const keyupArray = useRef<KeyDetails[][]>([]);
const keyCount = useRef<number[]>([]);
const [alert, setAlert] = useState<string | null>(null);
const [credentials, setCredentials] = useState({
oldPassword: "",
password: "",
confrimPassword: "",
twoFAPassword: "",
});
useEffect(() => {
resetData();
}, []);
const resetData = () => {
for (let i = 0; i < 3; i++) {
keydownArray.current.push([]);
keyupArray.current.push([]);
keyCount.current.push(0);
}
};
const onChangeCredentials = (e: React.ChangeEvent<HTMLInputElement>) => {
setCredentials({ ...credentials, [e.target.name]: e.target.value });
};
const clearField = (index: number) => {
if (index === 0) {
setCredentials({ ...credentials, password: "" });
} else if (index === 1) {
setCredentials({ ...credentials, confrimPassword: "" });
} else {
setCredentials({ ...credentials, twoFAPassword: "" });
}
keydownArray.current[index] = [];
keyupArray.current[index] = [];
keyCount.current[index] = 0;
};
const onKeyDown = (e: any) => {
const field = e.target.id;
const index = Number(field.substr(field.length - 1));
let details = keyEvent(e);
if (!details) return;
if (
["Backspace"].includes(details.code) ||
["Backspace"].includes(details.code)
) {
return clearField(index);
}
if (
["Tab", "NumpadEnter"].includes(details.code) ||
["Tab"].includes(details.key)
) {
details = {
...details,
code: "Enter",
key: "Enter",
};
}
keydownArray.current[index].push(details);
keyupArray.current[index].push({ ...details, time: null });
keyCount.current[index]++;
};
const onKeyUp = (e: any) => {
const field = e.target.id;
const index = Number(field.substr(field.length - 1));
let details = keyEvent(e);
if (!details) return;
if (!details.time) details.time = Date.now();
if (
["Backspace"].includes(details.code) ||
["Backspace"].includes(details.code)
) {
return clearField(index);
}
if (
["Tab", "NumpadEnter"].includes(details.code) ||
["Tab"].includes(details.key)
) {
details = {
...details,
code: "Enter",
key: "Enter",
};
}
let reqdUpKeystroke = keyupArray.current[index].find(
(element) => element.code === details?.code && !element.time
);
if (reqdUpKeystroke) reqdUpKeystroke.time = details.time;
};
const onSumit = () => {
setAlert(null);
if (credentials.password !== credentials.confrimPassword) {
return setAlert("Passwords do not match");
}
const payload: UpdatePasswordPayload = {
oldPassword: credentials.oldPassword,
passwords: [
credentials.password,
credentials.confrimPassword,
credentials.twoFAPassword,
],
keydown: keydownArray.current,
keyup: keyupArray.current,
};
resetData();
dispatch(changePassword(payload));
};
return (
<div className="card p-4">
<h5>Change password</h5>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">Old password</label>
<div className="col-sm-5">
<input
type="password"
className="form-control"
name="oldPassword"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">New password</label>
<div className="col-sm-5">
<input
type="password"
className="form-control"
name="password"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">Confirm new password</label>
<div className="col-sm-5">
<input
type="password"
className="form-control"
name="confrimPassword"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
</div>
<div className="mb-3 row">
<label className="col-sm-3 col-form-label">2FA confirm password</label>
<div className="col-sm-5">
<input
type="password"
className="form-control"
name="twoFAPassword"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
</div>
<button
type="button"
className="btn btn-primary"
style={{ margin: "auto", width: "300px" }}
onClick={onSumit}
>
Save Password
</button>
<Alert alert={alert} setAlert={setAlert} />
</div>
);
};
export default ChangePassword;
......@@ -16,8 +16,8 @@ const Charts = ({ real, attempts, title }: DataType) => {
}));
return (
<div className="mt-5">
<h4>{title}</h4>
<div className="mt-2">
<h6>{title}</h6>
<ResponsiveContainer height={280} width="100%">
<AreaChart data={data}>
<Tooltip />
......
import React from "react";
import { useSelector } from "react-redux";
import { Reducers } from "../common/types";
import Charts from "./Charts";
import ResultTable from "./ResultTable";
const KeystrokeResults = () => {
const keystrokeResult = useSelector(
(state: Reducers) => state.auth.keystrokeResult
);
return (
<div className="container">
<div className="row mt-5">
<div className="col-lg">
<ResultTable data={keystrokeResult?.result} />
</div>
</div>
<div className="row mt-5 ">
<Charts
title="Overall keystroke"
real={keystrokeResult?.db.full}
attempts={keystrokeResult?.attempt.full}
/>
</div>
<div className="row">
<Charts
title="DD keystroke"
real={keystrokeResult?.db.dd}
attempts={keystrokeResult?.attempt.dd}
/>
<Charts
title="Hold keystroke"
real={keystrokeResult?.db.hold}
attempts={keystrokeResult?.attempt.hold}
/>
<Charts
title="Flight keystroke"
real={keystrokeResult?.db.flight}
attempts={keystrokeResult?.attempt.flight}
/>
</div>
</div>
);
};
export default KeystrokeResults;
import React from "react";
import NavBar from "./NavBar";
type OwnProps = {
title: string;
children?: JSX.Element | JSX.Element[];
};
const Layout = ({ title, children }: OwnProps) => {
return (
<>
<NavBar />
<div className="container pb-5">
<h4 className="mb-3">{title}</h4>
{children}
</div>
</>
);
};
export default Layout;
import React, { useState, useRef, useEffect } from "react";
import { initialiseControls } from "../util/controls";
import { keyEvent } from "../util/keystrokeLogger";
import { KeyDetails, Result } from "../util/types";
import Charts from "./Charts";
import ResultTable from "./ResultTable";
const controls = {
standard: {
sd: 1.5,
threshold: 65,
use: true,
},
fullStandard: {
threshold: 1,
use: true,
},
import React, { useState, useRef } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { ACTIONS } from "../common/actions";
import AuthAPI from "../common/apis/auth";
import { keyEvent } from "../common/lib/util";
import { KeyDetails, SignInPayload, USER_TYPE } from "../common/types";
type OwnProps = {
setAlert: (alert: string) => void;
userType: USER_TYPE;
};
const Login = () => {
const Login = ({ userType, setAlert }: OwnProps) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [credentials, setCredentials] = useState({
userName: "",
email: "",
password: "",
});
const [result, setResult] = useState<Result | null>(null);
const keyCount = useRef<number>(0);
const keydownArray = useRef<KeyDetails[]>([]);
const keyupArray = useRef<KeyDetails[]>([]);
......@@ -33,10 +27,6 @@ const Login = () => {
setCredentials({ ...credentials, [e.target.name]: e.target.value });
};
useEffect(() => {
initialiseControls(controls);
}, []);
const clearData = () => {
setCredentials({ ...credentials, password: "" });
keyCount.current = 0;
......@@ -45,30 +35,30 @@ const Login = () => {
};
const submit = () => {
const data = {
username: credentials.userName,
const data: SignInPayload = {
email: credentials.email,
password: credentials.password,
keydown: keydownArray.current,
keyup: keyupArray.current,
controls,
userType,
};
clearData();
fetch("http://localhost:3001/user/login", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then(async (res) => {
let response: Result = await res.json();
if (res.ok) {
setResult(response);
} else {
console.log(response);
}
});
AuthAPI.signin(data)
.then((res: any) => {
if (res.success) {
const payload = {
...res,
organization: userType === USER_TYPE.ORGANIZATION ? res.user : {},
candidate: userType === USER_TYPE.CANDIDATE ? res.user : {},
};
dispatch({ type: ACTIONS.SET_AUTH, payload });
navigate("/home");
}
})
.catch((_) => {
setAlert("Attempt failed! Try again");
});
};
const onKeyDown = (e: any) => {
......@@ -108,81 +98,48 @@ const Login = () => {
if (details.code === "Enter") submit();
};
const disable = credentials.userName === "" || credentials.password === "";
const disable = credentials.email === "" || credentials.password === "";
return (
<div className="container">
<div className="row mt-5">
<div className="col-lg-4">
<div className="card card-body pt-4 pb-4">
<h5 className="card-title">Login</h5>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
User name
</label>
<input
className="form-control"
type="text"
aria-label=".form-control-lg example"
name="userName"
onChange={onChangeCredentials}
/>
</div>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Password
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="password"
id="password"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
<button
type="button"
className="btn btn-primary"
onClick={submit}
disabled={disable}
>
Login
</button>
</div>
</div>
<div className="col-lg">
<ResultTable data={result?.result} />
</div>
</div>
<div className="row mt-5 ">
<Charts
title="Overall keystroke"
real={result?.db.full}
attempts={result?.attempt.full}
<>
<h5 className="card-title">Sign in</h5>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Email
</label>
<input
className="form-control"
type="email"
aria-label=".form-control-lg example"
name="email"
onChange={onChangeCredentials}
/>
</div>
<div className="row">
<Charts
title="DD keystroke"
real={result?.db.dd}
attempts={result?.attempt.dd}
/>
<Charts
title="Hold keystroke"
real={result?.db.hold}
attempts={result?.attempt.hold}
/>
<Charts
title="Flight keystroke"
real={result?.db.flight}
attempts={result?.attempt.flight}
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Password
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="password"
id="password"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
/>
</div>
</div>
<button
type="button"
className="btn btn-primary"
onClick={submit}
disabled={disable}
>
Login
</button>
</>
);
};
......
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from "react";
import { useDispatch } from "react-redux";
import { ACTIONS } from "../common/actions";
import Avatar from "./Avatar";
const NavBar = () => {
const dispatch = useDispatch();
const onClickLogout = () => dispatch({ type: ACTIONS.SIGN_OUT_USER });
return (
<nav className="navbar navbar-expand-lg navbar-dark mb-3">
<div className="container">
<a className="navbar-brand" href="/home">
Smart Recruite
</a>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<a className="nav-link" href="/">
Link
</a>
</li>
</ul>
<nav className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href="/"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Avatar name="Hashan" size="sm" />
</a>
<ul className="dropdown-menu">
<li>
<a className="dropdown-item" href="/">
Action
</a>
</li>
<li>
<a className="dropdown-item" href="/settings">
Settings
</a>
</li>
<li>
<hr className="dropdown-divider" />
</li>
<li>
<a className="dropdown-item" onClick={onClickLogout}>
Logout
</a>
</li>
</ul>
</nav>
</div>
</div>
</nav>
);
};
export default NavBar;
import { useSelector } from "react-redux";
import { Navigate, Outlet } from "react-router-dom";
import { Reducers } from "../common/types";
export default function ProtectedRoutes() {
const auth = useSelector((state: Reducers) => state.auth?.token);
if (auth) return <Outlet />;
return <Navigate to="/" />;
}
import React from "react";
import { Stat } from "../util/types";
import { Stat } from "../common/types";
const ResultTable = ({ data }: { data?: Stat | null }) => {
if (!data) return null;
......@@ -9,7 +9,7 @@ const ResultTable = ({ data }: { data?: Stat | null }) => {
<tr>
<th></th>
<th>Standard</th>
<th>Standard (Full)</th>
<th>Advance (Full)</th>
</tr>
</thead>
<tbody>
......
import React, { useState, useRef, useEffect } from "react";
import { keyEvent } from "../util/keystrokeLogger";
import { KeyDetails } from "../util/types";
import AuthAPI from "../common/apis/auth";
import { keyEvent } from "../common/lib/util";
import { KeyDetails, SignUpPayload, USER_TYPE } from "../common/types";
const SignUp = () => {
const [alert, setAlert] = useState<string | null>(null);
type OwnProps = {
setAlert: (alert: string) => void;
userType: USER_TYPE;
setForm: (form: "sign-in" | "sign-up") => void;
};
const SignUp = ({ userType, setAlert, setForm }: OwnProps) => {
const [is2FAenabled, setis2FAenabled] = useState<boolean>(false);
const [credentials, setCredentials] = useState({
userName: "",
name: "",
email: "",
password: "",
confrimPassword: "",
twoFAPassword: "",
......@@ -38,8 +45,12 @@ const SignUp = () => {
return setAlert("Passwords do not match");
}
const data = {
username: credentials.userName,
if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(credentials.email)) {
return setAlert("Invalid email address");
}
const payload: SignUpPayload = {
email: credentials.email,
passwords: [
credentials.password,
credentials.confrimPassword,
......@@ -47,28 +58,29 @@ const SignUp = () => {
],
keydown: keydownArray.current,
keyup: keyupArray.current,
userType,
candidate:
userType === USER_TYPE.CANDIDATE
? { name: credentials.name, contacts: { email: credentials.email } }
: undefined,
organization:
userType === USER_TYPE.ORGANIZATION
? { name: credentials.name, contacts: { email: credentials.email } }
: undefined,
};
console.log("DATA ", data);
resetData();
fetch("http://localhost:3001/user/signup", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then(async (res) => {
const response = await res.json();
if (response.success) {
setAlert(`${response.username} signed up successfully`);
} else {
setAlert(response.msg);
}
});
AuthAPI.signup(payload)
.then((res: any) => {
if (res.success) {
setForm("sign-in");
setAlert("Successfully signed up. Please login");
}
})
.catch((error) => {
setAlert(error.msg);
});
};
const clearField = (index: number) => {
......@@ -146,132 +158,119 @@ const SignUp = () => {
);
if (reqdUpKeystroke) reqdUpKeystroke.time = details.time;
if (index === 2) {
onSumit();
}
};
const disableSubmit =
credentials.userName === "" || credentials.password === "" || !is2FAenabled;
credentials.email === "" ||
credentials.password === "" ||
!is2FAenabled ||
credentials.name === "";
const renderAlert = alert && (
<div
className="alert alert-warning alert-dismissible fade show mt-3"
role="alert"
>
{alert}
<button
type="button"
className="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
onClick={() => setAlert(null)}
></button>
</div>
);
const nameLabel =
userType === USER_TYPE.CANDIDATE ? "User name" : "Organization name";
return (
<div className="container">
<div className=" row">
<div className="col-lg"></div>
<div className="col-lg">
<div className="card card-body mt-5 pt-4 pb-4">
<h5 className="card-title">SignUp</h5>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
User name
</label>
<input
className="form-control"
type="text"
aria-label=".form-control-lg example"
onChange={onChangeCredentials}
name="userName"
/>
</div>
<>
<h5 className="card-title">Sign up</h5>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
{nameLabel}
</label>
<input
className="form-control"
type="text"
aria-label=".form-control-lg example"
onChange={onChangeCredentials}
name="name"
/>
</div>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Password
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="password"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
id="password-0"
value={credentials.password}
/>
</div>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Confirm Password
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="confrimPassword"
onChange={onChangeCredentials}
value={credentials.confrimPassword}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
id="password-1"
/>
</div>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Email
</label>
<input
className="form-control"
type="email"
aria-label=".form-control-lg example"
onChange={onChangeCredentials}
name="email"
/>
</div>
<div className="form-check mb-3">
<input
className="form-check-input"
type="checkbox"
name="2FA"
id="flexCheckDefault"
checked={is2FAenabled}
onChange={onChange2FAconcent}
/>
<label className="form-check-label" htmlFor="flexCheckDefault">
Enable keystroke dynamics 2FA
</label>
</div>
{is2FAenabled && (
<div className="mb-3">
<label
htmlFor="exampleFormControlInput1"
className="form-label"
>
Enter your password again
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="twoFAPassword"
onChange={onChangeCredentials}
value={credentials.twoFAPassword}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
id="password-2"
/>
</div>
)}
<button
type="button"
className="btn btn-primary"
disabled={disableSubmit}
onClick={onSumit}
>
Sign up
</button>
{renderAlert}
</div>
</div>
<div className="col-lg"></div>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Password
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="password"
onChange={onChangeCredentials}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
id="password-0"
value={credentials.password}
/>
</div>
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Confirm Password
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="confrimPassword"
onChange={onChangeCredentials}
value={credentials.confrimPassword}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
id="password-1"
/>
</div>
</div>
<div className="form-check mb-3">
<input
className="form-check-input"
type="checkbox"
name="2FA"
id="flexCheckDefault"
checked={is2FAenabled}
onChange={onChange2FAconcent}
/>
<label className="form-check-label" htmlFor="flexCheckDefault">
Enable keystroke dynamics 2FA
</label>
</div>
{is2FAenabled && (
<div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label">
Enter your password again
</label>
<input
className="form-control"
type="password"
aria-label=".form-control-lg example"
name="twoFAPassword"
onChange={onChangeCredentials}
value={credentials.twoFAPassword}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
id="password-2"
/>
</div>
)}
<button
type="button"
className="btn btn-primary"
disabled={disableSubmit}
onClick={onSumit}
>
Sign up
</button>
</>
);
};
......
import React from "react";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { store, persistor } from "./store";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
<Provider store={store}>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>
);
/// <reference types="react-scripts" />
/// <reference types="redux-persist" />
This diff is collapsed.
import { createStore, applyMiddleware, Store } from "redux";
import { persistStore } from "redux-persist";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./common/reducers/root";
import rootSaga from "./common/saga/root";
const sagaMiddleware = createSagaMiddleware();
const store: Store = createStore(
rootReducer,
{},
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
const persistor = persistStore(store);
export { store, persistor };
/* eslint-disable array-callback-return */
export function initialiseControls(controls: any) {
Object.keys(controls).map((detector) => {
if (!(detector in controls)) return null;
Object.keys(controls[detector]).map((controlType) => {
if (!(controlType in controls[detector])) return;
if (controlType === "checkbox") {
const controlId = `${detector}_${controlType}`;
const labelId = `${detector}_${controlType}_label`;
controls[detector][controlType] = document.getElementById(controlId);
controls[detector].label[controlType] =
document.getElementById(labelId);
return;
}
if (controlType === "slider") {
Object.keys(controls[detector][controlType]).map((sliderType) => {
if (!(sliderType in controls[detector][controlType])) return;
const controlId = `${detector}_${controlType}_${sliderType}`;
const labelId = `${controlId}_label`;
const slider: any = document.getElementById(controlId);
const label = document.getElementById(labelId);
if (label && slider) {
label.innerHTML = slider.value;
slider.oninput = () => {
label.innerHTML = slider.value;
};
}
controls[detector][controlType][sliderType] = slider;
controls[detector].label[sliderType] = label;
});
return;
}
});
});
}
import React from "react";
import NavBar from "../components/NavBar";
const Home = () => {
return (
<div>
<NavBar />
</div>
);
};
export default Home;
import React, { useState } from "react";
import Login from "../components/Login";
import SignUp from "../components/SignUp";
import COVER from "../res/cover.svg";
import { USER_TYPE } from "../common/types";
import Alert from "../components/Alert";
const Landing = () => {
const [alert, setAlert] = useState<string | null>(null);
const [userType, setUserType] = useState<USER_TYPE>(USER_TYPE.CANDIDATE);
const [form, setForm] = useState<"sign-in" | "sign-up">("sign-up");
const setCandidate = () => setUserType(USER_TYPE.CANDIDATE);
const setOrganization = () => setUserType(USER_TYPE.ORGANIZATION);
const candidateClassName =
userType === USER_TYPE.CANDIDATE ? "btn" : "btn deactive";
const organizationClassName =
userType === USER_TYPE.ORGANIZATION ? "btn active" : "btn deactive";
const renderForm =
form === "sign-up" ? (
<>
<SignUp userType={userType} setAlert={setAlert} setForm={setForm} />
<section>
<p>
Already have an account?
<button
type="button"
className="btn btn-link"
onClick={() => setForm("sign-in")}
>
Sing in
</button>
</p>
</section>
</>
) : (
<>
<Login userType={userType} setAlert={setAlert} />
<section>
<p>
New to Smart Recruiter?
<button
type="button"
className="btn btn-link"
onClick={() => setForm("sign-up")}
>
Sing up
</button>
</p>
</section>
</>
);
return (
<div className="landing">
<div className="container">
<div className="row">
<div className="col-6 mt-5">
<img src={COVER} alt="cover" />
</div>
<div className="col-6">
<div className="card card-body onboard p-4">
<div className="usertype-selector btn-group" role="group">
<button
type="button"
className={candidateClassName}
onClick={setCandidate}
>
Candidate
</button>
<button
type="button"
className={organizationClassName}
onClick={setOrganization}
>
Organization
</button>
</div>
{renderForm}
<Alert alert={alert} setAlert={setAlert} />
</div>
</div>
</div>
<div className="row" style={{ paddingBottom: "100px" }}>
<h1 className="display-6">Smart Recruiter </h1>
<p className="lead">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. At quae
saepe temporibus recusandae illo ex in voluptatibus assumenda
delectus sunt autem culpa ratione, ipsa totam magni ducimus eaque
quisquam. Harum!
</p>
</div>
</div>
</div>
);
};
export default Landing;
import React, { ChangeEvent, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateKeystrokeSettings } from "../common/actions/auth";
import { DEFAULT_CONTROLS } from "../common/config";
import { ControlsType, Reducers } from "../common/types";
import Avatar from "../components/Avatar";
import ChangePassword from "../components/ChangePassword";
import Charts from "../components/Charts";
import Layout from "../components/Layout";
import ResultTable from "../components/ResultTable";
const Settings = () => {
const dispatch = useDispatch();
const keystrokeResult = useSelector(
(state: Reducers) => state.auth.keystrokeResult
);
const [controls, setControls] = useState(
keystrokeResult?.controls || DEFAULT_CONTROLS
);
const onToggleStandard = () =>
setControls({
...controls,
standard: { ...controls.standard, use: !controls.standard.use },
});
const onToggleAdvance = () =>
setControls({
...controls,
fullStandard: {
...controls.fullStandard,
use: !controls.fullStandard.use,
},
});
const onChangeKeystroke = (e: ChangeEvent<HTMLInputElement>) => {
const [level, key] = e.target.name.split(".");
const attr = controls[level as keyof ControlsType];
setControls({
...controls,
[level]: {
...attr,
[key]: Number(e.target.value),
},
});
};
const onClickUpdateControls = () => {
dispatch(updateKeystrokeSettings(controls));
};
return (
<Layout title="Profile and Settings">
<div className="row">
<div className="col-8">
<div className="card p-4 mb-3">
<div className="mb-3 row">
<div className="col-sm-3">
<Avatar name="Hashan" size="lg" />
</div>
<div className="col-sm-9 ">
<input
className="form-control"
type="text"
placeholder="Default input"
aria-label="default input example"
/>
</div>
</div>
</div>
<ChangePassword />
</div>
<div className="col-4">
<div className="card p-4 keystrokes">
<h5>Keystroke-dynamics Settings</h5>
<div className="keystrokes-settings">
<div className="form-check form-switch">
<label className="form-check-label">Standard Settings</label>
<input
className="form-check-input"
type="checkbox"
role="switch"
checked={controls.standard.use}
onChange={onToggleStandard}
/>
</div>
<label htmlFor="customRange2" className="form-label">
SD Multiplier : {controls.standard.sd}
</label>
<input
type="range"
className="form-range"
min="0"
max="2.5"
step="0.1"
value={controls.standard.sd}
name="standard.sd"
onChange={onChangeKeystroke}
/>
<label htmlFor="customRange2" className="form-label">
Threshold : {controls.standard.threshold}
</label>
<input
type="range"
className="form-range"
min="1"
max="100"
value={controls.standard.threshold}
name="standard.threshold"
onChange={onChangeKeystroke}
/>
</div>
<div className="keystrokes-settings">
<div className="form-check form-switch">
<label className="form-check-label">Advance Settings</label>
<input
className="form-check-input"
type="checkbox"
role="switch"
checked={controls.fullStandard.use}
onChange={onToggleAdvance}
/>
</div>
<label htmlFor="customRange2" className="form-label">
Threshold : {controls.fullStandard.threshold}
</label>
<input
type="range"
className="form-range"
min="0"
max="2"
step="0.1"
value={controls.fullStandard.threshold}
name="fullStandard.threshold"
onChange={onChangeKeystroke}
/>
</div>
<button
type="button"
className="btn btn-primary"
onClick={onClickUpdateControls}
>
Update
</button>
<hr />
<h5>Last Login</h5>
<ResultTable data={keystrokeResult?.result} />
<Charts
title="Overall keystroke"
real={keystrokeResult?.db.full}
attempts={keystrokeResult?.attempt.full}
/>
</div>
</div>
</div>
</Layout>
);
};
export default Settings;
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
......@@ -20,7 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src"]
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment