keystroke authentication ui front end

parent b4557e66
This diff is collapsed.
......@@ -10,11 +10,16 @@
"@types/node": "^16.18.8",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"axios": "^1.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.5",
"react-scripts": "5.0.1",
"recharts": "^2.2.0",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.2",
"typescript": "^4.9.4",
"web-vitals": "^2.1.4"
},
......@@ -44,5 +49,6 @@
},
"devDependencies": {
"sass": "^1.56.2"
}
},
"proxy": "http://localhost:5000"
}
import React from "react";
import { Routes, BrowserRouter as Router, Route } from "react-router-dom";
import SignUp from "./components/SignUp";
import Login from "./components/Login";
import Landing from "./views/Landing.view";
import Home from "./views/Home.view";
import Settings from "./views/Settings.view";
import AppState from "./components/AppState";
import "./app.scss";
import "./components.scss";
import ProtectedRoutes from "./components/ProtectedRoute";
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<SignUp />} />
<Route path="/login" element={<Login />} />
<Route path="/" element={<Landing />} />
<Route element={<ProtectedRoutes />}>
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
<AppState />
</Router>
);
}
......
......@@ -5,3 +5,28 @@ body {
p {
margin: 0;
}
.landing {
background: rgb(131, 58, 180);
background: linear-gradient(
160deg,
rgba(131, 58, 180, 1) 0%,
rgba(143, 244, 241, 1) 0%,
rgba(119, 122, 255, 1) 100%
);
}
.navbar {
background-color: #5b8ddf;
.collapse {
img {
height: 35px;
width: 35px;
border-radius: 35px;
}
.dropdown-toggle::after {
display: none;
}
}
}
import { ACTIONS } from ".";
import { ControlsType, Reducers, UpdatePasswordPayload } from "../types";
export const setAppState = (payload: Reducers["auth"]["appState"]) => ({
type: ACTIONS.SET_APP_STATE,
payload,
});
export const updateKeystrokeSettings = (payload: ControlsType) => ({
type: ACTIONS.UPDATE_KEYSTROKE_SETTINGS,
payload,
});
export const changePassword = (payload: UpdatePasswordPayload) => ({
type: ACTIONS.CHANGE_PASSWORD,
payload,
});
export enum ACTIONS {
SET_AUTH = "SET_AUTH",
SIGN_OUT_USER = "SIGN_OUT_USER",
UPDATE_KEYSTROKE_SETTINGS = "UPDATE_KEYSTROKE_SETTINGS",
CHANGE_PASSWORD = "CHANGE_PASSWORD",
SET_APP_STATE = "SET_APP_STATE",
}
import { request } from "../lib/api";
import {
SignUpPayload,
SignInPayload,
ControlsType,
UpdatePasswordPayload,
} from "../types";
export default class AuthAPI {
static signup = (payload: SignUpPayload) =>
request("<BASE_URL>/auth/signup", "POST", payload, true);
static signin = (payload: SignInPayload) =>
request("<BASE_URL>/auth/login", "POST", payload, true);
static updateControls = (payload: ControlsType) =>
request("<BASE_URL>/auth/update", "POST", payload);
static updatePassword = (payload: UpdatePasswordPayload) =>
request("<BASE_URL>/auth/change-pwd", "POST", payload);
}
export const BASE_URL = "http://localhost:5000";
export const DEFAULT_CONTROLS = {
standard: {
sd: 1.5,
threshold: 65,
use: true,
},
fullStandard: {
threshold: 1,
use: true,
},
};
import axios, { AxiosError, AxiosResponse, AxiosRequestConfig } from "axios";
import { BASE_URL } from "../config";
import { logger } from "./util";
import { store } from "../../store";
const getToken = () => {
const token = store.getState().auth.token;
return token;
};
axios.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
return Promise.reject(error);
}
);
export const request = (
url: AxiosRequestConfig["url"],
method: AxiosRequestConfig["method"],
requestData?: AxiosRequestConfig["data"] | AxiosRequestConfig["params"],
isGuest?: boolean,
contentType?: string
) =>
new Promise(async (resolve, reject) => {
const endpoint = url?.replace?.("<BASE_URL>", BASE_URL);
const params = method === "GET" ? requestData : null;
const data = method === "GET" ? null : requestData;
const auth_token = !isGuest ? await getToken() : null;
const headers = {
auth_token,
"Content-Type": contentType || "application/json",
};
logger("REQUEST: ", method, endpoint, headers, requestData);
axios({
url: endpoint,
method,
data,
params,
headers,
timeout: 30000,
})
.then(async (response: AxiosResponse) => {
logger("RESPONSE: ", response);
resolve(response.data);
})
.catch(async (error: AxiosError) => {
if (error?.response) {
logger("ERROR RESPONSE: ", error?.response);
return reject(error?.response?.data);
} else if (error?.request) {
logger("NO RESPONSE: ", error?.request);
} else {
logger("SETUP ISSUE: ", error?.message);
}
reject(error);
});
});
export const logger = (log: any, ...optionalparams: any[]) => {
console.log(log, ...optionalparams);
};
// 16 - Shift
// 17 - Ctrl
// 18 - Alt
......
import { ACTIONS } from "../actions/index";
import { AuthReducer, USER_TYPE } from "../types";
const INITIAL_STATE: AuthReducer = {
token: null,
candidate: {},
organization: {},
userType: USER_TYPE.CANDIDATE,
userId: "",
appState: null,
};
const authReducer = (
state = INITIAL_STATE,
{ type, payload }: { type: ACTIONS; payload: any }
): AuthReducer => {
switch (type) {
case ACTIONS.SET_AUTH:
return { ...state, ...(payload as AuthReducer) };
case ACTIONS.SET_APP_STATE:
return { ...state, appState: payload };
case ACTIONS.SIGN_OUT_USER:
return { ...state, token: null };
default:
return state;
}
};
export default authReducer;
import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import authReducer from "../reducers/auth";
const AuthPersistConfig = {
storage,
key: "auth",
};
export default combineReducers({
auth: persistReducer(AuthPersistConfig, authReducer),
});
import { takeLeading, call, put, select } from "redux-saga/effects";
import { ACTIONS } from "../actions";
import AuthAPI from "../apis/auth";
import {
APP_STATE,
ControlsType,
KeystrokeResultType,
Reducers,
UpdatePasswordPayload,
} from "../types";
function* updateKeystrokeSettings({
payload,
}: {
type: typeof ACTIONS.UPDATE_KEYSTROKE_SETTINGS;
payload: ControlsType;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
yield call(AuthAPI.updateControls, payload);
const keystrokeResult: KeystrokeResultType = yield select(
(state: Reducers) => state.auth.keystrokeResult
);
yield put({
type: ACTIONS.SET_AUTH,
payload: { keystrokeResult: { ...keystrokeResult, controls: payload } },
});
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED, msg: error },
});
}
}
function* changePassword({
payload,
}: {
type: typeof ACTIONS.CHANGE_PASSWORD;
payload: UpdatePasswordPayload;
}) {
try {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.LOADING },
});
yield call(AuthAPI.updatePassword, payload);
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.SUCCESS },
});
} catch (error) {
yield put({
type: ACTIONS.SET_APP_STATE,
payload: { state: APP_STATE.FAILED, msg: error },
});
}
}
export default function* authSaga() {
yield takeLeading(ACTIONS.UPDATE_KEYSTROKE_SETTINGS, updateKeystrokeSettings);
yield takeLeading(ACTIONS.CHANGE_PASSWORD, changePassword);
}
import { all } from "redux-saga/effects";
import Auth from "./auth";
export default function* rootSaga() {
yield all([Auth()]);
}
export enum USER_TYPE {
ORGANIZATION = "ORGANIZATION",
CANDIDATE = "CANDIDATE",
}
export enum APP_STATE {
LOADING = "loading",
FAILED = "failed",
SUCCESS = "success",
}
export type KeyDetails = {
key: any;
code: any;
......@@ -55,9 +66,100 @@ export type KeystrokeType = {
full: number[];
};
export type Result = {
export type KeystrokeResultType = {
attempt: KeystrokeType;
db: KeystrokeType;
filteredDb: KeystrokeType;
result: Stat | null;
controls: ControlsType;
};
export type AddressType = {
addressLine: string;
city: string;
country: string;
};
export type CandidateType = {
_id?: string;
name: string;
bio: string;
contacts: {
email: string;
phone: string;
address: AddressType;
residentialAddress?: AddressType;
};
dateOfBirth: string;
jobIds: string[];
profilePicture: string;
};
export type OrganizationType = {
_id?: string;
name: string;
description: string;
contacts: {
email: string;
phone: string[];
address: AddressType;
website: string;
};
profilePicture: string;
};
export type ControlsType = {
standard: {
sd: number;
threshold: number;
use: boolean;
};
fullStandard: {
threshold: number;
use: boolean;
};
};
//PAYLOADS
export type SignUpPayload = {
passwords: string[];
keydown: KeyDetails[][];
keyup: KeyDetails[][];
email: string;
userType: USER_TYPE;
candidate?: CandidateType | {};
organization?: OrganizationType | {};
};
export type SignInPayload = {
password: string;
keydown: KeyDetails[];
keyup: KeyDetails[];
email: string;
userType: USER_TYPE;
};
export type UpdatePasswordPayload = {
passwords: string[];
keydown: KeyDetails[][];
keyup: KeyDetails[][];
oldPassword: string;
};
//REDUCERS
export type AuthReducer = {
token: string | null;
candidate: CandidateType | {};
organization: OrganizationType | {};
userType: USER_TYPE;
userId: string;
keystrokeResult?: KeystrokeResultType;
appState?: {
state: APP_STATE;
msg?: string;
} | null;
};
export type Reducers = {
auth: AuthReducer;
};
.card {
border: none;
box-shadow: 0 0 20px 0 rgb(86 153 196 / 15%);
border-radius: 15px;
}
.onboard {
width: 400px;
float: right;
margin-top: 100px;
.usertype-selector {
margin-top: -10px;
margin-bottom: 15px;
.btn {
border-radius: 0;
border-width: 0 0 2px 0;
background-color: white;
font-weight: 500;
color: #0d6efd;
border-bottom-color: #0d6efd;
&.deactive {
color: #d9d9d9;
border-bottom-color: #d9d9d9;
}
}
}
section {
margin-top: 10px;
.btn {
padding: 0;
margin-left: 10px;
font-size: 16px;
margin-bottom: 4px;
}
}
}
.avatar {
background-color: #8eb4f2;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin: auto;
&.lg {
height: 100px;
width: 100px;
border-radius: 100px;
font-size: 60px;
}
&.sm {
height: 35px;
width: 35px;
border-radius: 35px;
font-size: 20px;
}
}
.alert {
button {
&:active,
&:focus {
border: none;
outline: none;
box-shadow: none;
}
}
}
.keystrokes {
.keystrokes-settings {
margin-bottom: 10px;
.form-switch {
margin-top: 5px;
margin-bottom: 5px;
.form-check-label {
font-weight: 500;
}
}
}
}
.appstate {
padding: 10px 20px;
background-color: red;
max-width: 350px;
border-radius: 15px;
box-shadow: 0 0 10px 0 rgba(83, 83, 83, 0.662);
position: fixed;
width: 350px;
bottom: 30px;
right: 30px;
section {
p {
font-weight: bold;
}
}
&.failed {
background: rgb(131, 58, 180);
background: linear-gradient(
153deg,
rgba(131, 58, 180, 1) 0%,
rgba(255, 71, 71, 1) 0%,
rgba(255, 155, 119, 1) 100%
);
}
&.success {
background: rgb(131, 58, 180);
background: linear-gradient(
153deg,
rgba(131, 58, 180, 1) 0%,
rgba(46, 197, 198, 1) 0%,
rgba(216, 255, 134, 1) 100%
);
}
&.loading {
background: rgb(131, 58, 180);
background: linear-gradient(
153deg,
rgba(131, 58, 180, 1) 0%,
rgba(189, 189, 189, 1) 0%,
rgba(134, 188, 255, 1) 100%
);
}
}
import React from "react";
type OwnProps = {
alert?: string | null;
setAlert: (value: string | null) => void;
};
const Alert = ({ alert, setAlert }: OwnProps) => {
const onClose = () => setAlert(null);
if (!alert) return null;
return (
<div className="alert alert-warning alert-dismissible fade show mt-3">
{alert}
<button type="button" className="btn-close" onClick={onClose}></button>
</div>
);
};
export default Alert;
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setAppState } from "../common/actions/auth";
import { APP_STATE, Reducers } from "../common/types";
const AppState = () => {
const dispatch = useDispatch();
const appState = useSelector((state: Reducers) => state.auth.appState);
useEffect(() => {
if (appState?.state !== APP_STATE.LOADING) {
setTimeout(() => {
dispatch(setAppState(null));
}, 5000);
}
}, [appState?.state, dispatch]);
if (!appState) return null;
const state =
appState.state === APP_STATE.LOADING
? "Loading..."
: appState.state === APP_STATE.FAILED
? "Failed"
: "Success";
return (
<div className={`appstate ${appState?.state}`}>
<section>
<p>{state}</p>
</section>
<p>{appState?.msg}</p>
</div>
);
};
export default AppState;
import React from "react";
const Avatar = ({
url,
name,
size = "sm",
}: {
url?: string;
name: string;
size?: "sm" | "md" | "lg";
}) => {
if (url) return <img src={url} alt={name} className={`avatar img ${size}`} />;
return <div className={`avatar ${size}`}>{name[0]}</div>;
};
export default Avatar;
import React, { useRef, useEffect, useState } from "react";
import { KeyDetails, UpdatePasswordPayload } from "../common/types";
import Alert from "../components/Alert";
import { keyEvent } from "../common/lib/util";
import { useDispatch } from "react-redux";
import { changePassword } from "../common/actions/auth";
const ChangePassword = () => {
const dispatch = useDispatch();
const keydownArray = useRef<KeyDetails[][]>([]);
const keyupArray = useRef<KeyDetails[][]>([]);
const keyCount = useRef<number[]>([]);
const [alert, setAlert] = useState<string | null>(null);
const [credentials, setCredentials] = useState({
oldPassword: "",
password: "",
confrimPassword: "",
twoFAPassword: "",
});
useEffect(() => {
resetData();
}, []);
const resetData = () => {
for (let i = 0; i < 3; i++) {
keydownArray.current.push([]);
keyupArray.current.push([]);
keyCount.current.push(0);
}
};
const onChangeCredentials = (e: React.ChangeEvent<HTMLInputElement>) => {
setCredentials({ ...credentials, [e.target.name]: e.target.value });
};
const clearField = (index: number) => {
if (index === 0) {