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) => {