diff --git a/package-lock.json b/package-lock.json index c4ead49..5ed0926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d7980db..3d5106c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.ts b/server.ts index 31b1368..ea2fae8 100644 --- a/server.ts +++ b/server.ts @@ -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)); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index a06ea46..56a24b9 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -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" }); + } +} diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts new file mode 100644 index 0000000..a22bb84 --- /dev/null +++ b/src/controllers/usersController.ts @@ -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" }); + } +} diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..66f89ef --- /dev/null +++ b/src/middleware/authMiddleware.ts @@ -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(); +} diff --git a/src/models/user.ts b/src/models/user.ts index 326d906..571e9d6 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -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 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, diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index d4f7411..393aa40 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -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; diff --git a/src/routes/usersRoutes.ts b/src/routes/usersRoutes.ts new file mode 100644 index 0000000..805d27b --- /dev/null +++ b/src/routes/usersRoutes.ts @@ -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; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b7817a8..8947f7e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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", diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 88ae0a1..2603e26 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -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, + }, + }); +} diff --git a/src/validator/validator.ts b/src/validator/validator.ts index 5a8bb62..04f42cb 100644 --- a/src/validator/validator.ts +++ b/src/validator/validator.ts @@ -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 (