From da0814bcf55b9b6c1e9e6422651fc64cd157746c Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 13 Jan 2024 12:07:00 +0100 Subject: [PATCH] services --- server.ts | 4 + src/controllers/storeController.ts | 41 +++ src/controllers/storeServicesController.ts | 338 +++++++++++++++++++++ src/controllers/userController.ts | 113 ++++++- src/controllers/usersController.ts | 203 ++++++++++++- src/models/index.ts | 13 +- src/models/store.ts | 69 +++++ src/models/storeService.ts | 42 +++ src/models/storeServiceActivity.ts | 60 ++++ src/models/storeServiceActivityUsers.ts | 37 +++ src/models/user.ts | 33 +- src/routes/storeRoutes.ts | 8 + src/routes/storeServicesRoutes.ts | 19 ++ src/routes/userRoutes.ts | 1 + src/routes/usersRoutes.ts | 4 +- src/utils/constants.ts | 28 ++ src/utils/utils.ts | 20 +- src/validator/validator.ts | 89 +++++- 18 files changed, 1074 insertions(+), 48 deletions(-) create mode 100644 src/controllers/storeController.ts create mode 100644 src/controllers/storeServicesController.ts create mode 100644 src/models/store.ts create mode 100644 src/models/storeService.ts create mode 100644 src/models/storeServiceActivity.ts create mode 100644 src/models/storeServiceActivityUsers.ts create mode 100644 src/routes/storeRoutes.ts create mode 100644 src/routes/storeServicesRoutes.ts diff --git a/server.ts b/server.ts index ea2fae8..182e29b 100644 --- a/server.ts +++ b/server.ts @@ -4,6 +4,8 @@ import bodyParser from "body-parser"; import swaggerUI from "swagger-ui-express"; import cors from "cors"; +import storeRoutes from "./src/routes/storeRoutes"; +import storeServicesRoutes from "./src/routes/storeServicesRoutes"; import userRoutes from "./src/routes/userRoutes"; import usersRoutes from "./src/routes/usersRoutes"; @@ -44,6 +46,8 @@ const options = { // TODO: add cors app.use(cors()); app.use(bodyParser.json()); +app.use("/api/v1/store", storeRoutes); +app.use("/api/v1/store/services", storeServicesRoutes); app.use("/api/v1/user", userRoutes); app.use("/api/v1/users", usersRoutes); diff --git a/src/controllers/storeController.ts b/src/controllers/storeController.ts new file mode 100644 index 0000000..3684f83 --- /dev/null +++ b/src/controllers/storeController.ts @@ -0,0 +1,41 @@ +import { Request, Response } from "express"; +import Store from "../models/store"; +import { getUserSession } from "../utils/utils"; +import logger from "../logger/logger"; + +export async function GetStore(req: Request, res: Response) { + try { + const { storeId } = req.params; + + if (!storeId) { + return res.status(400).send({ err: "invalid request" }); + } + + // check if requester is the store owner + + const userSession = await getUserSession(req); + + if (!userSession) { + return res.status(401).send({ err: "unauthorized" }); + } + + const store = await Store.findOne({ + where: { + store_id: storeId, + }, + attributes: ["owner_user_id", "name"], + }); + + if (!store) { + return res.status(400).send({ err: "invalid request" }); + } + + if (store.owner_user_id !== userSession.user_id) { + return res.status(401).send({ err: "unauthorized" }); + } + + res.status(200).send({ store: { name: store.name } }); + } catch (error) { + res.status(500).send({ err: "invalid request" }); + } +} diff --git a/src/controllers/storeServicesController.ts b/src/controllers/storeServicesController.ts new file mode 100644 index 0000000..4de574b --- /dev/null +++ b/src/controllers/storeServicesController.ts @@ -0,0 +1,338 @@ +import { Request, Response } from "express"; +import StoreService from "../models/storeService"; +import { + getUserSession, + newStoreServiceActivityId, + newStoreServiceId, +} from "../utils/utils"; +import { + isStoreServiceActivityDescriptionValid, + isStoreServiceActivityDurationValid, + isStoreServiceActivityNameValid, + isStoreServiceActivityPriceValid, + isStoreServiceNameValid, +} from "../validator/validator"; +import Store from "../models/store"; +import logger from "../logger/logger"; +import StoreServiceActivity from "../models/storeServiceActivity"; +import StoreServiceActivityUsers from "../models/storeServiceActivityUsers"; +import User from "../models/user"; + +export async function GetStoreServices(req: Request, res: Response) { + try { + const { storeId } = req.params; + + if (!storeId) { + logger.debug("Invalid request"); + return res.status(400).send({ err: "invalid request" }); + } + + const services = await StoreService.findAll({ + where: { + store_id: storeId, + }, + attributes: ["service_id", "name"], + }); + + // get all users assigned to the store + // this user list will be used in the ui to show if no users are selected for a service activity + + const users = await User.findAll({ + where: { + store_id: storeId, + }, + attributes: ["user_id", "username"], + }); + + res.status(200).send({ services, users }); + } catch (error) { + console.log(error); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function CreateStoreService(req: Request, res: Response) { + try { + const { storeId, name } = req.body; + + if (!storeId || !name || !isStoreServiceNameValid(name)) { + logger.debug("Invalid request"); + return res.status(400).send({ err: "invalid request" }); + } + + // check if requester is the store owner + + const userSession = await getUserSession(req); + + if (!userSession) { + return res.status(401).send({ err: "unauthorized" }); + } + + const store = await Store.findOne({ + where: { + store_id: storeId, + }, + }); + + if (!store) { + return res.status(400).send({ err: "invalid request" }); + } + + if (store.owner_user_id !== userSession.user_id) { + return res.status(401).send({ err: "unauthorized" }); + } + + // create store service + + const service = await StoreService.create({ + service_id: newStoreServiceId(), + store_id: storeId, + name: name, + }); + + res.status(200).send({ service }); + } catch (error) { + console.log(error); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function UpdateStoreService(req: Request, res: Response) { + try { + const { serviceId, name } = req.body; + + if (!serviceId || !name || !isStoreServiceNameValid(name)) { + logger.debug("Invalid request"); + return res.status(400).send({ err: "invalid request" }); + } + + // check if requester is the store owner + + const userSession = await getUserSession(req); + + if (!userSession) { + return res.status(401).send({ err: "unauthorized" }); + } + + const service = await StoreService.findOne({ + where: { + service_id: serviceId, + }, + }); + + if (!service) { + return res.status(400).send({ err: "invalid request" }); + } + + const store = await Store.findOne({ + where: { + store_id: service.store_id, + }, + }); + + if (!store) { + return res.status(400).send({ err: "invalid request" }); + } + + if (store.owner_user_id !== userSession.user_id) { + return res.status(401).send({ err: "unauthorized" }); + } + + // update store service + + await StoreService.update( + { + name: name, + }, + { + where: { + service_id: serviceId, + }, + } + ); + + res.status(200).send({ msg: "success" }); + } catch (error) { + console.log(error); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function CreateStoreServiceActivity(req: Request, res: Response) { + try { + const { serviceId, name, description, price, duration } = req.body; + + if ( + !serviceId || + !name || + !isStoreServiceActivityNameValid(name) || + !isStoreServiceActivityDescriptionValid(description) || + !isStoreServiceActivityPriceValid(price) || + !isStoreServiceActivityDurationValid(duration) + ) { + logger.debug("Invalid request"); + return res.status(400).send({ err: "invalid request" }); + } + + // check if requester is the store owner + + const userSession = await getUserSession(req); + + if (!userSession) { + return res.status(401).send({ err: "unauthorized" }); + } + + const service = await StoreService.findOne({ + where: { + service_id: serviceId, + }, + }); + + if (!service) { + return res.status(400).send({ err: "invalid request" }); + } + + const store = await Store.findOne({ + where: { + store_id: service.store_id, + }, + }); + + if (!store) { + return res.status(400).send({ err: "invalid request" }); + } + + if (store.owner_user_id !== userSession.user_id) { + return res.status(401).send({ err: "unauthorized" }); + } + + // create store service activity + + const activity = await StoreServiceActivity.create({ + activity_id: newStoreServiceActivityId(), + service_id: serviceId, + name: name, + description: description, + price: price, + duration: duration, + }); + + res.status(200).send({ activity }); + } catch (error) { + console.log(error); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function GetStoreServiceActivities(req: Request, res: Response) { + try { + const { storeId, serviceId } = req.params; + + if (!storeId || !serviceId) { + return res.status(400).send({ err: "invalid request" }); + } + + const activities = await StoreServiceActivity.findAll({ + where: { + service_id: serviceId, + }, + attributes: ["activity_id", "name", "description", "price", "duration"], + }); + + /* + activities.forEach(async (activity) => { + const assignedUsers = await StoreServiceActivityUsers.findAll({ + where: { + activity_id: activity.activity_id, + }, + attributes: ["user_id"], + }); + }); */ + + res.status(200).send({ activities }); + } catch (error) { + console.log(error); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function UpdateStoreServiceActivity(req: Request, res: Response) { + try { + const { activityId, name, description, price, duration } = req.body; + + if ( + !activityId || + !name || + !isStoreServiceActivityNameValid(name) || + !isStoreServiceActivityDescriptionValid(description) || + !isStoreServiceActivityPriceValid(price) || + !isStoreServiceActivityDurationValid(duration) + ) { + logger.debug("Invalid request"); + return res.status(400).send({ err: "invalid request" }); + } + + // check if requester is the store owner + + const userSession = await getUserSession(req); + + if (!userSession) { + return res.status(401).send({ err: "unauthorized" }); + } + + const activity = await StoreServiceActivity.findOne({ + where: { + activity_id: activityId, + }, + }); + + if (!activity) { + return res.status(400).send({ err: "invalid request" }); + } + + const service = await StoreService.findOne({ + where: { + service_id: activity.service_id, + }, + }); + + if (!service) { + return res.status(400).send({ err: "invalid request" }); + } + + const store = await Store.findOne({ + where: { + store_id: service.store_id, + }, + }); + + if (!store) { + return res.status(400).send({ err: "invalid request" }); + } + + if (store.owner_user_id !== userSession.user_id) { + return res.status(401).send({ err: "unauthorized" }); + } + + // update store service activity + + await StoreServiceActivity.update( + { + name: name, + description: description, + price: price, + duration: duration, + }, + { + where: { + activity_id: activityId, + }, + } + ); + + res.status(200).send({ msg: "success" }); + } catch (error) { + console.log(error); + res.status(500).send({ err: "invalid request" }); + } +} diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 56a24b9..18822b6 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -6,15 +6,24 @@ import { isPasswordValid, isUsernameValid, } from "../validator/validator"; -import { Roles } from "../utils/constants"; +import { + CALENDAR_MAX_FUTURE_BOOKING_DAYS, + CALENDAR_MAX_SERVICE_DURATION, + CALENDAR_MIN_EARLIEST_BOOKING_TIME, + CALENDAR_PRIMARY_CALENDAR_ID, + CALENDAR_USING_PRIMARY_CALENDAR, + Roles, +} from "../utils/constants"; import { decodeBase64, getUserSession, hashPassword, matchPassword, + newStoreId, newUserId, saveSession, } from "../utils/utils"; +import Store from "../models/store"; export async function SignUp(req: Request, res: Response) { try { @@ -65,24 +74,51 @@ export async function SignUp(req: Request, res: Response) { const hashedPassword = await hashPassword(decodedPassword); - // create user + // create store - await User.create({ - user_id: newUserId(), - role: Roles.Master, - account_name: accountName, - username: username, - password: hashedPassword, + let userId = newUserId(); + + Store.create({ + store_id: newStoreId(), + owner_user_id: userId, + name: username, + calendar_max_future_booking_days: CALENDAR_MAX_FUTURE_BOOKING_DAYS, + calendar_min_earliest_booking_time: CALENDAR_MIN_EARLIEST_BOOKING_TIME, + calendar_primary_calendar_id: CALENDAR_PRIMARY_CALENDAR_ID, + calendar_using_primary_calendar: CALENDAR_USING_PRIMARY_CALENDAR, + calendar_max_service_duration: CALENDAR_MAX_SERVICE_DURATION, }) - .then((user) => { + .then(async (store) => { logger.debug( - "User created with accountName: %s username: %s", - user.account_name, - user.username + "Store created with storeId: %s storeName: %s", + store.store_id, + store.name ); - // create session - saveSession(res, user.user_id, user.username); + // create user + + await User.create({ + user_id: userId, + store_id: store.store_id, + role: Roles.Master, + account_name: accountName, + username: username, + password: hashedPassword, + }) + .then((user) => { + 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) => { + logger.error(err); + res.status(500).send({ err: "invalid request" }); + }); }) .catch((err) => { logger.error(err); @@ -180,13 +216,60 @@ export async function GetUser(req: Request, res: Response) { where: { user_id: session.user_id, }, + attributes: ["user_id", "username"], }); if (!user) { return res.status(401).send({ err: "unauthorized" }); } - res.status(200).send({ username: user.username }); + const stores = await Store.findAll({ + where: { + owner_user_id: user.user_id, + }, + attributes: ["store_id", "name"], + }); + + res.status(200).send({ user, stores }); + } catch (error) { + logger.error(error); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function IsAccountNameAvailable(req: Request, res: Response) { + try { + let { accountName } = req.body; + + // validate request + + if (!accountName) { + 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 already exists with this accountName: %s", + accountName + ); + return res.status(400).send({ err: "invalid request" }); + } + + res.status(200).send({ msg: "account name available" }); } catch (error) { logger.error(error); res.status(500).send({ err: "invalid request" }); diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index a22bb84..2425026 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -3,6 +3,7 @@ import logger from "../logger/logger"; import { isAccountNameValid, isPasswordValid, + isUserIdValid, isUsernameValid, } from "../validator/validator"; import { @@ -16,11 +17,11 @@ import { Roles } from "../utils/constants"; export async function AddEmployee(req: Request, res: Response) { try { - let { username, accountName, password } = req.body; + let { storeId, username, accountName, password } = req.body; // validate request - if (!username || !accountName || !password) { + if (!storeId || !username || !accountName || !password) { return res.status(400).send({ err: "invalid request" }); } @@ -53,11 +54,10 @@ export async function AddEmployee(req: Request, res: Response) { accountName = accountName.toLowerCase(); - if (!isUsernameValid(username)) { - return res.status(400).send({ err: "invalid request" }); - } - - if (!isAccountNameValid(accountName)) { + if ( + !isUsernameValid(username) || + !(await isAccountNameValid(accountName)) + ) { return res.status(400).send({ err: "invalid request" }); } @@ -94,11 +94,11 @@ export async function AddEmployee(req: Request, res: Response) { await User.create({ user_id: newUserId(), + store_id: storeId, role: Roles.Worker, account_name: accountName, username: username, password: hashedPassword, - master_user_id: requester.user_id, }) .then(() => { res.status(200).send({ msg: "success" }); @@ -115,6 +115,8 @@ export async function AddEmployee(req: Request, res: Response) { export async function GetEmployees(req: Request, res: Response) { try { + let { storeId } = req.params; + const requesterSession = await getUserSession(req); if (!requesterSession) { @@ -126,18 +128,191 @@ export async function GetEmployees(req: Request, res: Response) { const employees = await User.findAll({ where: { - master_user_id: requesterSession.user_id, + store_id: storeId, }, - attributes: ["username", "account_name"], + attributes: ["user_id", "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" }); } } + +export async function UpdateEmployee(req: Request, res: Response) { + try { + let { userId, username, accountName } = req.body; + + // validate request + // check if username or account name is provided + + if (!userId || (!username && !accountName)) { + return res.status(400).send({ err: "invalid request" }); + } + + // validate username and account name + + if (!isUserIdValid(userId)) { + return res.status(400).send({ err: "invalid request" }); + } + + let update = {}; + + if (accountName) { + accountName = accountName.toLowerCase(); + + if (!(await isAccountNameValid(accountName))) { + res.status(400).send({ err: "invalid request" }); + return; + } + + update = { + ...update, + account_name: accountName, + }; + } + + if (username) { + if (!isUsernameValid(username)) { + res.status(400).send({ err: "invalid request" }); + return; + } + + update = { + ...update, + username: username, + }; + } + + // 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" }); + } + + // check if user exists + + const existingUser = await User.findOne({ + where: { + user_id: userId, + }, + }); + + if (!existingUser) { + logger.debug("User not found"); + return res.status(400).send({ err: "invalid request" }); + } + + // update user + + await User.update( + { + username: username, + account_name: accountName, + }, + { + where: { + user_id: userId, + }, + } + ) + .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 DeleteEmployee(req: Request, res: Response) { + try { + let { userId } = req.body; + + // validate request + + if (!userId) { + 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" }); + } + + // check if user exists + + const existingUser = await User.findOne({ + where: { + user_id: userId, + }, + }); + + if (!existingUser) { + logger.debug("User not found"); + return res.status(400).send({ err: "invalid request" }); + } + + // delete user + + await User.destroy({ + where: { + user_id: userId, + }, + }) + .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" }); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index acf6ebf..0dc271c 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,9 +1,18 @@ import Session from "./session"; +import Store from "./store"; +import StoreService from "./storeService"; +import StoreServiceActivity from "./storeServiceActivity"; +import StoreServiceActivityUsers from "./storeServiceActivityUsers"; + import User from "./user"; function syncModels() { - User.sync({ alter: true }); - Session.sync({ alter: true }); + User.sync(); + Session.sync(); + Store.sync(); + StoreService.sync(); + StoreServiceActivity.sync(); + StoreServiceActivityUsers.sync(); } export default syncModels; diff --git a/src/models/store.ts b/src/models/store.ts new file mode 100644 index 0000000..28a8bb3 --- /dev/null +++ b/src/models/store.ts @@ -0,0 +1,69 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface StoreAttributes { + store_id: string; + owner_user_id: string; + name: string; + calendar_max_future_booking_days: number; + calendar_min_earliest_booking_time: number; + calendar_primary_calendar_id: string; + calendar_using_primary_calendar: boolean; + calendar_max_service_duration: number; +} + +class Store extends Model implements StoreAttributes { + declare store_id: string; + declare owner_user_id: string; + declare name: string; + declare calendar_max_future_booking_days: number; + declare calendar_min_earliest_booking_time: number; + declare calendar_primary_calendar_id: string; + declare calendar_using_primary_calendar: boolean; + declare calendar_max_service_duration: number; +} + +Store.init( + { + // Model attributes are defined here + store_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + owner_user_id: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + calendar_max_future_booking_days: { + type: DataTypes.INTEGER, + allowNull: false, + }, + calendar_min_earliest_booking_time: { + type: DataTypes.INTEGER, + allowNull: false, + }, + calendar_primary_calendar_id: { + type: DataTypes.STRING, + allowNull: false, + }, + calendar_using_primary_calendar: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + calendar_max_service_duration: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + tableName: "stores", + sequelize, // passing the `sequelize` instance is required + } +); + +export default Store; diff --git a/src/models/storeService.ts b/src/models/storeService.ts new file mode 100644 index 0000000..f009c88 --- /dev/null +++ b/src/models/storeService.ts @@ -0,0 +1,42 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface StoreServiceAttributes { + service_id: string; + store_id: string; + name: string; +} + +class StoreService + extends Model + implements StoreServiceAttributes +{ + declare service_id: string; + declare store_id: string; + declare name: string; +} + +StoreService.init( + { + // Model attributes are defined here + service_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + store_id: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: "store_services", + sequelize, // passing the `sequelize` instance is required + } +); + +export default StoreService; diff --git a/src/models/storeServiceActivity.ts b/src/models/storeServiceActivity.ts new file mode 100644 index 0000000..2a16203 --- /dev/null +++ b/src/models/storeServiceActivity.ts @@ -0,0 +1,60 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface StoreServiceActivityAttributes { + activity_id: string; + service_id: string; + name: string; + description: string; + price: number; + duration: number; +} + +class StoreServiceActivity + extends Model + implements StoreServiceActivityAttributes +{ + declare activity_id: string; + declare service_id: string; + declare name: string; + declare description: string; + declare price: number; + declare duration: number; +} + +StoreServiceActivity.init( + { + // Model attributes are defined here + activity_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + service_id: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + allowNull: false, + }, + price: { + type: DataTypes.FLOAT, + allowNull: false, + }, + duration: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + tableName: "store_service_activities", + sequelize, // passing the `sequelize` instance is required + } +); + +export default StoreServiceActivity; diff --git a/src/models/storeServiceActivityUsers.ts b/src/models/storeServiceActivityUsers.ts new file mode 100644 index 0000000..9f60cc9 --- /dev/null +++ b/src/models/storeServiceActivityUsers.ts @@ -0,0 +1,37 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface StoreServiceActivityUsersAttributes { + activity_id: string; + user_id: string; +} + +class StoreServiceActivityUsers + extends Model + implements StoreServiceActivityUsersAttributes +{ + declare activity_id: string; + declare user_id: string; +} + +StoreServiceActivityUsers.init( + { + // Model attributes are defined here + activity_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + user_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: "store_service_activity_users", + sequelize, // passing the `sequelize` instance is required + } +); + +export default StoreServiceActivityUsers; diff --git a/src/models/user.ts b/src/models/user.ts index 571e9d6..22e4bc6 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -3,22 +3,29 @@ import sequelize from "../database/database"; interface UserAttributes { user_id: string; - master_user_id?: string; + store_id: string; + // TODO: change to role_id role: string; account_name: string; username: string; password: string; - calendar_settings?: string; + calendar_max_future_booking_days?: number; + calendar_min_earliest_booking_time?: number; + calendar_primary_calendar_id?: string; + calendar_using_primary_calendar?: boolean; } class User extends Model implements UserAttributes { declare user_id: string; - declare master_user_id: string; + declare store_id: string; declare role: string; declare account_name: string; declare username: string; declare password: string; - declare calendar_settings: string; + declare calendar_max_future_booking_days: number; + declare calendar_min_earliest_booking_time: number; + declare calendar_primary_calendar_id: string; + declare calendar_using_primary_calendar: boolean; } User.init( @@ -29,13 +36,13 @@ User.init( type: DataTypes.STRING, allowNull: false, }, - master_user_id: { + store_id: { type: DataTypes.STRING, - // allowNull defaults to true + allowNull: false, }, role: { type: DataTypes.STRING, - // allowNull defaults to true + allowNull: false, }, account_name: { type: DataTypes.STRING, @@ -49,9 +56,17 @@ User.init( type: DataTypes.STRING, allowNull: false, }, - calendar_settings: { + calendar_max_future_booking_days: { + type: DataTypes.INTEGER, + }, + calendar_min_earliest_booking_time: { + type: DataTypes.INTEGER, + }, + calendar_primary_calendar_id: { type: DataTypes.STRING, - // allowNull defaults to true + }, + calendar_using_primary_calendar: { + type: DataTypes.BOOLEAN, }, }, { diff --git a/src/routes/storeRoutes.ts b/src/routes/storeRoutes.ts new file mode 100644 index 0000000..7dc73b5 --- /dev/null +++ b/src/routes/storeRoutes.ts @@ -0,0 +1,8 @@ +import express from "express"; +const router = express.Router(); + +import * as storeController from "../controllers/storeController"; + +router.get("/:storeId", storeController.GetStore); + +export default router; diff --git a/src/routes/storeServicesRoutes.ts b/src/routes/storeServicesRoutes.ts new file mode 100644 index 0000000..ea181f5 --- /dev/null +++ b/src/routes/storeServicesRoutes.ts @@ -0,0 +1,19 @@ +import express from "express"; +const router = express.Router(); + +import * as storeServicesController from "../controllers/storeServicesController"; + +router.get("/:storeId", storeServicesController.GetStoreServices); +router.post("/", storeServicesController.CreateStoreService); +router.post("/update", storeServicesController.UpdateStoreService); +router.post("/activity", storeServicesController.CreateStoreServiceActivity); +router.get( + "/activities/:storeId/:serviceId", + storeServicesController.GetStoreServiceActivities +); +router.post( + "/activity/update", + storeServicesController.UpdateStoreServiceActivity +); + +export default router; diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 393aa40..f2125be 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -8,5 +8,6 @@ router.post("/auth/signup", userController.SignUp); router.post("/auth/login", userController.Login); router.delete("/auth/logout", sessionProtection, userController.Logout); router.get("/", sessionProtection, userController.GetUser); +router.post("/auth/check/accountname", userController.IsAccountNameAvailable); export default router; diff --git a/src/routes/usersRoutes.ts b/src/routes/usersRoutes.ts index 805d27b..91a2970 100644 --- a/src/routes/usersRoutes.ts +++ b/src/routes/usersRoutes.ts @@ -4,6 +4,8 @@ const router = express.Router(); import * as usersController from "../controllers/usersController"; router.post("/", usersController.AddEmployee); -router.get("/", usersController.GetEmployees); +router.get("/:storeId", usersController.GetEmployees); +router.post("/update", usersController.UpdateEmployee); +router.delete("/", usersController.DeleteEmployee); export default router; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8947f7e..caa3b28 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,5 @@ export const DEFAULT_SESSION_EXPIRY = 365 * 24 * 60 * 60 * 1000; // 365 days +export const USER_SESSION_LENGTH = 32; export const USERNAME_MIN_LENGTH = 3; export const USERNAME_MAX_LENGTH = 20; @@ -9,9 +10,36 @@ export const ACCOUNT_NAME_MAX_LENGTH = 20; export const PASSWORD_MIN_LENGTH = 8; export const PASSWORD_MAX_LENGTH = 64; +export const USER_ID_LENGTH = 36; + // Header name for the session ID export const HEADER_X_AUTHORIZATION: string = "x-authorization"; +export const STORE_SERVICE_MIN_LENGTH = 3; +export const STORE_SERVICE_MAX_LENGTH = 32; + +export const STORE_SERVICE_ACTIVITY_NAME_MIN_LENGTH = 3; +export const STORE_SERVICE_ACTIVITY_NAME_MAX_LENGTH = 32; + +export const STORE_SERVICE_ACTIVITY_DESCRIPTION_MIN_LENGTH = 3; +export const STORE_SERVICE_ACTIVITY_DESCRIPTION_MAX_LENGTH = 1024; + +export const STORE_SERVICE_ACTIVITY_PRICE_MIN = 0; +export const STORE_SERVICE_ACTIVITY_PRICE_MAX = 10000000; + +export const STORE_SERVICE_ACTIVITY_DURATION_HOURS_MAX = 23; +export const STORE_SERVICE_ACTIVITY_DURATION_MINUTES_MAX = 60; +export const STORE_SERVICE_ACTIVITY_DURATION_MIN = 0; +export const STORE_SERVICE_ACTIVITY_DURATION_MAX = + STORE_SERVICE_ACTIVITY_DURATION_HOURS_MAX * 60 + + STORE_SERVICE_ACTIVITY_DURATION_MINUTES_MAX; + +export const CALENDAR_MAX_FUTURE_BOOKING_DAYS = 14; +export const CALENDAR_MIN_EARLIEST_BOOKING_TIME = 1; +export const CALENDAR_PRIMARY_CALENDAR_ID = ""; +export const CALENDAR_USING_PRIMARY_CALENDAR = false; +export const CALENDAR_MAX_SERVICE_DURATION = 1440; // 24 hours in minutes + 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 2603e26..2da6cd1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -3,7 +3,11 @@ import bcrypt from "bcrypt"; import { v4 as uuidv4 } from "uuid"; import { Request, Response } from "express"; import Session from "../models/session"; -import { DEFAULT_SESSION_EXPIRY, HEADER_X_AUTHORIZATION } from "./constants"; +import { + DEFAULT_SESSION_EXPIRY, + HEADER_X_AUTHORIZATION, + USER_SESSION_LENGTH, +} from "./constants"; export async function matchPassword(decodedPassword: string, password: string) { return await bcrypt.compare(decodedPassword, password); @@ -21,8 +25,20 @@ export function newUserId() { return uuidv4(); } +export function newStoreId() { + return uuidv4(); +} + +export function newStoreServiceId() { + return uuidv4(); +} + +export function newStoreServiceActivityId() { + return uuidv4(); +} + export function newUserSession() { - return crypto.randomBytes(32).toString("hex"); + return crypto.randomBytes(USER_SESSION_LENGTH).toString("hex"); } export async function saveSession( diff --git a/src/validator/validator.ts b/src/validator/validator.ts index 04f42cb..f5833c4 100644 --- a/src/validator/validator.ts +++ b/src/validator/validator.ts @@ -1,3 +1,4 @@ +import logger from "../logger/logger"; import { USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, @@ -5,7 +6,19 @@ import { PASSWORD_MAX_LENGTH, ACCOUNT_NAME_MIN_LENGTH, ACCOUNT_NAME_MAX_LENGTH, + USER_ID_LENGTH, + STORE_SERVICE_MIN_LENGTH, + STORE_SERVICE_MAX_LENGTH, + STORE_SERVICE_ACTIVITY_NAME_MAX_LENGTH, + STORE_SERVICE_ACTIVITY_NAME_MIN_LENGTH, + STORE_SERVICE_ACTIVITY_DESCRIPTION_MAX_LENGTH, + STORE_SERVICE_ACTIVITY_DESCRIPTION_MIN_LENGTH, + STORE_SERVICE_ACTIVITY_PRICE_MAX, + STORE_SERVICE_ACTIVITY_PRICE_MIN, + STORE_SERVICE_ACTIVITY_DURATION_MAX, + STORE_SERVICE_ACTIVITY_DURATION_MIN, } from "../utils/constants"; +import User from "../models/user"; // TODO: regex for username export function isUsernameValid(username: string) { @@ -16,11 +29,28 @@ 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 - ); +export async function isAccountNameValid(accountName: string) { + if ( + accountName.length < ACCOUNT_NAME_MIN_LENGTH || + accountName.length > ACCOUNT_NAME_MAX_LENGTH + ) { + return false; + } + + // check if account name is already taken in the database + + const existingUser = await User.findOne({ + where: { + account_name: accountName, + }, + }); + + if (existingUser) { + logger.debug("User already exists with this accountName: %s", accountName); + return false; + } + + return true; } // TODO: regex for password @@ -30,3 +60,52 @@ export function isPasswordValid(password: string) { password.length <= PASSWORD_MAX_LENGTH ); } + +export function isUserIdValid(userId: string) { + return userId.length === USER_ID_LENGTH; +} + +export function isStoreServiceNameValid(storeServiceName: string) { + return ( + storeServiceName.length >= STORE_SERVICE_MIN_LENGTH && + storeServiceName.length <= STORE_SERVICE_MAX_LENGTH + ); +} + +export function isStoreServiceActivityNameValid( + storeServiceActivityName: string +) { + return ( + storeServiceActivityName.length >= STORE_SERVICE_ACTIVITY_NAME_MIN_LENGTH && + storeServiceActivityName.length <= STORE_SERVICE_ACTIVITY_NAME_MAX_LENGTH + ); +} + +export function isStoreServiceActivityDescriptionValid( + storeServiceActivityDescription: string +) { + return ( + storeServiceActivityDescription.length >= + STORE_SERVICE_ACTIVITY_DESCRIPTION_MIN_LENGTH && + storeServiceActivityDescription.length <= + STORE_SERVICE_ACTIVITY_DESCRIPTION_MAX_LENGTH + ); +} + +export function isStoreServiceActivityPriceValid( + storeServiceActivityPrice: any +) { + return ( + storeServiceActivityPrice >= STORE_SERVICE_ACTIVITY_PRICE_MIN && + storeServiceActivityPrice <= STORE_SERVICE_ACTIVITY_PRICE_MAX + ); +} + +export function isStoreServiceActivityDurationValid( + storeServiceActivityDuration: any +) { + return ( + storeServiceActivityDuration >= STORE_SERVICE_ACTIVITY_DURATION_MIN && + storeServiceActivityDuration <= STORE_SERVICE_ACTIVITY_DURATION_MAX + ); +}