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 @@ ...@@ -10,11 +10,16 @@
"@types/node": "^16.18.8", "@types/node": "^16.18.8",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"axios": "^1.3.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.5", "react-router-dom": "^6.4.5",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"recharts": "^2.2.0", "recharts": "^2.2.0",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.2",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
...@@ -44,5 +49,6 @@ ...@@ -44,5 +49,6 @@
}, },
"devDependencies": { "devDependencies": {
"sass": "^1.56.2" "sass": "^1.56.2"
} },
"proxy": "http://localhost:5000"
} }
import React from "react"; import React from "react";
import { Routes, BrowserRouter as Router, Route } from "react-router-dom"; import { Routes, BrowserRouter as Router, Route } from "react-router-dom";
import SignUp from "./components/SignUp"; import Landing from "./views/Landing.view";
import Login from "./components/Login"; import Home from "./views/Home.view";
import Settings from "./views/Settings.view";
import AppState from "./components/AppState";
import "./app.scss"; import "./app.scss";
import "./components.scss";
import ProtectedRoutes from "./components/ProtectedRoute";
function App() { function App() {
return ( return (
<Router> <Router>
<Routes> <Routes>
<Route path="/" element={<SignUp />} /> <Route path="/" element={<Landing />} />
<Route path="/login" element={<Login />} /> <Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes> </Routes>
<AppState />
</Router> </Router>
); );
} }
......
...@@ -5,3 +5,28 @@ body { ...@@ -5,3 +5,28 @@ body {
p { p {
margin: 0; 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 // 16 - Shift
// 17 - Ctrl // 17 - Ctrl
// 18 - Alt // 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 = { export type KeyDetails = {
key: any; key: any;
code: any; code: any;
...@@ -55,9 +66,100 @@ export type KeystrokeType = { ...@@ -55,9 +66,100 @@ export type KeystrokeType = {
full: number[]; full: number[];
}; };
export type Result = { export type KeystrokeResultType = {
attempt: KeystrokeType; attempt: KeystrokeType;
db: KeystrokeType; db: KeystrokeType;
filteredDb: KeystrokeType; filteredDb: KeystrokeType;
result: Stat | null; 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) => { ...@@ -16,8 +16,8 @@ const Charts = ({ real, attempts, title }: DataType) => {
})); }));
return ( return (
<div className="mt-5"> <div className="mt-2">
<h4>{title}</h4> <h6>{title}</h6>
<ResponsiveContainer height={280} width="100%"> <ResponsiveContainer height={280} width="100%">
<AreaChart data={data}> <AreaChart data={data}>
<Tooltip /> <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 React, { useState, useRef } from "react";
import { initialiseControls } from "../util/controls"; import { useDispatch } from "react-redux";
import { keyEvent } from "../util/keystrokeLogger"; import { useNavigate } from "react-router-dom";
import { KeyDetails, Result } from "../util/types"; import { ACTIONS } from "../common/actions";
import Charts from "./Charts"; import AuthAPI from "../common/apis/auth";
import ResultTable from "./ResultTable"; import { keyEvent } from "../common/lib/util";
import { KeyDetails, SignInPayload, USER_TYPE } from "../common/types";
const controls = {
standard: { type OwnProps = {
sd: 1.5, setAlert: (alert: string) => void;
threshold: 65, userType: USER_TYPE;
use: true,
},
fullStandard: {
threshold: 1,
use: true,
},
}; };
const Login = () => { const Login = ({ userType, setAlert }: OwnProps) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
userName: "", email: "",
password: "", password: "",
}); });
const [result, setResult] = useState<Result | null>(null);
const keyCount = useRef<number>(0); const keyCount = useRef<number>(0);
const keydownArray = useRef<KeyDetails[]>([]); const keydownArray = useRef<KeyDetails[]>([]);
const keyupArray = useRef<KeyDetails[]>([]); const keyupArray = useRef<KeyDetails[]>([]);
...@@ -33,10 +27,6 @@ const Login = () => { ...@@ -33,10 +27,6 @@ const Login = () => {
setCredentials({ ...credentials, [e.target.name]: e.target.value }); setCredentials({ ...credentials, [e.target.name]: e.target.value });
}; };
useEffect(() => {
initialiseControls(controls);
}, []);
const clearData = () => { const clearData = () => {
setCredentials({ ...credentials, password: "" }); setCredentials({ ...credentials, password: "" });
keyCount.current = 0; keyCount.current = 0;
...@@ -45,29 +35,29 @@ const Login = () => { ...@@ -45,29 +35,29 @@ const Login = () => {
}; };
const submit = () => { const submit = () => {
const data = { const data: SignInPayload = {
username: credentials.userName, email: credentials.email,
password: credentials.password, password: credentials.password,
keydown: keydownArray.current, keydown: keydownArray.current,
keyup: keyupArray.current, keyup: keyupArray.current,
controls, userType,
}; };
clearData(); clearData();
fetch("http://localhost:3001/user/login", { AuthAPI.signin(data)
method: "POST", .then((res: any) => {
mode: "cors", if (res.success) {
headers: { const payload = {
"Content-Type": "application/json", ...res,
}, organization: userType === USER_TYPE.ORGANIZATION ? res.user : {},
body: JSON.stringify(data), candidate: userType === USER_TYPE.CANDIDATE ? res.user : {},
}).then(async (res) => { };
let response: Result = await res.json(); dispatch({ type: ACTIONS.SET_AUTH, payload });
if (res.ok) { navigate("/home");
setResult(response);
} else {
console.log(response);
} }
})
.catch((_) => {
setAlert("Attempt failed! Try again");
}); });
}; };
...@@ -108,23 +98,20 @@ const Login = () => { ...@@ -108,23 +98,20 @@ const Login = () => {
if (details.code === "Enter") submit(); if (details.code === "Enter") submit();
}; };
const disable = credentials.userName === "" || credentials.password === ""; const disable = credentials.email === "" || credentials.password === "";
return ( return (
<div className="container"> <>
<div className="row mt-5"> <h5 className="card-title">Sign in</h5>
<div className="col-lg-4">
<div className="card card-body pt-4 pb-4">
<h5 className="card-title">Login</h5>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label"> <label htmlFor="exampleFormControlInput1" className="form-label">
User name Email
</label> </label>
<input <input
className="form-control" className="form-control"
type="text" type="email"
aria-label=".form-control-lg example" aria-label=".form-control-lg example"
name="userName" name="email"
onChange={onChangeCredentials} onChange={onChangeCredentials}
/> />
</div> </div>
...@@ -152,37 +139,7 @@ const Login = () => { ...@@ -152,37 +139,7 @@ const Login = () => {
> >
Login Login
</button> </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}
/>
</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>
</div>
); );
}; };
......
/* 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 React from "react";
import { Stat } from "../util/types"; import { Stat } from "../common/types";
const ResultTable = ({ data }: { data?: Stat | null }) => { const ResultTable = ({ data }: { data?: Stat | null }) => {
if (!data) return null; if (!data) return null;
...@@ -9,7 +9,7 @@ const ResultTable = ({ data }: { data?: Stat | null }) => { ...@@ -9,7 +9,7 @@ const ResultTable = ({ data }: { data?: Stat | null }) => {
<tr> <tr>
<th></th> <th></th>
<th>Standard</th> <th>Standard</th>
<th>Standard (Full)</th> <th>Advance (Full)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
......
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { keyEvent } from "../util/keystrokeLogger"; import AuthAPI from "../common/apis/auth";
import { KeyDetails } from "../util/types"; import { keyEvent } from "../common/lib/util";
import { KeyDetails, SignUpPayload, USER_TYPE } from "../common/types";
const SignUp = () => { type OwnProps = {
const [alert, setAlert] = useState<string | null>(null); 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 [is2FAenabled, setis2FAenabled] = useState<boolean>(false);
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
userName: "", name: "",
email: "",
password: "", password: "",
confrimPassword: "", confrimPassword: "",
twoFAPassword: "", twoFAPassword: "",
...@@ -38,8 +45,12 @@ const SignUp = () => { ...@@ -38,8 +45,12 @@ const SignUp = () => {
return setAlert("Passwords do not match"); return setAlert("Passwords do not match");
} }
const data = { if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(credentials.email)) {
username: credentials.userName, return setAlert("Invalid email address");
}
const payload: SignUpPayload = {
email: credentials.email,
passwords: [ passwords: [
credentials.password, credentials.password,
credentials.confrimPassword, credentials.confrimPassword,
...@@ -47,27 +58,28 @@ const SignUp = () => { ...@@ -47,27 +58,28 @@ const SignUp = () => {
], ],
keydown: keydownArray.current, keydown: keydownArray.current,
keyup: keyupArray.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(); resetData();
fetch("http://localhost:3001/user/signup", { AuthAPI.signup(payload)
method: "POST", .then((res: any) => {
mode: "cors", if (res.success) {
headers: { setForm("sign-in");
"Content-Type": "application/json", setAlert("Successfully signed up. Please login");
},
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);
} }
})
.catch((error) => {
setAlert(error.msg);
}); });
}; };
...@@ -146,48 +158,43 @@ const SignUp = () => { ...@@ -146,48 +158,43 @@ const SignUp = () => {
); );
if (reqdUpKeystroke) reqdUpKeystroke.time = details.time; if (reqdUpKeystroke) reqdUpKeystroke.time = details.time;
if (index === 2) {
onSumit();
}
}; };
const disableSubmit = const disableSubmit =
credentials.userName === "" || credentials.password === "" || !is2FAenabled; credentials.email === "" ||
credentials.password === "" ||
!is2FAenabled ||
credentials.name === "";
const renderAlert = alert && ( const nameLabel =
<div userType === USER_TYPE.CANDIDATE ? "User name" : "Organization name";
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>
);
return ( return (
<div className="container"> <>
<div className=" row"> <h5 className="card-title">Sign up</h5>
<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"> <div className="mb-3">
<label htmlFor="exampleFormControlInput1" className="form-label"> <label htmlFor="exampleFormControlInput1" className="form-label">
User name {nameLabel}
</label> </label>
<input <input
className="form-control" className="form-control"
type="text" type="text"
aria-label=".form-control-lg example" aria-label=".form-control-lg example"
onChange={onChangeCredentials} onChange={onChangeCredentials}
name="userName" name="name"
/>
</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>
...@@ -239,10 +246,7 @@ const SignUp = () => { ...@@ -239,10 +246,7 @@ const SignUp = () => {
</div> </div>
{is2FAenabled && ( {is2FAenabled && (
<div className="mb-3"> <div className="mb-3">
<label <label htmlFor="exampleFormControlInput1" className="form-label">
htmlFor="exampleFormControlInput1"
className="form-label"
>
Enter your password again Enter your password again
</label> </label>
<input <input
...@@ -266,12 +270,7 @@ const SignUp = () => { ...@@ -266,12 +270,7 @@ const SignUp = () => {
> >
Sign up Sign up
</button> </button>
{renderAlert} </>
</div>
</div>
<div className="col-lg"></div>
</div>
</div>
); );
}; };
......
import React from "react"; import React from "react";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App"; import App from "./App";
import { store, persistor } from "./store";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement document.getElementById("root") as HTMLElement
); );
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}>
<PersistGate persistor={persistor}>
<App /> <App />
</PersistGate>
</Provider>
</React.StrictMode> </React.StrictMode>
); );
/// <reference types="react-scripts" /> /// <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": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
...@@ -20,7 +16,5 @@ ...@@ -20,7 +16,5 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src"]
"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