keystroke back end

parent 9075f16a
......@@ -2,7 +2,6 @@
"name": "keystrokedynamics",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"build": "yarn tsc",
"start": "yarn build && node lib/index.js",
......@@ -13,6 +12,7 @@
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.19",
......@@ -25,6 +25,7 @@
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191",
"@types/node": "^18.15.1",
"eslint": "^7.5.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-airbnb-base": "^14.2.0",
......
......@@ -2,3 +2,14 @@ export const JWT_SECRET = "JWT_SECRET_SMART_RECRUITER";
export const MONGO_URL =
"mongodb+srv://smart_req_admin:CvCbwaX6A4rnJuwv@smartreq.duejzyf.mongodb.net/App?retryWrites=true&w=majority";
export const API_PORT = process.env.API_PORT || 5000;
export const DEFAULT_CONTROLS = {
standard: {
sd: 1.5,
threshold: 65,
use: true,
},
fullStandard: {
threshold: 1,
use: true,
},
};
......@@ -44,6 +44,7 @@ export type AuthType = {
password: string;
keystrokeDataTimestamps: number[];
keystrokeData: KeystrokeDataType;
controls?: ControlsType;
};
export type CandidateType = {
......@@ -101,3 +102,18 @@ export type SignUpPayload = {
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;
};
import express from "express";
import * as mongoose from "mongoose";
import mongoose from "mongoose";
import cors from "cors";
import { API_PORT, MONGO_URL } from "./config/contants";
const app = express();
app.use(cors());
// Routes
const userRoute = require("./routes/user");
const authRoute = require("./routes/auth");
// Environment constants
// Service Initialisation
mongoose.connect(MONGO_URL, {
useFindAndModify: false,
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
});
// const db = mongoose.connection;
// db.on("error", (error) => console.log(error, "connection error:"));
// db.once("open", () => {
// console.log("Connected to MongoDB Instance");
// });
// Express Initialisation
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
......@@ -23,6 +34,6 @@ app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Routes
app.use("/user", userRoute);
app.use("/auth", authRoute);
app.listen(API_PORT, () => console.log(`Listening on port ${API_PORT}`));
......@@ -7,14 +7,14 @@ export const authMiddleware = (
res: Response,
next: NextFunction
) => {
const token = req.header("Authorization");
const token = req.header("auth_token");
if (!token) {
return res.status(401).send("Authorization denied");
} else {
const decodedToken = <jwt.JwtPayload>jwt.verify(token, JWT_SECRET);
if (decodedToken) {
req.query.id = decodedToken.id;
req.query.userId = decodedToken.userId;
return next();
} else {
return res.status(401).send("Unauthorized Access");
......
......@@ -49,6 +49,17 @@ const authSchema = new Schema<AuthType>({
covMatrix: [[Number]],
},
},
controls: {
standard: {
sd: { type: Number, default: 1.5, require: false },
threshold: { type: Number, default: 65, require: false },
use: { type: Boolean, default: true, require: false },
},
fullStandard: {
threshold: { type: Number, default: 1, require: false },
use: { type: Boolean, default: true, require: false },
},
},
});
const Auth = model<AuthType>("Auth", authSchema);
......
import * as express from "express";
import { Router } from "express";
import * as _ from "lodash";
import * as jwt from "jsonwebtoken";
import * as bcrypt from "bcryptjs";
import {
ControlsType,
KeystrokeDataType,
SignInPayload,
SignUpPayload,
TypedRequest,
UpdatePasswordPayload,
USER_TYPE,
} from "../config/types";
import Auth from "../models/Auth";
......@@ -18,11 +21,12 @@ import {
addAttemptToKeystrokeData,
computeDataTendencies,
processAttempt,
updatePasswordFromProcessedData,
} from "../utilities/keystroke";
import { JWT_SECRET } from "../config/contants";
import { DEFAULT_CONTROLS, JWT_SECRET } from "../config/contants";
import { authMiddleware } from "../middlewares/auth";
const router = express.Router();
const router = Router();
router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => {
const {
......@@ -41,7 +45,7 @@ router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => {
if (auth) {
return res
.status(403)
.json({ success: false, msg: "User already exists in DB" });
.json({ success: false, msg: "User already exists!" });
}
const processedAttempts: KeystrokeDataType[] = Array(attemptCount)
......@@ -88,8 +92,8 @@ router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => {
}
});
router.post("/login", async (req, res) => {
const { password, keydown, keyup, email, controls, userType } = req.body;
router.post("/login", async (req: TypedRequest<{}, SignInPayload>, res) => {
const { password, keydown, keyup, email, userType } = req.body;
const auth = await Auth.findOne({ email });
......@@ -106,21 +110,14 @@ router.post("/login", async (req, res) => {
const isMatch = await bcrypt.compare(password, auth.password);
const credentialsValid =
isMatch &&
_.isEqual(processedAttempt.hold.keys, auth.keystrokeData.hold.keys) &&
_.isEqual(processedAttempt.flight.keys, auth.keystrokeData.flight.keys) &&
_.isEqual(processedAttempt.dd.keys, auth.keystrokeData.dd.keys) &&
_.isEqual(processedAttempt.full.keys, auth.keystrokeData.full.keys);
if (!credentialsValid) {
if (!isMatch) {
return res.status(403).json({ success: false, msg: "Invalid Credentials" });
}
const result = processAttempt({
userKeystrokeData: auth.keystrokeData,
attemptKeystrokeData: processedAttempt,
controls,
controls: auth.controls || DEFAULT_CONTROLS,
});
if (result.accepted) {
......@@ -130,46 +127,49 @@ router.post("/login", async (req, res) => {
});
await Auth.updateOne({ email }, { $set: newUserData });
}
let user;
let user;
if (userType === USER_TYPE.CANDIDATE) {
user = await Candidates.findById(auth.userId);
} else {
user = await Organizations.findById(auth.userId);
}
if (userType === USER_TYPE.CANDIDATE) {
user = await Candidates.findById(auth.userId);
} else {
user = await Organizations.findById(auth.userId);
}
const token = await jwt.sign(auth, JWT_SECRET, { expiresIn: "2h" });
return res.json({
user,
result,
token,
db: {
hold: auth.keystrokeData.hold.means,
flight: auth.keystrokeData.flight.means,
dd: auth.keystrokeData.dd.means,
full: auth.keystrokeData.full.means,
},
filteredDb: {
hold: auth.keystrokeData.hold.filteredMeans,
flight: auth.keystrokeData.flight.filteredMeans,
dd: auth.keystrokeData.dd.filteredMeans,
full: auth.keystrokeData.full.filteredMeans,
},
attempt: {
hold: processedAttempt.hold.times,
flight: processedAttempt.flight.times,
dd: processedAttempt.dd.times,
full: processedAttempt.full.times,
},
});
});
const token = await jwt.sign({ userId: auth.userId }, JWT_SECRET, {
expiresIn: "2h",
});
router.post("/validateKeystroke", authMiddleware, (req, res) => {
const processedKeystrokeData = processKeystrokeData(req.body);
res.json(processedKeystrokeData);
return res.json({
success: true,
user,
token,
keystrokeResult: {
result,
db: {
hold: auth.keystrokeData.hold.means,
flight: auth.keystrokeData.flight.means,
dd: auth.keystrokeData.dd.means,
full: auth.keystrokeData.full.means,
},
filteredDb: {
hold: auth.keystrokeData.hold.filteredMeans,
flight: auth.keystrokeData.flight.filteredMeans,
dd: auth.keystrokeData.dd.filteredMeans,
full: auth.keystrokeData.full.filteredMeans,
},
attempt: {
hold: processedAttempt.hold.times,
flight: processedAttempt.flight.times,
dd: processedAttempt.dd.times,
full: processedAttempt.full.times,
},
controls: auth.controls || DEFAULT_CONTROLS,
},
});
} else {
return res.status(403).json({ success: false, msg: "Invalid Credentials" });
}
});
router.get("/tendencies/:userId", authMiddleware, async (req, res) => {
......@@ -187,4 +187,72 @@ router.get("/tendencies/:userId", authMiddleware, async (req, res) => {
.catch((error) => res.status(500).send(error));
});
router.post(
"/update",
authMiddleware,
async (req: TypedRequest<{ userId: string }, ControlsType>, res) => {
try {
await Auth.updateOne(
{ userId: req.query.userId },
{ $set: { controls: req.body } }
);
return res.send("SUCCESS");
} catch (error) {
return res.status(400).send("FAILED");
}
}
);
router.post(
"/change-pwd",
authMiddleware,
async (req: TypedRequest<{ userId: string }, UpdatePasswordPayload>, res) => {
const { passwords, keydown, keyup, oldPassword } = req.body;
const attemptCount = passwords.length;
const auth = await Auth.findOne({ userId: req.query.userId });
const isMatch = await bcrypt.compare(oldPassword, auth.password);
if (!isMatch) {
return res
.status(403)
.json({ success: false, msg: "Invalid Credentials" });
}
const processedAttempts: KeystrokeDataType[] = Array(attemptCount)
.fill(0)
.map((v, i) =>
processKeystrokeData({ keydown: keydown[i], keyup: keyup[i] })
);
const passwordsEqual = processedAttempts.every((v) =>
_.isEqual(v.full.keys, processedAttempts[0].full.keys)
);
if (!passwordsEqual) {
return res
.status(403)
.json({ success: false, msg: "Passwords don't match" });
}
try {
const newPasswordData = await updatePasswordFromProcessedData(
passwords,
processedAttempts
);
Auth.updateOne({ userId: req.query.userId }, { $set: newPasswordData });
return res.json({
success: true,
});
} catch (error) {
return res
.status(500)
.json({ success: false, msg: "Error signing up", error });
}
}
);
module.exports = router;
......@@ -406,3 +406,68 @@ export const processAttempt = ({
return result;
};
export const updatePasswordFromProcessedData = async (
passwords: string[],
processedData: KeystrokeDataType[] | any
) => {
const keystroke = {
keys: [],
times: [],
sums: [],
means: [],
sd: [],
filteredMeans: [],
filteredSd: [],
covMatrix: [],
};
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(passwords[0], salt);
let newPasswordData: Partial<AuthType> = {
password: hashedPassword,
keystrokeDataTimestamps: [],
keystrokeData: {
hold: keystroke,
flight: keystroke,
dd: keystroke,
full: keystroke,
},
};
newPasswordData = processedData.reduce((acc, v, i) => {
TYPES.map((type) => {
const key = type as keyof KeystrokeDataType;
if (i === 0) {
// Keys of the processed data is checked for equality
// So the first one is used for the dataset
acc.keystrokeData[key].keys = v[key].keys;
const length = v[key].keys.length;
// Length of the processedData array is the number of attempts
// Times is an array of arrays of each attempt
acc.keystrokeData[key].times = Array(v.length).fill([]);
acc.keystrokeData[key].means = Array(length).fill(0);
acc.keystrokeData[key].sd = Array(length).fill(0);
acc.keystrokeData[key].filteredMeans = Array(length).fill(0);
acc.keystrokeData[key].filteredSd = Array(length).fill(0);
}
acc.keystrokeData[key].times[i] = v[key].times;
return type;
});
acc.keystrokeDataTimestamps.push(Date.now());
return acc;
}, newPasswordData);
newPasswordData.keystrokeData = computeDataTendencies(
newPasswordData.keystrokeData
);
return newPasswordData;
};
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": false,
"outDir": "lib",
"sourceMap": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "commonjs",
"lib": ["es6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"outDir": "lib",
"rootDir": "src",
"strict": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"esModuleInterop": true,
"resolveJsonModule": true
},
"compileOnSave": true,
"include": ["src"]
......
......@@ -140,6 +140,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-18.11.14.tgz"
integrity sha512-0KXV57tENYmmJMl+FekeW9V3O/rlcqGQQJ/hNh9r8pKIj304pskWuEd8fCyNT86g/TpO0gcOTiLzsHLEURFMIQ==
"@types/node@^18.15.1":
version "18.15.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.1.tgz#41dc2bf78e8085a250d4670d95edb7fba621dd29"
integrity sha512-U2TWca8AeHSmbpi314QBESRk7oPjSZjDsR+c+H4ECC1l+kFgpZf8Ydhv3SJpPy51VyZHHqxlb6mTTqYNNRVAIw==
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
......@@ -478,6 +483,14 @@ core-util-is@~1.0.0:
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
......@@ -1528,6 +1541,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.12.2, object-inspect@^1.9.0:
version "1.12.2"
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz"
......@@ -2114,7 +2132,7 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
vary@~1.1.2:
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
......
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