keystroke authentication ui front end

parent b4557e66
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