diff --git a/public/scripts/employeeDetail.js b/public/scripts/employeeDetail.js new file mode 100644 index 0000000..748c459 --- /dev/null +++ b/public/scripts/employeeDetail.js @@ -0,0 +1,213 @@ +let hideEmployeeSavedAlertTimer = undefined; + +document.addEventListener("DOMContentLoaded", () => { + // TODO: Things that need doing when the view is loaded + getSaveActionElement().addEventListener("click", saveActionClick); +}); + +// Save +function saveActionClick(event) { + // TODO: Actually save the employee via an AJAX call + if (!validateSave()) { + return; + } + + const saveActionElement = event.target; + saveActionElement.disabled = true; + + const employeeId = getEmployeeId(); + const employeeIdIsDefined = ((employeeId != null) && (employeeId.trim() !== "")); + const saveActionUrl = ("/api/employeeDetail/" + + (employeeIdIsDefined ? employeeId : "")); + const saveEmployeeRequest = { + id: employeeId, + firstName: getEmployeeFirstName(), + lastName: getEmployeeLastName(), + password: getEmployeePassword(), + type: getEmployeeType() + }; + + if (employeeIdIsDefined) { + ajaxPatch(saveActionUrl, saveEmployeeRequest, (callbackResponse) => { + saveActionElement.disabled = false; + + if (isSuccessResponse(callbackResponse)) { + displayEmployeeSavedAlertModal(); + } + }); + } else { + ajaxPost(saveActionUrl, saveEmployeeRequest, (callbackResponse) => { + saveActionElement.disabled = false; + + if (isSuccessResponse(callbackResponse)) { + displayEmployeeSavedAlertModal(); + + if ((callbackResponse.data != null) + && (callbackResponse.data.employee != null) + && (callbackResponse.data.employee.id.trim() !== "")) { + + document.getElementById("employeeID").classList.remove("hidden"); + + setEmployeeId(callbackResponse.data.employee.id.trim()); + } + } + }); + } +}; + + + +function validateSave() { + const employeeFirstName = getEmployeeFirstName(); + if ((employeeFirstName == null) || (employeeFirstName.trim() === "")) { + displayError("Please provide your first name."); + employeeFirstName.focus(); + employeeFirstName.select(); + return false; + } + + const employeeLastName = getEmployeeLastName(); + if ((employeeLastName == null) || (employeeLastName.trim() === "")) { + displayError("Please provide your last name."); + employeeLastName.focus(); + employeeLastName.select(); + return false; + } + + const employeePassword = getEmployeePassword(); + if ((employeePassword == null) || (employeePassword.trim() === "") || (employeePassword != getEmployeeConfirmPassword())) { + displayError("The password you entered is not correct."); + employeePassword.focus(); + employeePassword.select(); + return false; + } + + const employeeType = getEmployeeType(); + if ((employeeType == null) || (employeeType.trim() === "")) { + displayError("Please provide a valid employee type."); + employeeType.focus(); + employeeType.select(); + return false; + } + + return true; +} + +function displayEmployeeSavedAlertModal() { + if (hideEmployeeSavedAlertTimer) { + clearTimeout(hideEmployeeSavedAlertTimer); + } + + const savedAlertModalElement = getSavedAlertModalElement(); + savedAlertModalElement.style.display = "none"; + savedAlertModalElement.style.display = "block"; + + hideEmployeeSavedAlertTimer = setTimeout(hideEmployeeSavedAlertModal, 1200); +} + +function hideEmployeeSavedAlertModal() { + if (hideEmployeeSavedAlertTimer) { + clearTimeout(hideEmployeeSavedAlertTimer); + } + + getSavedAlertModalElement().style.display = "none"; +} + +function ajaxPost(resourceRelativeUri, data, callback) { + return ajax(resourceRelativeUri, "POST", data, callback); +} + +function ajaxPatch(resourceRelativeUri, data, callback) { + return ajax(resourceRelativeUri, "PATCH", data, callback); +} + +function ajax(resourceRelativeUri, verb, data, callback) { + const httpRequest = new XMLHttpRequest(); + + if (httpRequest == null) { + return httpRequest; + } + + httpRequest.onreadystatechange = () => { + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if ((httpRequest.status >= 200) && (httpRequest.status < 300)) { + handleSuccessResponse(httpRequest, callback); + } else { + handleFailureResponse(httpRequest, callback); + } + } + }; + + httpRequest.open(verb, resourceRelativeUri, true); + if (data != null) { + httpRequest.setRequestHeader('Content-Type', 'application/json'); + httpRequest.send(JSON.stringify(data)); + } else { + httpRequest.send(); + } + + return httpRequest; +} +// End save + +// Getters and setters + +function getSaveActionElement() { + return document.getElementById("saveButton"); +} + +function getSavedAlertModalElement() { + return document.getElementById("employeeSavedAlertModal"); +} + +function getEmployeeFirstName() { + return getEmployeeFirstNameElement().value; +} + +function getEmployeeFirstNameElement() { + return document.getElementById("employeeFirstName"); +} + +function getEmployeeLastName() { + return getEmployeeLastNameElement().value; +} + +function getEmployeeLastNameElement() { + return document.getElementById("employeeLastName"); +} + +function getEmployeePassword() { + return getEmployeePasswordElement().value; +} + +function getEmployeePasswordElement() { + return document.getElementById("employeePassword"); +} + +function getEmployeeConfirmPassword() { + return getEmployeeConfirmPasswordElement().value; +} + +function getEmployeeConfirmPasswordElement() { + return document.getElementById("employeeConfirmPassword"); +} + +function getEmployeeType() { + return getEmployeeTypeElement().value; +} + +function getEmployeeTypeElement() { + return document.getElementById("employeeType"); +} + +function getEmployeeId() { + return getEmployeeIdElement().value; +} +function setEmployeeId(employeeId) { + getEmployeeIdElement().value = employeeId; +} +function getEmployeeIdElement() { + return document.getElementById("employeeId"); +} + +// End getters and setters diff --git a/public/scripts/master.js b/public/scripts/master.js index 4c71626..fbf883f 100644 --- a/public/scripts/master.js +++ b/public/scripts/master.js @@ -145,7 +145,7 @@ function clearError() { function displayError(errorMessage) { if ((errorMessage == null) || (errorMessage === "")) { - return; + return false; //Just to prevent invalid URL redirects, returns false } const errorMessageDisplayElement = getErrorMessageDisplayElement(); @@ -154,7 +154,7 @@ function displayError(errorMessage) { if ((errorMessageContainerElement == null) || (errorMessageDisplayElement == null)) { - return; + return false; //Just to prevent invalid URL redirects, returns false } errorMessageDisplayElement.innerHTML = errorMessage; @@ -185,9 +185,9 @@ function signOutActionClickHandler() { && (callbackResponse.data.redirectUrl != null) && (callbackResponse.data.redirectUrl !== "")) { - window.location.replace(callbackResponse.data.redirectUrl); + window.location.replace(callbackResponse.data.redirectUrl); //This sends it back to redirect URL } else { - window.location.replace("/"); + window.location.replace("/"); //This sends it back to app main view if there isn't a redirect URL } }); } diff --git a/public/scripts/signIn.js b/public/scripts/signIn.js index 9d69ba7..f22d439 100644 --- a/public/scripts/signIn.js +++ b/public/scripts/signIn.js @@ -1,8 +1,21 @@ document.addEventListener("DOMContentLoaded", () => { - // TODO: Anything you want to do when the page is loaded? }); function validateForm() { - // TODO: Validate the user input + var employeeID = document.getElementById("employeeID").value; + var pass = document.getElementById("password").value; + + if((isNaN(employeeID)) || (employeeID == "")) + { + alert("Enter a valid ID number") + return false; + } + + if(pass == "") + { + alert("Enter a password") + return false; + } + return true; -} +}; diff --git a/src/app.ts b/src/app.ts index cf7f595..a5704c8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,6 +7,7 @@ import * as fileSystem from "fs"; import bodyParser from "body-parser"; import compression from "compression"; import session from "express-session"; +import { Resources } from "./resourceLookup"; // Load environment variables from .env file, where API keys and passwords are configured dotenv.config({ path: ".env" }); @@ -46,4 +47,6 @@ fileSystem.readdirSync(__dirname + "/routes").forEach(function (routeConfig: str } }); +Resources.loadStrings(); + export default app; \ No newline at end of file diff --git a/src/controllers/commands/employees/activeEmployeeExistsQuery.ts b/src/controllers/commands/employees/activeEmployeeExistsQuery.ts new file mode 100644 index 0000000..8bca9e8 --- /dev/null +++ b/src/controllers/commands/employees/activeEmployeeExistsQuery.ts @@ -0,0 +1,21 @@ +import { EmployeeModel } from "../models/employeeModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import * as EmployeeRepository from "../models/employeeModel"; +import { CommandResponse, Employee } from "../../typeDefinitions"; +import * as EmployeeHelper from "./helpers/employeeHelper"; + +export const execute = async (): Promise> => { + return EmployeeRepository.queryActiveExists() + .then((queriedActiveUser: (EmployeeModel | null)): Promise> => { + if (queriedActiveUser) { + return Promise.resolve(>{ + status: 200, + data: EmployeeHelper.mapEmployeeData(queriedActiveUser)}); + } + + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.EMPLOYEES_UNABLE_TO_QUERY) + }); + }); + }; diff --git a/src/controllers/commands/employees/clearActiveUserCommand.ts b/src/controllers/commands/employees/clearActiveUserCommand.ts new file mode 100644 index 0000000..79e509e --- /dev/null +++ b/src/controllers/commands/employees/clearActiveUserCommand.ts @@ -0,0 +1,20 @@ +import { ActiveUserModel } from "../models/activeUserModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import * as ActiveUserRepository from "../models/activeUserModel"; +import { CommandResponse, ActiveUser } from "../../typeDefinitions"; +import * as DatabaseConnection from "../models/databaseConnection"; + +export const execute = async (sessionKey: string): Promise> => { + return ActiveUserRepository.queryBySessionKey(sessionKey, await DatabaseConnection.createTransaction()) + .then((activeUser: (ActiveUserModel | null)): Promise> => { + if (activeUser) { + return Promise.resolve(>{ + status: 200 + }); + } + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.USER_SESSION_NOT_FOUND) + }); + }); +}; diff --git a/src/controllers/commands/employees/employeeCreateCommand.ts b/src/controllers/commands/employees/employeeCreateCommand.ts new file mode 100644 index 0000000..6ed3c22 --- /dev/null +++ b/src/controllers/commands/employees/employeeCreateCommand.ts @@ -0,0 +1,97 @@ +import Sequelize from "sequelize"; +import { EmployeeModel } from "../models/employeeModel"; +import * as EmployeeRepository from "../models/employeeModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import * as DatabaseConnection from "../models/databaseConnection"; +import { CommandResponse, Employee, EmployeeSaveRequest } from "../../typeDefinitions"; +import * as EmployeeHelper from "./helpers/employeeHelper"; +import { EmployeeClassification } from "../models/constants/entityTypes"; + +const validateSaveRequest = ( + saveEmployeeRequest: EmployeeSaveRequest +): CommandResponse => { + + let errorMessage: string = ""; + + if (!saveEmployeeRequest.firstName) { + errorMessage = Resources.getString(ResourceKey.EMPLOYEE_FIRST_NAME_INVALID); + } else if (!saveEmployeeRequest.lastName) { + errorMessage = Resources.getString(ResourceKey.EMPLOYEE_LAST_NAME_INVALID); + } else if (!saveEmployeeRequest.password) { + errorMessage = Resources.getString(ResourceKey.EMPLOYEE_PASSWORD_INVALID); + } + + return ((errorMessage === "") + ? >{ status: 200 } + : >{ + status: 422, + message: errorMessage + }); +}; + +export const execute = async ( + saveEmployeeRequest: any +): Promise> => { + + const validationResponse: CommandResponse = + validateSaveRequest(saveEmployeeRequest); + if (validationResponse.status !== 200) { + return Promise.reject(validationResponse); + } + + const classes: any = { + "General Manager": EmployeeClassification.GeneralManager, + "Shift Manager": EmployeeClassification.ShiftManager, + "Cashier": EmployeeClassification.Cashier + }; + + const employeeToCreate: EmployeeModel = { + id: saveEmployeeRequest.id, + active: saveEmployeeRequest.active, + lastName: saveEmployeeRequest.lastName, + firstName: saveEmployeeRequest.firstName, + managerId: saveEmployeeRequest.managerId, + classification: Number(classes[saveEmployeeRequest.type] || EmployeeClassification.NotDefined), + password: Buffer.from(EmployeeHelper.hashString(saveEmployeeRequest.password), "utf8") + }; + + let createTransaction: Sequelize.Transaction; + + return DatabaseConnection.createTransaction() + .then((createdTransaction: Sequelize.Transaction): Promise => { + createTransaction = createdTransaction; + + return EmployeeModel.create( + employeeToCreate, + { + transaction: createTransaction + } + ); + }).then((createdEmployee: EmployeeModel): CommandResponse => { + createTransaction.commit(); + + return >{ + status: 201, + data: { + id: createdEmployee.id, + active: createdEmployee.active, + lastName: createdEmployee.lastName, + createdOn: createdEmployee.createdOn, + firstName: createdEmployee.firstName, + managerId: createdEmployee.managerId, + employeeId: createdEmployee.employeeId.toString(), + classification: createdEmployee.classification + } + }; + }).catch((error: any): Promise> => { + if (createTransaction != null) { + createTransaction.rollback(); + } + + return Promise.reject(>{ + status: (error.status || 500), + message: (error.message + || Resources.getString(ResourceKey.EMPLOYEE_UNABLE_TO_SAVE)) + }); + }); +}; diff --git a/src/controllers/commands/employees/employeeExistsQuery.ts b/src/controllers/commands/employees/employeeExistsQuery.ts new file mode 100644 index 0000000..3641921 --- /dev/null +++ b/src/controllers/commands/employees/employeeExistsQuery.ts @@ -0,0 +1,21 @@ +import { EmployeeModel } from "../models/employeeModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import * as EmployeeRepository from "../models/employeeModel"; +import { CommandResponse, Employee } from "../../typeDefinitions"; +import * as EmployeeHelper from "./helpers/employeeHelper"; + +export const execute = async (): Promise> => { + return EmployeeRepository.queryExists() + .then((queriedActiveUser: (EmployeeModel | null)): Promise> => { + if (queriedActiveUser) { + return Promise.resolve(>{ + status: 200, + data: EmployeeHelper.mapEmployeeData(queriedActiveUser)}); + } + + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.EMPLOYEES_UNABLE_TO_QUERY) + }); + }); + }; diff --git a/src/controllers/commands/employees/employeeQuery.ts b/src/controllers/commands/employees/employeeQuery.ts new file mode 100644 index 0000000..ac06175 --- /dev/null +++ b/src/controllers/commands/employees/employeeQuery.ts @@ -0,0 +1,30 @@ +import * as Helper from "../helpers/helper"; +import { EmployeeModel } from "../models/employeeModel"; +import * as EmployeeHelper from "./helpers/employeeHelper"; +import * as EmployeeRepository from "../models/employeeModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import { CommandResponse, Employee } from "../../typeDefinitions"; + +export const queryById = async (employeeId?: string): Promise> => { + if (Helper.isBlankString(employeeId)) { + return >{ + status: 422, + message: Resources.getString(ResourceKey.EMPLOYEE_RECORD_ID_INVALID) + }; + } + + return EmployeeRepository.queryById(employeeId) + .then((queriedEmployee: (EmployeeModel | null)): Promise> => { + if (queriedEmployee == null) { + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.EMPLOYEE_NOT_FOUND) + }); + } + + return Promise.resolve(>{ + status: 200, + data: EmployeeHelper.mapEmployeeData(queriedEmployee) + }); + }); +}; diff --git a/src/controllers/commands/employees/employeeSignInCommand.ts b/src/controllers/commands/employees/employeeSignInCommand.ts new file mode 100644 index 0000000..50ef912 --- /dev/null +++ b/src/controllers/commands/employees/employeeSignInCommand.ts @@ -0,0 +1,78 @@ +import { EmployeeModel } from "../models/employeeModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import * as EmployeeRepository from "../models/employeeModel"; +import { CommandResponse, UserSignInRequest, ActiveUser } from "../../typeDefinitions"; +import { ActiveUserModel } from "../models/activeUserModel"; +import * as ActiveUserRepository from "../models/activeUserModel"; +import * as DatabaseConnection from "../models/databaseConnection"; +import * as Helper from "../helpers/helper"; +import * as EmployeeHelper from "./helpers/employeeHelper"; +import Sequelize from "sequelize"; + +export const execute = async (userSignInRequest: UserSignInRequest, session: Express.Session): Promise> => { + if (Helper.isBlankString(userSignInRequest.employeeId)) { + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.EMPLOYEE_RECORD_ID_INVALID) + }); + } + else if (Helper.isBlankString(userSignInRequest.password)) { + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.EMPLOYEE_PASSWORD_INVALID) + }); + } + + const employeeModel: (EmployeeModel | null) = await EmployeeRepository.queryByEmployeeId(parseInt(userSignInRequest.employeeId)); + + if (employeeModel && (parseInt(userSignInRequest.employeeId) == employeeModel.employeeId) && (EmployeeHelper.hashString(userSignInRequest.password) == employeeModel.password.toString("utf8"))) { + const activeUserModel: any = await ActiveUserRepository.queryByEmployeeId(employeeModel.id).catch(console.error); + + const createdTransaction: Sequelize.Transaction = await DatabaseConnection.createTransaction(); + let activeUserToCreate: any = activeUserModel; + if (activeUserModel) { + activeUserModel.sessionKey = session.id; + activeUserToCreate = activeUserModel; + await ActiveUserModel.update( + activeUserModel, + { + transaction: createdTransaction + } + ); + } + else { + activeUserToCreate = { + name: employeeModel.firstName, + employeeId: employeeModel.id, + sessionKey: session.id, + classification: employeeModel.classification, + id: employeeModel.id, + createdOn: employeeModel.createdOn + }; + + await ActiveUserModel.create( + activeUserToCreate, + { + transaction: createdTransaction + } + ); + } + createdTransaction.commit(); + + return >{ + status: 201, + data: { + id: activeUserToCreate.id, + name: activeUserToCreate.name, + employeeId: activeUserToCreate.employeeId, + classification: activeUserToCreate.classification + } + }; + } + return Promise.reject(>{ + status: 404, + message: ( + Resources.getString(ResourceKey.USER_SIGN_IN_CREDENTIALS_INVALID) + ) + }); +}; \ No newline at end of file diff --git a/src/controllers/commands/employees/employeeUpdateCommand.ts b/src/controllers/commands/employees/employeeUpdateCommand.ts new file mode 100644 index 0000000..0d70197 --- /dev/null +++ b/src/controllers/commands/employees/employeeUpdateCommand.ts @@ -0,0 +1,88 @@ +import Sequelize from "sequelize"; +import { EmployeeModel } from "../models/employeeModel"; +import * as EmployeeHelper from "./helpers/employeeHelper"; +import * as EmployeeRepository from "../models/employeeModel"; +import { Resources, ResourceKey } from "../../../resourceLookup"; +import * as DatabaseConnection from "../models/databaseConnection"; +import { CommandResponse, Employee, EmployeeSaveRequest } from "../../typeDefinitions"; + +const validateSaveRequest = ( + saveEmployeeRequest: EmployeeSaveRequest +): CommandResponse => { + + let errorMessage: string = ""; + + if (!saveEmployeeRequest.firstName) { + errorMessage = Resources.getString(ResourceKey.EMPLOYEE_FIRST_NAME_INVALID); + } else if (!saveEmployeeRequest.lastName) { + errorMessage = Resources.getString(ResourceKey.EMPLOYEE_LAST_NAME_INVALID); + } else if (!saveEmployeeRequest.password) { + errorMessage = Resources.getString(ResourceKey.EMPLOYEE_PASSWORD_INVALID); + } + + return ((errorMessage === "") + ? >{ status: 200 } + : >{ + status: 422, + message: errorMessage + }); +}; + +export const execute = async ( + saveEmployeeRequest: EmployeeSaveRequest +): Promise> => { + + const validationResponse: CommandResponse = + validateSaveRequest(saveEmployeeRequest); + if (validationResponse.status !== 200) { + return Promise.reject(validationResponse); + } + + let updateTransaction: Sequelize.Transaction; + + return DatabaseConnection.createTransaction() + .then((createdTransaction: Sequelize.Transaction): Promise => { + updateTransaction = createdTransaction; + + return EmployeeRepository.queryById( + saveEmployeeRequest.id, + updateTransaction); + }).then((queriedEmployee: (EmployeeModel | null)): Promise => { + if (queriedEmployee == null) { + return Promise.reject(>{ + status: 404, + message: Resources.getString(ResourceKey.EMPLOYEE_NOT_FOUND) + }); + } + + return queriedEmployee.update( + { + id: saveEmployeeRequest.id, + active: saveEmployeeRequest.active, + lastName: saveEmployeeRequest.lastName, + firstName: saveEmployeeRequest.firstName, + managerId: saveEmployeeRequest.managerId, + classification: saveEmployeeRequest.classification + }, + { + transaction: updateTransaction + }); + }).then((updatedEmployee: EmployeeModel): CommandResponse => { + updateTransaction.commit(); + + return >{ + status: 200, + data: EmployeeHelper.mapEmployeeData(updatedEmployee) + }; + }).catch((error: any): Promise> => { + if (updateTransaction != null) { + updateTransaction.rollback(); + } + + return Promise.reject(>{ + status: (error.status || 500), + message: (error.messsage + || Resources.getString(ResourceKey.EMPLOYEE_UNABLE_TO_SAVE)) + }); + }); +}; diff --git a/src/controllers/commands/employees/helpers/employeeHelper.ts b/src/controllers/commands/employees/helpers/employeeHelper.ts new file mode 100644 index 0000000..f150db8 --- /dev/null +++ b/src/controllers/commands/employees/helpers/employeeHelper.ts @@ -0,0 +1,28 @@ +import { EmployeeClassification } from "../../models/constants/entityTypes"; +import { Employee } from "../../../typeDefinitions"; +import { EmployeeModel } from "../../models/employeeModel"; +import crypto from "crypto"; + +export const hashString = (toHash: string): string => { + return crypto.createHash("md5").update(toHash).digest("hex"); +}; + +export const isElevatedUser = (employeeClassification: EmployeeClassification): boolean => { + if ([EmployeeClassification.GeneralManager, EmployeeClassification.ShiftManager].indexOf(employeeClassification) <= -1) { + return false; + } + return true; +}; + +export const mapEmployeeData = (queriedEmployee: EmployeeModel): Employee => { + return { + id: queriedEmployee.id, + active: queriedEmployee.active, + lastName: queriedEmployee.lastName, + createdOn: queriedEmployee.createdOn, + firstName: queriedEmployee.firstName, + managerId: queriedEmployee.managerId, + employeeId: queriedEmployee.employeeId.toString(), + classification: queriedEmployee.classification + }; +}; diff --git a/src/controllers/commands/models/activeUserModel.ts b/src/controllers/commands/models/activeUserModel.ts index 091b41c..1f685d1 100644 --- a/src/controllers/commands/models/activeUserModel.ts +++ b/src/controllers/commands/models/activeUserModel.ts @@ -78,12 +78,12 @@ export const queryBySessionKey = async ( }; export const queryByEmployeeId = async ( - employeeId: string, + id: any, queryTransaction?: Sequelize.Transaction ): Promise => { return ActiveUserModel.findOne({ transaction: queryTransaction, - where: { employeeId: employeeId } + where: { id: id } }); }; diff --git a/src/controllers/commands/models/constants/entityTypes.ts b/src/controllers/commands/models/constants/entityTypes.ts new file mode 100644 index 0000000..f017e20 --- /dev/null +++ b/src/controllers/commands/models/constants/entityTypes.ts @@ -0,0 +1,6 @@ +export enum EmployeeClassification { + NotDefined = -1, + Cashier = 101, + ShiftManager = 501, + GeneralManager = 701 +} diff --git a/src/controllers/commands/models/databaseConnection.ts b/src/controllers/commands/models/databaseConnection.ts index f5be20f..c6a8175 100644 --- a/src/controllers/commands/models/databaseConnection.ts +++ b/src/controllers/commands/models/databaseConnection.ts @@ -7,6 +7,9 @@ export const DatabaseConnection: Sequelize.Sequelize = process.env.DATABASE_URL, { dialect: "postgres", + dialectOptions: { + ssl: true + }, protocol: "postgres", omitNull: true, freezeTableName: true, diff --git a/src/controllers/commands/models/employeeModel.ts b/src/controllers/commands/models/employeeModel.ts index b2c704b..013bad1 100644 --- a/src/controllers/commands/models/employeeModel.ts +++ b/src/controllers/commands/models/employeeModel.ts @@ -107,3 +107,7 @@ export const queryActiveExists = async (): Promise => { where: { active: true } }); }; + +export const queryExists = async (): Promise => { + return EmployeeModel.findOne(); +}; diff --git a/src/controllers/employeeDetailRouteController.ts b/src/controllers/employeeDetailRouteController.ts new file mode 100644 index 0000000..d530a27 --- /dev/null +++ b/src/controllers/employeeDetailRouteController.ts @@ -0,0 +1,131 @@ +import { Request, Response } from "express"; +import * as Helper from "./helpers/routeControllerHelper"; +import { Resources, ResourceKey } from "../resourceLookup"; +import * as EmployeeHelper from "./commands/employees/helpers/employeeHelper"; +import * as EmployeeQuery from "./commands/employees/employeeQuery"; +import * as EmployeeCreateCommand from "./commands/employees/employeeCreateCommand"; +import * as EmployeeUpdateCommand from "./commands/employees/employeeUpdateCommand"; +import { ViewNameLookup, ParameterLookup, RouteLookup, QueryParameterLookup } from "./lookups/routingLookup"; +import * as ValidateActiveUser from "./commands/activeUsers/validateActiveUserCommand"; +import * as ActiveEmployeeExistsQuery from "./commands/employees/activeEmployeeExistsQuery"; +import * as EmployeeExistsQuery from "./commands/employees/employeeExistsQuery"; +import { ApiResponse, CommandResponse, Employee, EmployeeSaveRequest, ActiveUser, PageResponse } from "./typeDefinitions"; + +interface CanCreateEmployee { + employeeExists: boolean; + isElevatedUser: boolean; +} + +const determineCanCreateEmployee = async (req: Request): Promise => { + return EmployeeExistsQuery.execute() + .then((activeUserCommandResponse: CommandResponse): Promise => { + return ValidateActiveUser.execute((req.session!).id) + .then((activeUser: CommandResponse): Promise => { + if (EmployeeHelper.isElevatedUser(activeUser.data!.classification)) { + return Promise.resolve( { employeeExists: true, isElevatedUser: true }); + } + + return Promise.resolve( { employeeExists: true, isElevatedUser: false }); + }).catch((error: any): Promise => { + return Promise.reject({ employeeExists: true, isElevatedUser: false }); + }); + }).catch((error: any): Promise => { + return Promise.resolve({ employeeExists: false, isElevatedUser: false}); + }); +}; + +export const start = async (req: Request, res: Response): Promise => { + if (Helper.handleInvalidSession(req, res)) { + return; + } + + return determineCanCreateEmployee(req) + .then((canCreateEmployee: CanCreateEmployee): void => { + if (canCreateEmployee.employeeExists + && !canCreateEmployee.isElevatedUser) { + return res.redirect(Helper.buildNoPermissionsRedirectUrl()); + } + + return res.render(ViewNameLookup.EmployeeDetail); + }).catch((canCreateEmployee: CanCreateEmployee): void => { + return res.redirect(RouteLookup.SignIn + "?" + QueryParameterLookup.ErrorCode + "=" + ResourceKey.USER_SESSION_NOT_ACTIVE); + }); + }; + +export const startWithEmployee = async (req: Request, res: Response): Promise => { + if (Helper.handleInvalidSession(req, res)) { + return; + } + + return ValidateActiveUser.execute((req.session).id) + .then((activeUserCommandResponse: CommandResponse): Promise> => { + if (!EmployeeHelper.isElevatedUser((activeUserCommandResponse.data).classification)) { + return Promise.reject(>{ + status: 403, + message: Resources.getString(ResourceKey.USER_NO_PERMISSIONS) + }); + } + + return EmployeeQuery.queryById(activeUserCommandResponse.data!.id); + }).then((employeeCommandResponse: CommandResponse): void => { + return res.render(ViewNameLookup.EmployeeDetail, employeeCommandResponse.data); + }).catch((error: any): void => { + res.send({ + errorMessage: error.message, + redirectUrl: RouteLookup.SignIn + }); + }); +}; + +const saveEmployee = async ( + req: Request, + res: Response, + performSave: ( + employeeSaveRequest: EmployeeSaveRequest, + isInitialEmployee?: boolean + ) => Promise> +): Promise => { + + if (Helper.handleInvalidApiSession(req, res)) { + return; + } + + let employeeExists: boolean; + + return determineCanCreateEmployee(req) + .then((canCreateEmployee: CanCreateEmployee): Promise> => { + if (canCreateEmployee.employeeExists + && !canCreateEmployee.isElevatedUser) { + + return Promise.reject(>{ + status: 403, + message: Resources.getString(ResourceKey.USER_NO_PERMISSIONS) + }); + } + + employeeExists = canCreateEmployee.employeeExists; + + return performSave(req.body, !employeeExists); + }).then((saveEmployeeCommandResponse: CommandResponse): void => { + res.status(saveEmployeeCommandResponse!.status) + .send({ + redirectUrl: RouteLookup.SignIn + "?id=" + saveEmployeeCommandResponse.data!.id + }); + }).catch((error: any): void => { + return Helper.processApiError( + error, + res, + { + defaultErrorMessage: Resources.getString( + ResourceKey.EMPLOYEE_UNABLE_TO_SAVE) + }); + }); + }; + +export const updateEmployee = async (req: Request, res: Response): Promise => { + return saveEmployee(req, res, EmployeeUpdateCommand.execute); +}; + +export const createEmployee = async (req: Request, res: Response): Promise => { + return saveEmployee(req, res, EmployeeCreateCommand.execute); +}; diff --git a/src/controllers/lookups/routingLookup.ts b/src/controllers/lookups/routingLookup.ts index 60143fc..a0811d2 100644 --- a/src/controllers/lookups/routingLookup.ts +++ b/src/controllers/lookups/routingLookup.ts @@ -1,5 +1,6 @@ export enum ParameterLookup { - ProductId = "productId" + ProductId = "productId", + EmployeeId = "employeeId" } export enum QueryParameterLookup { @@ -10,7 +11,8 @@ export enum ViewNameLookup { SignIn = "signIn", MainMenu = "mainMenu", ProductDetail = "productDetail", - ProductListing = "productListing" + ProductListing = "productListing", + EmployeeDetail = "employeeDetail" } export enum RouteLookup { @@ -20,9 +22,12 @@ export enum RouteLookup { MainMenu = "/mainMenu", ProductDetail = "/productDetail", ProductListing = "/productListing", + EmployeeDetail = "/employeeDetail", + Employee = "/employee", // Page routing - parameters ProductIdParameter = "/:productId", + EmployeeIdParameter = "/:employeeId", // End page routing - parameters // End page routing diff --git a/src/controllers/mainMenuRouteController.ts b/src/controllers/mainMenuRouteController.ts index bfe2f93..e6c4a90 100644 --- a/src/controllers/mainMenuRouteController.ts +++ b/src/controllers/mainMenuRouteController.ts @@ -3,6 +3,7 @@ import { Resources } from "../resourceLookup"; import * as Helper from "./helpers/routeControllerHelper"; import { ViewNameLookup, QueryParameterLookup } from "./lookups/routingLookup"; import * as ValidateActiveUser from "./commands/activeUsers/validateActiveUserCommand"; +import * as EmployeeHelper from "./commands/employees/helpers/employeeHelper"; import { PageResponse, CommandResponse, ActiveUser, MainMenuPageResponse } from "./typeDefinitions"; export const start = async (req: Request, res: Response): Promise => { @@ -13,7 +14,7 @@ export const start = async (req: Request, res: Response): Promise => { return ValidateActiveUser.execute((req.session).id) .then((activeUserCommandResponse: CommandResponse): void => { // TODO: Examine the ActiveUser classification if you want this information - const isElevatedUser: boolean = true; + const isElevatedUser: boolean = EmployeeHelper.isElevatedUser((activeUserCommandResponse.data).classification); // This recommends to Firefox that it refresh the page every time // it is accessed diff --git a/src/controllers/productDetailRouteController.ts b/src/controllers/productDetailRouteController.ts index 460aa78..3cb2eba 100644 --- a/src/controllers/productDetailRouteController.ts +++ b/src/controllers/productDetailRouteController.ts @@ -1,11 +1,14 @@ import { Request, Response } from "express"; +import * as Helper from "./helpers/routeControllerHelper"; import { Resources, ResourceKey } from "../resourceLookup"; import * as ProductQuery from "./commands/products/productQuery"; -import { ViewNameLookup, ParameterLookup, RouteLookup } from "./lookups/routingLookup"; import * as ProductCreateCommand from "./commands/products/productCreateCommand"; import * as ProductDeleteCommand from "./commands/products/productDeleteCommand"; import * as ProductUpdateCommand from "./commands/products/productUpdateCommand"; -import { CommandResponse, Product, ProductDetailPageResponse, ApiResponse, ProductSaveResponse, ProductSaveRequest } from "./typeDefinitions"; +import * as EmployeeHelper from "./commands/employees/helpers/employeeHelper"; +import * as ValidateActiveUser from "./commands/activeUsers/validateActiveUserCommand"; +import { ViewNameLookup, ParameterLookup, RouteLookup } from "./lookups/routingLookup"; +import { CommandResponse, Product, ProductDetailPageResponse, ApiResponse, ProductSaveResponse, ProductSaveRequest, ActiveUser } from "./typeDefinitions"; const processStartProductDetailError = (res: Response, error: any): void => { let errorMessage: (string | undefined) = ""; @@ -27,12 +30,20 @@ const processStartProductDetailError = (res: Response, error: any): void => { }; export const start = async (req: Request, res: Response): Promise => { - return ProductQuery.queryById(req.params[ParameterLookup.ProductId]) - .then((productsCommandResponse: CommandResponse): void => { + if (Helper.handleInvalidSession(req, res)) { + return; + } + let isElevatedUser: boolean; + return ValidateActiveUser.execute((req.session).id) + .then((activeUserCommandResponse: CommandResponse): Promise> => { + isElevatedUser = EmployeeHelper.isElevatedUser((activeUserCommandResponse.data).classification); + return ProductQuery.queryById(req.params[ParameterLookup.ProductId]); + }).then((productsCommandResponse: CommandResponse): void => { return res.render( ViewNameLookup.ProductDetail, { - product: productsCommandResponse.data + product: productsCommandResponse.data, + isElevatedUser: isElevatedUser }); }).catch((error: any): void => { return processStartProductDetailError(res, error); @@ -45,30 +56,51 @@ const saveProduct = async ( performSave: (productSaveRequest: ProductSaveRequest) => Promise> ): Promise => { - return performSave(req.body) - .then((createProductCommandResponse: CommandResponse): void => { + if (Helper.handleInvalidApiSession(req, res)) { + return; + } + + return ValidateActiveUser.execute((req.session).id) + .then((activeUserCommandResponse: CommandResponse): Promise> => { + if (false/* TODO: Verify that the user associated with the current session is elevated or not */) { + return Promise.reject(>{ + status: 403, + message: Resources.getString(ResourceKey.USER_NO_PERMISSIONS) + }); + } + + return performSave(req.body); + }).then((createProductCommandResponse: CommandResponse): void => { res.status(createProductCommandResponse.status) .send({ product: createProductCommandResponse.data }); }).catch((error: any): void => { - res.status(error.status || 500) - .send({ - errorMessage: (error.message - || Resources.getString(ResourceKey.PRODUCT_UNABLE_TO_SAVE)) + return Helper.processApiError( + error, + res, + { + redirectBaseLocation: RouteLookup.ProductListing, + defaultErrorMessage: Resources.getString( + ResourceKey.PRODUCT_UNABLE_TO_SAVE) }); }); }; export const updateProduct = async (req: Request, res: Response): Promise => { - saveProduct(req, res, ProductUpdateCommand.execute); + return saveProduct(req, res, ProductUpdateCommand.execute); }; export const createProduct = async (req: Request, res: Response): Promise => { - saveProduct(req, res, ProductCreateCommand.execute); + return saveProduct(req, res, ProductCreateCommand.execute); }; export const deleteProduct = async (req: Request, res: Response): Promise => { + if (Helper.handleInvalidApiSession(req, res)) { + return; + } + + // TODO: Verify that the user associated with the current session is elevated or not return ProductDeleteCommand.execute(req.params[ParameterLookup.ProductId]) .then((deleteProductCommandResponse: CommandResponse): void => { res.status(deleteProductCommandResponse.status) @@ -76,10 +108,13 @@ export const deleteProduct = async (req: Request, res: Response): Promise redirectUrl: RouteLookup.ProductListing }); }).catch((error: any): void => { - res.status(error.status || 500) - .send({ - errorMessage: (error.message - || Resources.getString(ResourceKey.PRODUCT_UNABLE_TO_DELETE)) + return Helper.processApiError( + error, + res, + { + redirectBaseLocation: RouteLookup.ProductListing, + defaultErrorMessage: Resources.getString( + ResourceKey.PRODUCT_UNABLE_TO_DELETE) }); }); }; diff --git a/src/controllers/productListingRouteController.ts b/src/controllers/productListingRouteController.ts index 9a32b3d..ead01f8 100644 --- a/src/controllers/productListingRouteController.ts +++ b/src/controllers/productListingRouteController.ts @@ -2,7 +2,10 @@ import { Request, Response } from "express"; import { ViewNameLookup } from "./lookups/routingLookup"; import { Resources, ResourceKey } from "../resourceLookup"; import * as ProductsQuery from "./commands/products/productsQuery"; -import { CommandResponse, Product, ProductListingPageResponse } from "./typeDefinitions"; +import * as EmployeeHelper from "./commands/employees/helpers/employeeHelper"; +import * as ValidateActiveUser from "./commands/activeUsers/validateActiveUserCommand"; +import * as Helper from "./helpers/routeControllerHelper"; +import { CommandResponse, Product, ProductListingPageResponse, ActiveUser } from "./typeDefinitions"; const processStartProductListingError = (error: any, res: Response): void => { res.setHeader( @@ -21,8 +24,16 @@ const processStartProductListingError = (error: any, res: Response): void => { }; export const start = async (req: Request, res: Response): Promise => { - return ProductsQuery.query() - .then((productsCommandResponse: CommandResponse): void => { + if (Helper.handleInvalidSession(req, res)) { + return; + } + let isElevatedUser: boolean; + return ValidateActiveUser.execute((req.session).id) + .then((activeUserCommandResponse: CommandResponse): Promise> => { + isElevatedUser = EmployeeHelper.isElevatedUser((activeUserCommandResponse.data).classification); + + return ProductsQuery.query(); + }).then((productsCommandResponse: CommandResponse): void => { res.setHeader( "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store"); @@ -30,6 +41,7 @@ export const start = async (req: Request, res: Response): Promise => { return res.render( ViewNameLookup.ProductListing, { + isElevatedUser: isElevatedUser, products: productsCommandResponse.data }); }).catch((error: any): void => { diff --git a/src/controllers/signInRouteController.ts b/src/controllers/signInRouteController.ts index 6363b08..d634eb0 100644 --- a/src/controllers/signInRouteController.ts +++ b/src/controllers/signInRouteController.ts @@ -1,11 +1,55 @@ import { Request, Response } from "express"; +import { Resources, ResourceKey } from "../resourceLookup"; +import { ViewNameLookup, QueryParameterLookup, RouteLookup } from "./lookups/routingLookup"; +import { ApiResponse, PageResponse, UserSignInRequest, Employee, CommandResponse } from "./typeDefinitions"; +import * as EmployeeExists from "./commands/employees/employeeExistsQuery"; +import * as EmployeeSignIn from "./commands/employees/employeeSignInCommand"; +import * as ClearActiveUser from "./commands/employees/clearActiveUserCommand"; + +const processSignInError = (res: Response, error: any): void => { + let errorMessage: (string | undefined) = ""; + if ((error.status != null) && (error.status >= 500)) { + errorMessage = error.message; + } + + return res.status((error.status || 500)) + .render( + ViewNameLookup.MainMenu, + { + errorMessage: (error.message + || + Resources.getString(ResourceKey.EMPLOYEE_UNABLE_TO_QUERY)) + }); +}; + +export const start = async (req: Request, res: Response): Promise => { + return EmployeeExists.execute() + .then((employeeQueryCommandResponse: CommandResponse): void => { + return res.render(ViewNameLookup.SignIn); +}).catch((error: any): void => { + return res.redirect(RouteLookup.EmployeeDetail); + }); +}; export const signIn = async (req: Request, res: Response): Promise => { - // TODO: Use the credentials provided in the request body (req.body) - // and the "id" property of the (Express.Session)req.session variable - // to sign in the user + const signInRequest: UserSignInRequest = { employeeId: req.body["employeeId"], password: req.body["password"] }; + return EmployeeSignIn.execute(signInRequest, req.session!) + .then((): void => { + return res.redirect(ViewNameLookup.MainMenu); +}).catch((error: any): void => { + return processSignInError(res, error); +}); }; export const clearActiveUser = async (req: Request, res: Response): Promise => { - // TODO: Sign out the user associated with req.session.id + return ClearActiveUser.execute((req.session).id) + .then((): void => { + res.send({ + redirectUrl: RouteLookup.SignIn + }); + }).catch((error: any): void => { + res.send({ + errorMessage: error.message + }); + }); }; diff --git a/src/controllers/typeDefinitions.ts b/src/controllers/typeDefinitions.ts index 3c6b8de..75f82f6 100644 --- a/src/controllers/typeDefinitions.ts +++ b/src/controllers/typeDefinitions.ts @@ -4,6 +4,23 @@ export interface ProductSaveRequest { count: number; lookupCode: string; } + +export interface EmployeeSaveRequest { + id?: string; + active: boolean; + lastName: string; + password: string; + firstName: string; + managerId?: string; + classification: number; + isInitialEmployee?: boolean; +} + +export interface UserSignInRequest { + employeeId: string; + password: string; +} + // End request object definitions // Response object definitions diff --git a/src/routes/employeeDetailRoutes.ts b/src/routes/employeeDetailRoutes.ts new file mode 100644 index 0000000..aad2b19 --- /dev/null +++ b/src/routes/employeeDetailRoutes.ts @@ -0,0 +1,20 @@ +import express from "express"; +import { RouteLookup } from "../controllers/lookups/routingLookup"; +import * as EmployeeDetailRouteController from "../controllers/employeeDetailRouteController"; + +function employeeDetailRoutes(server: express.Express) { + server.get(RouteLookup.EmployeeDetail, EmployeeDetailRouteController.start); + server.get( + (RouteLookup.EmployeeDetail + RouteLookup.EmployeeIdParameter), + EmployeeDetailRouteController.startWithEmployee); + + server.post( + (RouteLookup.API + RouteLookup.EmployeeDetail), + EmployeeDetailRouteController.createEmployee); + + server.patch( + (RouteLookup.API + RouteLookup.Employee + RouteLookup.EmployeeIdParameter), + EmployeeDetailRouteController.updateEmployee); +} + +module.exports.routes = employeeDetailRoutes; diff --git a/src/routes/mainMenuRoutes.ts b/src/routes/mainMenuRoutes.ts new file mode 100644 index 0000000..8fc4251 --- /dev/null +++ b/src/routes/mainMenuRoutes.ts @@ -0,0 +1,10 @@ +import express from "express"; +import { RouteLookup } from "../controllers/lookups/routingLookup"; +import * as mainMenuRouteController from "../controllers/mainMenuRouteController"; + +function mainMenuRoutes(server: express.Express) { + // TODO: Route for initial page load + server.get(RouteLookup.MainMenu, mainMenuRouteController.start); +} + +module.exports.routes = mainMenuRoutes; diff --git a/src/routes/signInRoutes.ts b/src/routes/signInRoutes.ts index 2ba6248..83dd7b0 100644 --- a/src/routes/signInRoutes.ts +++ b/src/routes/signInRoutes.ts @@ -3,7 +3,7 @@ import { RouteLookup } from "../controllers/lookups/routingLookup"; import * as SignInRouteController from "../controllers/signInRouteController"; function signInRoutes(server: express.Express) { - // TODO: Route for initial page load + server.get(RouteLookup.SignIn, SignInRouteController.start); server.post(RouteLookup.SignIn, SignInRouteController.signIn); diff --git a/views/employeeDetail.ejs b/views/employeeDetail.ejs new file mode 100644 index 0000000..8c3e975 --- /dev/null +++ b/views/employeeDetail.ejs @@ -0,0 +1,111 @@ + + + + Register - Employee + + + + + + + + + + +
+

Employee Detail

+
+ +
+
class="hidden" <% } %>> +

+ <% if (locals.errorMessage && (locals.errorMessage !== "")) { %> + <%= locals.errorMessage %> + <% } %> +

+
+ +
+ + + + + + + + + + + class="hidden"<% } %>> + + + + + + + + + + + + + + + + + + + + + + + + + + +
Employee ID: + +
First Name: + +
Last Name: + +
Password: + +
Confirm Password: + +
Employee Type: +
+
+ + +
+ +
+ + +
+ + + + + + \ No newline at end of file diff --git a/views/mainMenu.ejs b/views/mainMenu.ejs index 048fad1..3cc80dc 100644 --- a/views/mainMenu.ejs +++ b/views/mainMenu.ejs @@ -2,12 +2,14 @@ Register - Main Menu - + - + + + @@ -15,7 +17,7 @@

Main Menu

- +
class="hidden" <% } %>>

@@ -26,6 +28,43 @@

+ +
+
+
+ + + +
+
+
+ + + +
class="hidden"<% } %>> + Create Employee +
+
+
+
+ +
class="hidden"<% } %>> + Sales Report +
+
+
+
+ +
class="hidden"<% } %>> + Cashier Report +
+
+
+
-
+
class="hidden"<% } %>>
class="hidden"<% } %>> diff --git a/views/productListing.ejs b/views/productListing.ejs index 35c45ca..5a91338 100644 --- a/views/productListing.ejs +++ b/views/productListing.ejs @@ -9,6 +9,7 @@ + @@ -24,10 +25,10 @@ <% } %>
- +
-
- Create New +
class="hidden"<% } %>> + Create New


diff --git a/views/signIn.ejs b/views/signIn.ejs index 556f05c..e040901 100644 --- a/views/signIn.ejs +++ b/views/signIn.ejs @@ -2,7 +2,7 @@ Register - Sign In - + @@ -15,7 +15,7 @@

Sign In

- +
class="hidden" <% } %>>

@@ -26,10 +26,14 @@

- +
+
+
+

+
- + - \ No newline at end of file +