employees

main
alex 2024-01-11 01:15:23 +01:00
parent dae4b4a1ed
commit 366e3f3b23
12 changed files with 365 additions and 9 deletions

23
package-lock.json generated
View File

@ -12,6 +12,7 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mariadb": "^3.2.3",
@ -24,6 +25,7 @@
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.6",
@ -134,6 +136,15 @@
"@types/node": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -648,6 +659,18 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",

View File

@ -14,6 +14,7 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mariadb": "^3.2.3",
@ -26,6 +27,7 @@
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.6",

View File

@ -2,8 +2,10 @@ import express, { Express } from "express";
import dotenv from "dotenv";
import bodyParser from "body-parser";
import swaggerUI from "swagger-ui-express";
import cors from "cors";
import userRoutes from "./src/routes/userRoutes";
import usersRoutes from "./src/routes/usersRoutes";
dotenv.config();
@ -39,8 +41,11 @@ const options = {
apis: ["./src/routes/*.ts"],
};
// TODO: add cors
app.use(cors());
app.use(bodyParser.json());
app.use("/api/v1/user", userRoutes);
app.use("/api/v1/users", usersRoutes);
const specs = swaggerJsDoc(options);
app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs));

View File

@ -1,22 +1,28 @@
import { Request, Response } from "express";
import logger from "../logger/logger";
import User from "../models/user";
import { isPasswordValid, isUsernameValid } from "../validator/validator";
import {
isAccountNameValid,
isPasswordValid,
isUsernameValid,
} from "../validator/validator";
import { Roles } from "../utils/constants";
import {
decodeBase64,
getUserSession,
hashPassword,
matchPassword,
newUserId,
saveSession,
} from "../utils/utils";
export async function SignUp(req: Request, res: Response) {
try {
const { username, password } = req.body;
let { username, accountName, password } = req.body;
// validate request
if (!username || !password) {
if (!username || !accountName || !password) {
return res.status(400).send({ err: "invalid request" });
}
@ -24,16 +30,25 @@ export async function SignUp(req: Request, res: Response) {
return res.status(400).send({ err: "invalid request" });
}
if (!isAccountNameValid(accountName)) {
return res.status(400).send({ err: "invalid request" });
}
accountName = accountName.toLowerCase();
// check if user already exists
const existingUser = await User.findOne({
where: {
username: username,
account_name: accountName,
},
});
if (existingUser) {
logger.debug("User already exists with this username: %s", username);
logger.debug(
"User already exists with this accountName: %s",
accountName
);
return res.status(400).send({ err: "invalid request" });
}
@ -46,6 +61,8 @@ export async function SignUp(req: Request, res: Response) {
return res.status(400).send({ err: "invalid request" });
}
// hash password
const hashedPassword = await hashPassword(decodedPassword);
// create user
@ -53,12 +70,18 @@ export async function SignUp(req: Request, res: Response) {
await User.create({
user_id: newUserId(),
role: Roles.Master,
account_name: accountName,
username: username,
password: hashedPassword,
})
.then((user) => {
logger.debug("User created with username: %s", user.username);
logger.debug(
"User created with accountName: %s username: %s",
user.account_name,
user.username
);
// create session
saveSession(res, user.user_id, user.username);
})
.catch((err) => {
@ -70,3 +93,102 @@ export async function SignUp(req: Request, res: Response) {
res.status(500).send({ err: "invalid request" });
}
}
export async function Login(req: Request, res: Response) {
try {
let { accountName, password } = req.body;
// validate request
if (!accountName || !password) {
return res.status(400).send({ err: "invalid request" });
}
accountName = accountName.toLowerCase();
if (!isAccountNameValid(accountName)) {
return res.status(400).send({ err: "invalid request" });
}
// check if user exists
const user = await User.findOne({
where: {
account_name: accountName,
},
});
if (!user) {
logger.debug(
"User does not exist with this accountName: %s",
accountName
);
return res.status(400).send({ err: "invalid request" });
}
// decode password
const decodedPassword = decodeBase64(password);
if (!isPasswordValid(decodedPassword)) {
logger.debug("Password is not valid");
return res.status(400).send({ err: "invalid request" });
}
// compare password
const match = await matchPassword(decodedPassword, user.password);
if (!match) {
logger.debug("Password is not valid");
return res.status(400).send({ err: "invalid request" });
}
// create session
saveSession(res, user.user_id, user.username);
} catch (error) {
logger.error(error);
res.status(500).send({ err: "invalid request" });
}
}
export async function Logout(req: Request, res: Response) {
try {
const session = await getUserSession(req);
if (!session) {
return res.status(401).send({ err: "unauthorized" });
}
await session.destroy();
res.status(200).send({ msg: "logout successful" });
} catch (error) {
logger.error(error);
res.status(500).send({ err: "invalid request" });
}
}
export async function GetUser(req: Request, res: Response) {
try {
const session = await getUserSession(req);
if (!session) {
return res.status(401).send({ err: "unauthorized" });
}
const user = await User.findOne({
where: {
user_id: session.user_id,
},
});
if (!user) {
return res.status(401).send({ err: "unauthorized" });
}
res.status(200).send({ username: user.username });
} catch (error) {
logger.error(error);
res.status(500).send({ err: "invalid request" });
}
}

View File

@ -0,0 +1,143 @@
import { Request, Response } from "express";
import logger from "../logger/logger";
import {
isAccountNameValid,
isPasswordValid,
isUsernameValid,
} from "../validator/validator";
import {
decodeBase64,
getUserSession,
hashPassword,
newUserId,
} from "../utils/utils";
import User from "../models/user";
import { Roles } from "../utils/constants";
export async function AddEmployee(req: Request, res: Response) {
try {
let { username, accountName, password } = req.body;
// validate request
if (!username || !accountName || !password) {
return res.status(400).send({ err: "invalid request" });
}
// verify if requester is a store master
const requesterSession = await getUserSession(req);
if (!requesterSession) {
logger.debug("Requester session not found");
return res.status(401).send({ err: "unauthorized" });
}
const requester = await User.findOne({
where: {
user_id: requesterSession.user_id,
},
});
if (!requester) {
logger.debug("Requester not found");
return res.status(401).send({ err: "unauthorized" });
}
if (requester.role !== Roles.Master) {
logger.debug("Requester is not a store master");
return res.status(401).send({ err: "unauthorized" });
}
// validate username and account name
accountName = accountName.toLowerCase();
if (!isUsernameValid(username)) {
return res.status(400).send({ err: "invalid request" });
}
if (!isAccountNameValid(accountName)) {
return res.status(400).send({ err: "invalid request" });
}
// check if user already exists
const existingUser = await User.findOne({
where: {
account_name: accountName,
},
});
if (existingUser) {
logger.debug(
"User already exists with this accountName: %s",
accountName
);
return res.status(400).send({ err: "invalid request" });
}
// decode password
const decodedPassword = decodeBase64(password);
if (!isPasswordValid(decodedPassword)) {
logger.debug("Password is not valid");
return res.status(400).send({ err: "invalid request" });
}
// hash password
const hashedPassword = await hashPassword(decodedPassword);
// create user
await User.create({
user_id: newUserId(),
role: Roles.Worker,
account_name: accountName,
username: username,
password: hashedPassword,
master_user_id: requester.user_id,
})
.then(() => {
res.status(200).send({ msg: "success" });
})
.catch((err) => {
logger.error(err);
res.status(500).send({ err: "invalid request" });
});
} catch (error) {
logger.error(error);
res.status(500).send({ err: "invalid request" });
}
}
export async function GetEmployees(req: Request, res: Response) {
try {
const requesterSession = await getUserSession(req);
if (!requesterSession) {
logger.debug("Requester session not found");
return res.status(401).send({ err: "unauthorized" });
}
// find all employees of the requester and select only the username and account name
const employees = await User.findAll({
where: {
master_user_id: requesterSession.user_id,
},
attributes: ["username", "account_name"],
});
// simulate a delay
await new Promise((resolve) => setTimeout(resolve, 4000));
res.status(200).send({ employees: employees });
} catch (error) {
logger.error(error);
res.status(500).send({ err: "invalid request" });
}
}

View File

@ -0,0 +1,12 @@
import { Request } from "express";
import { getUserSession } from "../utils/utils";
export async function sessionProtection(req: Request, res: any, next: any) {
const session = await getUserSession(req);
if (!session) {
return res.status(401).send({ err: "unauthorized" });
}
next();
}

View File

@ -5,6 +5,7 @@ interface UserAttributes {
user_id: string;
master_user_id?: string;
role: string;
account_name: string;
username: string;
password: string;
calendar_settings?: string;
@ -14,6 +15,7 @@ class User extends Model<UserAttributes> implements UserAttributes {
declare user_id: string;
declare master_user_id: string;
declare role: string;
declare account_name: string;
declare username: string;
declare password: string;
declare calendar_settings: string;
@ -35,6 +37,10 @@ User.init(
type: DataTypes.STRING,
// allowNull defaults to true
},
account_name: {
type: DataTypes.STRING,
unique: true,
},
username: {
type: DataTypes.STRING,
allowNull: false,

View File

@ -2,7 +2,11 @@ import express from "express";
const router = express.Router();
import * as userController from "../controllers/userController";
import { sessionProtection } from "../middleware/authMiddleware";
router.post("/signup", userController.SignUp);
router.post("/auth/signup", userController.SignUp);
router.post("/auth/login", userController.Login);
router.delete("/auth/logout", sessionProtection, userController.Logout);
router.get("/", sessionProtection, userController.GetUser);
export default router;

View File

@ -0,0 +1,9 @@
import express from "express";
const router = express.Router();
import * as usersController from "../controllers/usersController";
router.post("/", usersController.AddEmployee);
router.get("/", usersController.GetEmployees);
export default router;

View File

@ -3,9 +3,15 @@ export const DEFAULT_SESSION_EXPIRY = 365 * 24 * 60 * 60 * 1000; // 365 days
export const USERNAME_MIN_LENGTH = 3;
export const USERNAME_MAX_LENGTH = 20;
export const ACCOUNT_NAME_MIN_LENGTH = 3;
export const ACCOUNT_NAME_MAX_LENGTH = 20;
export const PASSWORD_MIN_LENGTH = 8;
export const PASSWORD_MAX_LENGTH = 64;
// Header name for the session ID
export const HEADER_X_AUTHORIZATION: string = "x-authorization";
export const Roles = {
// admin of the whole system independent of stores
Admin: "admin",

View File

@ -1,9 +1,9 @@
import crypto from "crypto";
import bcrypt from "bcrypt";
import { v4 as uuidv4 } from "uuid";
import { Response } from "express";
import { Request, Response } from "express";
import Session from "../models/session";
import { DEFAULT_SESSION_EXPIRY } from "./constants";
import { DEFAULT_SESSION_EXPIRY, HEADER_X_AUTHORIZATION } from "./constants";
export async function matchPassword(decodedPassword: string, password: string) {
return await bcrypt.compare(decodedPassword, password);
@ -48,3 +48,17 @@ export async function saveSession(
res.status(500).send({ err: "invalid request" });
}
}
export async function getUserSession(req: Request) {
const sessionId = req.get(HEADER_X_AUTHORIZATION);
if (!sessionId) {
return null;
}
return await Session.findOne({
where: {
session_id: sessionId,
},
});
}

View File

@ -3,6 +3,8 @@ import {
USERNAME_MAX_LENGTH,
PASSWORD_MIN_LENGTH,
PASSWORD_MAX_LENGTH,
ACCOUNT_NAME_MIN_LENGTH,
ACCOUNT_NAME_MAX_LENGTH,
} from "../utils/constants";
// TODO: regex for username
@ -13,6 +15,14 @@ export function isUsernameValid(username: string) {
);
}
// TODO: regex for account name
export function isAccountNameValid(accountName: string) {
return (
accountName.length >= ACCOUNT_NAME_MIN_LENGTH &&
accountName.length <= ACCOUNT_NAME_MAX_LENGTH
);
}
// TODO: regex for password
export function isPasswordValid(password: string) {
return (