diff --git a/backend/node_modules/.package-lock.json b/backend/node_modules/.package-lock.json index 8e347b4..b1a67e2 100644 --- a/backend/node_modules/.package-lock.json +++ b/backend/node_modules/.package-lock.json @@ -487,6 +487,19 @@ "node": ">=6.6.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1634,6 +1647,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/backend/node_modules/cors/CONTRIBUTING.md b/backend/node_modules/cors/CONTRIBUTING.md new file mode 100644 index 0000000..591b09a --- /dev/null +++ b/backend/node_modules/cors/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# contributing to `cors` + +CORS is a node.js package for providing a [connect](http://www.senchalabs.org/connect/)/[express](http://expressjs.com/) middleware that can be used to enable [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) with various options. Learn more about the project in [the README](README.md). + +## The CORS Spec + +[http://www.w3.org/TR/cors/](http://www.w3.org/TR/cors/) + +## Pull Requests Welcome + +* Include `'use strict';` in every javascript file. +* 2 space indentation. +* Please run the testing steps below before submitting. + +## Testing + +```bash +$ npm install +$ npm test +``` + +## Interactive Testing Harness + +[http://node-cors-client.herokuapp.com](http://node-cors-client.herokuapp.com) + +Related git repositories: + +* [https://github.com/TroyGoode/node-cors-server](https://github.com/TroyGoode/node-cors-server) +* [https://github.com/TroyGoode/node-cors-client](https://github.com/TroyGoode/node-cors-client) + +## License + +[MIT License](http://www.opensource.org/licenses/mit-license.php) diff --git a/backend/node_modules/cors/HISTORY.md b/backend/node_modules/cors/HISTORY.md new file mode 100644 index 0000000..5762bce --- /dev/null +++ b/backend/node_modules/cors/HISTORY.md @@ -0,0 +1,58 @@ +2.8.5 / 2018-11-04 +================== + + * Fix setting `maxAge` option to `0` + +2.8.4 / 2017-07-12 +================== + + * Work-around Safari bug in default pre-flight response + +2.8.3 / 2017-03-29 +================== + + * Fix error when options delegate missing `methods` option + +2.8.2 / 2017-03-28 +================== + + * Fix error when frozen options are passed + * Send "Vary: Origin" when using regular expressions + * Send "Vary: Access-Control-Request-Headers" when dynamic `allowedHeaders` + +2.8.1 / 2016-09-08 +================== + +This release only changed documentation. + +2.8.0 / 2016-08-23 +================== + + * Add `optionsSuccessStatus` option + +2.7.2 / 2016-08-23 +================== + + * Fix error when Node.js running in strict mode + +2.7.1 / 2015-05-28 +================== + + * Move module into expressjs organization + +2.7.0 / 2015-05-28 +================== + + * Allow array of matching condition as `origin` option + * Allow regular expression as `origin` option + +2.6.1 / 2015-05-28 +================== + + * Update `license` in package.json + +2.6.0 / 2015-04-27 +================== + + * Add `preflightContinue` option + * Fix "Vary: Origin" header added for "*" diff --git a/backend/node_modules/cors/LICENSE b/backend/node_modules/cors/LICENSE new file mode 100644 index 0000000..fd10c84 --- /dev/null +++ b/backend/node_modules/cors/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/node_modules/cors/README.md b/backend/node_modules/cors/README.md new file mode 100644 index 0000000..732b847 --- /dev/null +++ b/backend/node_modules/cors/README.md @@ -0,0 +1,243 @@ +# cors + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Build Status][travis-image]][travis-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +CORS is a node.js package for providing a [Connect](http://www.senchalabs.org/connect/)/[Express](http://expressjs.com/) middleware that can be used to enable [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) with various options. + +**[Follow me (@troygoode) on Twitter!](https://twitter.com/intent/user?screen_name=troygoode)** + +* [Installation](#installation) +* [Usage](#usage) + * [Simple Usage](#simple-usage-enable-all-cors-requests) + * [Enable CORS for a Single Route](#enable-cors-for-a-single-route) + * [Configuring CORS](#configuring-cors) + * [Configuring CORS Asynchronously](#configuring-cors-asynchronously) + * [Enabling CORS Pre-Flight](#enabling-cors-pre-flight) +* [Configuration Options](#configuration-options) +* [Demo](#demo) +* [License](#license) +* [Author](#author) + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install cors +``` + +## Usage + +### Simple Usage (Enable *All* CORS Requests) + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +app.use(cors()) + +app.get('/products/:id', function (req, res, next) { + res.json({msg: 'This is CORS-enabled for all origins!'}) +}) + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) +``` + +### Enable CORS for a Single Route + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +app.get('/products/:id', cors(), function (req, res, next) { + res.json({msg: 'This is CORS-enabled for a Single Route'}) +}) + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) +``` + +### Configuring CORS + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +var corsOptions = { + origin: 'http://example.com', + optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204 +} + +app.get('/products/:id', cors(corsOptions), function (req, res, next) { + res.json({msg: 'This is CORS-enabled for only example.com.'}) +}) + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) +``` + +### Configuring CORS w/ Dynamic Origin + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +var whitelist = ['http://example1.com', 'http://example2.com'] +var corsOptions = { + origin: function (origin, callback) { + if (whitelist.indexOf(origin) !== -1) { + callback(null, true) + } else { + callback(new Error('Not allowed by CORS')) + } + } +} + +app.get('/products/:id', cors(corsOptions), function (req, res, next) { + res.json({msg: 'This is CORS-enabled for a whitelisted domain.'}) +}) + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) +``` + +If you do not want to block REST tools or server-to-server requests, +add a `!origin` check in the origin function like so: + +```javascript +var corsOptions = { + origin: function (origin, callback) { + if (whitelist.indexOf(origin) !== -1 || !origin) { + callback(null, true) + } else { + callback(new Error('Not allowed by CORS')) + } + } +} +``` + +### Enabling CORS Pre-Flight + +Certain CORS requests are considered 'complex' and require an initial +`OPTIONS` request (called the "pre-flight request"). An example of a +'complex' CORS request is one that uses an HTTP verb other than +GET/HEAD/POST (such as DELETE) or that uses custom headers. To enable +pre-flighting, you must add a new OPTIONS handler for the route you want +to support: + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +app.options('/products/:id', cors()) // enable pre-flight request for DELETE request +app.del('/products/:id', cors(), function (req, res, next) { + res.json({msg: 'This is CORS-enabled for all origins!'}) +}) + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) +``` + +You can also enable pre-flight across-the-board like so: + +```javascript +app.options('*', cors()) // include before other routes +``` + +### Configuring CORS Asynchronously + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +var whitelist = ['http://example1.com', 'http://example2.com'] +var corsOptionsDelegate = function (req, callback) { + var corsOptions; + if (whitelist.indexOf(req.header('Origin')) !== -1) { + corsOptions = { origin: true } // reflect (enable) the requested origin in the CORS response + } else { + corsOptions = { origin: false } // disable CORS for this request + } + callback(null, corsOptions) // callback expects two parameters: error and options +} + +app.get('/products/:id', cors(corsOptionsDelegate), function (req, res, next) { + res.json({msg: 'This is CORS-enabled for a whitelisted domain.'}) +}) + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) +``` + +## Configuration Options + +* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. Possible values: + - `Boolean` - set `origin` to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), as defined by `req.header('Origin')`, or set it to `false` to disable CORS. + - `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed. + - `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com". + - `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com". + - `Function` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (which expects the signature `err [object], allow [bool]`) as the second. +* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`). +* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header. +* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed. +* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted. +* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted. +* `preflightContinue`: Pass the CORS preflight response to the next handler. +* `optionsSuccessStatus`: Provides a status code to use for successful `OPTIONS` requests, since some legacy browsers (IE11, various SmartTVs) choke on `204`. + +The default configuration is the equivalent of: + +```json +{ + "origin": "*", + "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "preflightContinue": false, + "optionsSuccessStatus": 204 +} +``` + +For details on the effect of each CORS header, read [this](http://www.html5rocks.com/en/tutorials/cors/) article on HTML5 Rocks. + +## Demo + +A demo that illustrates CORS working (and not working) using jQuery is available here: [http://node-cors-client.herokuapp.com/](http://node-cors-client.herokuapp.com/) + +Code for that demo can be found here: + +* Client: [https://github.com/TroyGoode/node-cors-client](https://github.com/TroyGoode/node-cors-client) +* Server: [https://github.com/TroyGoode/node-cors-server](https://github.com/TroyGoode/node-cors-server) + +## License + +[MIT License](http://www.opensource.org/licenses/mit-license.php) + +## Author + +[Troy Goode](https://github.com/TroyGoode) ([troygoode@gmail.com](mailto:troygoode@gmail.com)) + +[coveralls-image]: https://img.shields.io/coveralls/expressjs/cors/master.svg +[coveralls-url]: https://coveralls.io/r/expressjs/cors?branch=master +[downloads-image]: https://img.shields.io/npm/dm/cors.svg +[downloads-url]: https://npmjs.org/package/cors +[npm-image]: https://img.shields.io/npm/v/cors.svg +[npm-url]: https://npmjs.org/package/cors +[travis-image]: https://img.shields.io/travis/expressjs/cors/master.svg +[travis-url]: https://travis-ci.org/expressjs/cors diff --git a/backend/node_modules/cors/lib/index.js b/backend/node_modules/cors/lib/index.js new file mode 100644 index 0000000..5475aec --- /dev/null +++ b/backend/node_modules/cors/lib/index.js @@ -0,0 +1,238 @@ +(function () { + + 'use strict'; + + var assign = require('object-assign'); + var vary = require('vary'); + + var defaults = { + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + preflightContinue: false, + optionsSuccessStatus: 204 + }; + + function isString(s) { + return typeof s === 'string' || s instanceof String; + } + + function isOriginAllowed(origin, allowedOrigin) { + if (Array.isArray(allowedOrigin)) { + for (var i = 0; i < allowedOrigin.length; ++i) { + if (isOriginAllowed(origin, allowedOrigin[i])) { + return true; + } + } + return false; + } else if (isString(allowedOrigin)) { + return origin === allowedOrigin; + } else if (allowedOrigin instanceof RegExp) { + return allowedOrigin.test(origin); + } else { + return !!allowedOrigin; + } + } + + function configureOrigin(options, req) { + var requestOrigin = req.headers.origin, + headers = [], + isAllowed; + + if (!options.origin || options.origin === '*') { + // allow any origin + headers.push([{ + key: 'Access-Control-Allow-Origin', + value: '*' + }]); + } else if (isString(options.origin)) { + // fixed origin + headers.push([{ + key: 'Access-Control-Allow-Origin', + value: options.origin + }]); + headers.push([{ + key: 'Vary', + value: 'Origin' + }]); + } else { + isAllowed = isOriginAllowed(requestOrigin, options.origin); + // reflect origin + headers.push([{ + key: 'Access-Control-Allow-Origin', + value: isAllowed ? requestOrigin : false + }]); + headers.push([{ + key: 'Vary', + value: 'Origin' + }]); + } + + return headers; + } + + function configureMethods(options) { + var methods = options.methods; + if (methods.join) { + methods = options.methods.join(','); // .methods is an array, so turn it into a string + } + return { + key: 'Access-Control-Allow-Methods', + value: methods + }; + } + + function configureCredentials(options) { + if (options.credentials === true) { + return { + key: 'Access-Control-Allow-Credentials', + value: 'true' + }; + } + return null; + } + + function configureAllowedHeaders(options, req) { + var allowedHeaders = options.allowedHeaders || options.headers; + var headers = []; + + if (!allowedHeaders) { + allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers + headers.push([{ + key: 'Vary', + value: 'Access-Control-Request-Headers' + }]); + } else if (allowedHeaders.join) { + allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string + } + if (allowedHeaders && allowedHeaders.length) { + headers.push([{ + key: 'Access-Control-Allow-Headers', + value: allowedHeaders + }]); + } + + return headers; + } + + function configureExposedHeaders(options) { + var headers = options.exposedHeaders; + if (!headers) { + return null; + } else if (headers.join) { + headers = headers.join(','); // .headers is an array, so turn it into a string + } + if (headers && headers.length) { + return { + key: 'Access-Control-Expose-Headers', + value: headers + }; + } + return null; + } + + function configureMaxAge(options) { + var maxAge = (typeof options.maxAge === 'number' || options.maxAge) && options.maxAge.toString() + if (maxAge && maxAge.length) { + return { + key: 'Access-Control-Max-Age', + value: maxAge + }; + } + return null; + } + + function applyHeaders(headers, res) { + for (var i = 0, n = headers.length; i < n; i++) { + var header = headers[i]; + if (header) { + if (Array.isArray(header)) { + applyHeaders(header, res); + } else if (header.key === 'Vary' && header.value) { + vary(res, header.value); + } else if (header.value) { + res.setHeader(header.key, header.value); + } + } + } + } + + function cors(options, req, res, next) { + var headers = [], + method = req.method && req.method.toUpperCase && req.method.toUpperCase(); + + if (method === 'OPTIONS') { + // preflight + headers.push(configureOrigin(options, req)); + headers.push(configureCredentials(options, req)); + headers.push(configureMethods(options, req)); + headers.push(configureAllowedHeaders(options, req)); + headers.push(configureMaxAge(options, req)); + headers.push(configureExposedHeaders(options, req)); + applyHeaders(headers, res); + + if (options.preflightContinue) { + next(); + } else { + // Safari (and potentially other browsers) need content-length 0, + // for 204 or they just hang waiting for a body + res.statusCode = options.optionsSuccessStatus; + res.setHeader('Content-Length', '0'); + res.end(); + } + } else { + // actual response + headers.push(configureOrigin(options, req)); + headers.push(configureCredentials(options, req)); + headers.push(configureExposedHeaders(options, req)); + applyHeaders(headers, res); + next(); + } + } + + function middlewareWrapper(o) { + // if options are static (either via defaults or custom options passed in), wrap in a function + var optionsCallback = null; + if (typeof o === 'function') { + optionsCallback = o; + } else { + optionsCallback = function (req, cb) { + cb(null, o); + }; + } + + return function corsMiddleware(req, res, next) { + optionsCallback(req, function (err, options) { + if (err) { + next(err); + } else { + var corsOptions = assign({}, defaults, options); + var originCallback = null; + if (corsOptions.origin && typeof corsOptions.origin === 'function') { + originCallback = corsOptions.origin; + } else if (corsOptions.origin) { + originCallback = function (origin, cb) { + cb(null, corsOptions.origin); + }; + } + + if (originCallback) { + originCallback(req.headers.origin, function (err2, origin) { + if (err2 || !origin) { + next(err2); + } else { + corsOptions.origin = origin; + cors(corsOptions, req, res, next); + } + }); + } else { + next(); + } + } + }); + }; + } + + // can pass either an options hash, an options delegate, or nothing + module.exports = middlewareWrapper; + +}()); diff --git a/backend/node_modules/cors/package.json b/backend/node_modules/cors/package.json new file mode 100644 index 0000000..ff37d98 --- /dev/null +++ b/backend/node_modules/cors/package.json @@ -0,0 +1,41 @@ +{ + "name": "cors", + "description": "Node.js CORS middleware", + "version": "2.8.5", + "author": "Troy Goode (https://github.com/troygoode/)", + "license": "MIT", + "keywords": [ + "cors", + "express", + "connect", + "middleware" + ], + "repository": "expressjs/cors", + "main": "./lib/index.js", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "devDependencies": { + "after": "0.8.2", + "eslint": "2.13.1", + "express": "4.16.3", + "mocha": "5.2.0", + "nyc": "13.1.0", + "supertest": "3.3.0" + }, + "files": [ + "lib/index.js", + "CONTRIBUTING.md", + "HISTORY.md", + "LICENSE", + "README.md" + ], + "engines": { + "node": ">= 0.10" + }, + "scripts": { + "test": "npm run lint && nyc --reporter=html --reporter=text mocha --require test/support/env", + "lint": "eslint lib test" + } +} diff --git a/backend/node_modules/object-assign/index.js b/backend/node_modules/object-assign/index.js new file mode 100644 index 0000000..0930cf8 --- /dev/null +++ b/backend/node_modules/object-assign/index.js @@ -0,0 +1,90 @@ +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ + +'use strict'; +/* eslint-disable no-unused-vars */ +var getOwnPropertySymbols = Object.getOwnPropertySymbols; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var propIsEnumerable = Object.prototype.propertyIsEnumerable; + +function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + + return Object(val); +} + +function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + + // Detect buggy property enumeration order in older V8 versions. + + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== + 'abcdefghijklmnopqrst') { + return false; + } + + return true; + } catch (err) { + // We don't expect any of the above to throw, but better to be safe. + return false; + } +} + +module.exports = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + + return to; +}; diff --git a/backend/node_modules/object-assign/license b/backend/node_modules/object-assign/license new file mode 100644 index 0000000..654d0bf --- /dev/null +++ b/backend/node_modules/object-assign/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/backend/node_modules/object-assign/package.json b/backend/node_modules/object-assign/package.json new file mode 100644 index 0000000..503eb1e --- /dev/null +++ b/backend/node_modules/object-assign/package.json @@ -0,0 +1,42 @@ +{ + "name": "object-assign", + "version": "4.1.1", + "description": "ES2015 `Object.assign()` ponyfill", + "license": "MIT", + "repository": "sindresorhus/object-assign", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "test": "xo && ava", + "bench": "matcha bench.js" + }, + "files": [ + "index.js" + ], + "keywords": [ + "object", + "assign", + "extend", + "properties", + "es2015", + "ecmascript", + "harmony", + "ponyfill", + "prollyfill", + "polyfill", + "shim", + "browser" + ], + "devDependencies": { + "ava": "^0.16.0", + "lodash": "^4.16.4", + "matcha": "^0.7.0", + "xo": "^0.16.0" + } +} diff --git a/backend/node_modules/object-assign/readme.md b/backend/node_modules/object-assign/readme.md new file mode 100644 index 0000000..1be09d3 --- /dev/null +++ b/backend/node_modules/object-assign/readme.md @@ -0,0 +1,61 @@ +# object-assign [![Build Status](https://travis-ci.org/sindresorhus/object-assign.svg?branch=master)](https://travis-ci.org/sindresorhus/object-assign) + +> ES2015 [`Object.assign()`](http://www.2ality.com/2014/01/object-assign.html) [ponyfill](https://ponyfill.com) + + +## Use the built-in + +Node.js 4 and up, as well as every evergreen browser (Chrome, Edge, Firefox, Opera, Safari), +support `Object.assign()` :tada:. If you target only those environments, then by all +means, use `Object.assign()` instead of this package. + + +## Install + +``` +$ npm install --save object-assign +``` + + +## Usage + +```js +const objectAssign = require('object-assign'); + +objectAssign({foo: 0}, {bar: 1}); +//=> {foo: 0, bar: 1} + +// multiple sources +objectAssign({foo: 0}, {bar: 1}, {baz: 2}); +//=> {foo: 0, bar: 1, baz: 2} + +// overwrites equal keys +objectAssign({foo: 0}, {foo: 1}, {foo: 2}); +//=> {foo: 2} + +// ignores null and undefined sources +objectAssign({foo: 0}, null, {bar: 1}, undefined); +//=> {foo: 0, bar: 1} +``` + + +## API + +### objectAssign(target, [source, ...]) + +Assigns enumerable own properties of `source` objects to the `target` object and returns the `target` object. Additional `source` objects will overwrite previous ones. + + +## Resources + +- [ES2015 spec - Object.assign](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign) + + +## Related + +- [deep-assign](https://github.com/sindresorhus/deep-assign) - Recursive `Object.assign()` + + +## License + +MIT © [Sindre Sorhus](https://sindresorhus.com) diff --git a/backend/package-lock.json b/backend/package-lock.json index c14fe1e..7044a48 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "bcryptjs": "^3.0.2", + "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", @@ -506,6 +507,19 @@ "node": ">=6.6.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1668,6 +1682,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/backend/package.json b/backend/package.json index 4f3cb9e..a2e12bf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,7 @@ { "dependencies": { "bcryptjs": "^3.0.2", + "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 8c97cdc..c6048fc 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -3,22 +3,24 @@ import express from 'express'; import bcrypt from 'bcryptjs'; import { generateToken } from "../utils/utils.js"; + //SIGNUP export const signup = async(req,res,next) =>{ + console.log("Signup request received"); const {fname,lname,mname,sex,birthdate,email,password,role,position,profilePic,phone, address} = req.body; try { //check if all fields have data - if(!fname || !lname || !mname || !sex || !birthdate || !email || !password || !role || !position || !profilePic || !phone || !address) + if(!fname || !lname || !sex || !birthdate || !email || !password || !role || !position || !phone || !address) { - const error = new Error("All fields are required"); + const error = new Error("All fields are required"); error.statusCode = 400; return next(error); } - //accepts admin and employee only (accesibility) + //accepts admin and employee only (accessibility) - if (role !== 'admin' && role !== 'employee'){ + if (role !== 'admin' && role !== 'staff'){ return res.status(400).json({message: "Invalid role. Choose the right role"}); } @@ -50,21 +52,25 @@ export const signup = async(req,res,next) =>{ const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); + // https://avatar-placeholder.iran.liara.run/ + const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${email}`; + const girlProfilePic = `https://avatar.iran.liara.run/public/girl?username=${email}`; //for new employee + const newEmployee = await Employee.create({ - fname, - lname, - mname, - sex, - birthdate, - email: email.toLowerCase(), - password: hashedPassword, - role, - position, - profilePic, - phone, - address + fname, + lname, + mname, + sex: sex.toLowerCase(), + birthdate, + email: email.toLowerCase(), + password: hashedPassword, + role, + position, + phone, + address, + profilePic : sex.toLowerCase() === "male" ? boyProfilePic : girlProfilePic }); //generate token @@ -77,14 +83,19 @@ export const signup = async(req,res,next) =>{ return res.status(201).json({ message: "Employee created successfully", employee:{ + emp_id: newEmployee.emp_id, id: newEmployee.id, fname: newEmployee.fname, lname: newEmployee.lname, + mname: newEmployee.mname, sex: newEmployee.sex, birthdate: newEmployee.birthdate, email: newEmployee.email, role: newEmployee.role, position: newEmployee.position, + profilePic: newEmployee.profilePic, + phone: newEmployee.phone, + address: newEmployee.address } }) @@ -134,6 +145,7 @@ export const login = async(req,res) =>{ //send response res.status(200).json({ message: "Login successful", + token: LoginEmployee.token, employee:{ id: LoginEmployee.id, fname: LoginEmployee.fname, @@ -144,9 +156,9 @@ export const login = async(req,res) =>{ email: LoginEmployee.email, role: LoginEmployee.role, position: LoginEmployee.position, - profilePic: LoginEmployee.profilePic, phone: LoginEmployee.phone, - address: LoginEmployee.address + address: LoginEmployee.address, + profilePic: LoginEmployee.profilePic } }) @@ -167,6 +179,9 @@ export const logout = async(req,res) =>{ res.status(200).json({message: "Logged out successfully"}); } catch (error) { - console.log() + console.log(error); + res.status(500).json({message: "Internal server error (Logout)"}); + + } } \ No newline at end of file diff --git a/backend/src/middlewares/auth.middleware.js b/backend/src/middlewares/auth.middleware.js index 320e9be..9d88c3c 100644 --- a/backend/src/middlewares/auth.middleware.js +++ b/backend/src/middlewares/auth.middleware.js @@ -24,4 +24,4 @@ export const protectRoute = async(req, res, next) =>{ } catch (error) { } -} \ No newline at end of file +} diff --git a/backend/src/models/employee.model.js b/backend/src/models/employee.model.js index 32628d1..c265c24 100644 --- a/backend/src/models/employee.model.js +++ b/backend/src/models/employee.model.js @@ -26,7 +26,7 @@ const Employee = db.define('employee',{ }, mname:{ type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, sex:{ type: DataTypes.ENUM('male', 'female'), @@ -58,7 +58,7 @@ const Employee = db.define('employee',{ allowNull: false, }, role:{ - type: DataTypes.ENUM('admin', 'employee'), + type: DataTypes.ENUM('admin', 'staff'), allowNull: false, }, position:{ diff --git a/backend/src/routes/auth.route.js b/backend/src/routes/auth.route.js index 40ff4f2..db1561c 100644 --- a/backend/src/routes/auth.route.js +++ b/backend/src/routes/auth.route.js @@ -1,5 +1,6 @@ import express from 'express'; import { signup, login, logout } from '../controllers/auth.controller.js'; +import {protectRoute} from '../middlewares/auth.middleware.js'; const router = express.Router(); @@ -8,4 +9,9 @@ router.post("/signup",signup); router.post("/logout",logout); + +//middlewares +// router.post("/update-profile", protectRoute, updateProfile); +// router.post("/update-password", protectRoute, updatePassword); + export default router; \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js index b32aae8..5f81653 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,27 +1,33 @@ -import express, {Router} from 'express'; +import express from 'express'; import dotenv from 'dotenv' import bodyparser from 'body-parser'; import sequelize from './utils/db.js'; import EmployeeModel from './models/employee.model.js'; import CrudRoutes from './routes/crud.route.js' import authRoute from './routes/auth.route.js' +import cors from 'cors' const app = express(); +const corsOptions = { + origin: 'http://localhost:5173', + methods: ['GET','POST','PUT','DELETE','OPTIONS'], + allowedHeaders:['Content-Type', 'Authorization'], + optionsSuccessStatus: 200 +}; + dotenv.config(); const PORT = process.env.PORT || 5001; -//middleware -app.use(bodyparser.json()); +//middlewares +app.use(express.json()); app.use(bodyparser.urlencoded({extended: false})); -app.use((req, res, next) =>{ - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); - next(); +app.use(cors(corsOptions)); + + -}); //test routes CRUD @@ -33,12 +39,28 @@ app.get('/',(req,res,next)=>{ app.use('/employees',CrudRoutes); app.use('/api/auth', authRoute); //error handling -app.use((error, req,res,next) =>{ - console.log(error); +app.use((error, req, res, next) =>{ // Added 'next' for completeness, though not strictly used here + console.error("--- GLOBAL ERROR HANDLER CAUGHT ---"); // More prominent logging + console.error("Timestamp:", new Date().toISOString()); + console.error("Request URL:", req.originalUrl); + console.error("Request Method:", req.method); + console.error("Error Name:", error.name); + console.error("Error Message:", error.message); + console.error("Error Status Code:", error.statusCode); + if (error.data) { + console.error("Error Data:", error.data); + } + console.error("Error Stack:", error.stack); // Log the full stack + console.error("-----------------------------------"); + const status = error.statusCode || 500; - const message = error.message; - const data = error.data - res.status(status).json({message: message}); + + const message = error.message || "An internal server error occurred."; + const responseError = { message: message }; + if (error.data) { // If the error object has a 'data' property, include it + responseError.data = error.data; + } + res.status(status).json(responseError); }); @@ -64,12 +86,4 @@ startServer(); -// app.use(express.json()); -// app.use("/api/auth", authRoute); - - - -// app.listen(5001,() =>{ -// console.log(`Server is running on PORT ${PORT}`); -// }) diff --git a/frontend/index.html b/frontend/index.html index 6b1f708..8bc5abb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,7 @@ + MPCC-System diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7d4195b..185e16c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,11 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.2", @@ -897,6 +902,76 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz", + "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2423,7 +2498,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2772,6 +2846,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2835,6 +2921,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2977,6 +3072,17 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3025,6 +3131,12 @@ "react-dom": ">=16" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3e27edf..2f38dc4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.2", diff --git a/frontend/public/images/icons/active-customer-icon.svg b/frontend/public/images/icons/active-customer-icon.svg new file mode 100644 index 0000000..a9d6321 --- /dev/null +++ b/frontend/public/images/icons/active-customer-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/active-icon.png b/frontend/public/images/icons/active-icon.png new file mode 100644 index 0000000..bf86c8e Binary files /dev/null and b/frontend/public/images/icons/active-icon.png differ diff --git a/frontend/public/images/icons/appointment-icon.png b/frontend/public/images/icons/appointment-icon.png new file mode 100644 index 0000000..c56f440 Binary files /dev/null and b/frontend/public/images/icons/appointment-icon.png differ diff --git a/frontend/public/images/icons/billing-icon.png b/frontend/public/images/icons/billing-icon.png new file mode 100644 index 0000000..e5cdda9 Binary files /dev/null and b/frontend/public/images/icons/billing-icon.png differ diff --git a/frontend/public/images/icons/customer-icon.png b/frontend/public/images/icons/customer-icon.png new file mode 100644 index 0000000..42fada7 Binary files /dev/null and b/frontend/public/images/icons/customer-icon.png differ diff --git a/frontend/public/images/icons/dashboard-icon.png b/frontend/public/images/icons/dashboard-icon.png new file mode 100644 index 0000000..625b423 Binary files /dev/null and b/frontend/public/images/icons/dashboard-icon.png differ diff --git a/frontend/public/images/icons/indicator-green.png b/frontend/public/images/icons/indicator-green.png new file mode 100644 index 0000000..0d52a4b Binary files /dev/null and b/frontend/public/images/icons/indicator-green.png differ diff --git a/frontend/public/images/icons/indicator-red.png b/frontend/public/images/icons/indicator-red.png new file mode 100644 index 0000000..4d8e705 Binary files /dev/null and b/frontend/public/images/icons/indicator-red.png differ diff --git a/frontend/public/images/icons/indicatorUp.svg b/frontend/public/images/icons/indicatorUp.svg new file mode 100644 index 0000000..dcf5dda --- /dev/null +++ b/frontend/public/images/icons/indicatorUp.svg @@ -0,0 +1,2 @@ + + diff --git a/frontend/public/images/icons/inventory-icon.png b/frontend/public/images/icons/inventory-icon.png new file mode 100644 index 0000000..ecbbec7 Binary files /dev/null and b/frontend/public/images/icons/inventory-icon.png differ diff --git a/frontend/public/images/icons/payroll-icon.png b/frontend/public/images/icons/payroll-icon.png new file mode 100644 index 0000000..fd63b43 Binary files /dev/null and b/frontend/public/images/icons/payroll-icon.png differ diff --git a/frontend/public/images/icons/sale-icon.png b/frontend/public/images/icons/sale-icon.png new file mode 100644 index 0000000..2517750 Binary files /dev/null and b/frontend/public/images/icons/sale-icon.png differ diff --git a/frontend/public/images/icons/sales-icon.svg b/frontend/public/images/icons/sales-icon.svg new file mode 100644 index 0000000..2a550b7 --- /dev/null +++ b/frontend/public/images/icons/sales-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/transac-icon.png b/frontend/public/images/icons/transac-icon.png new file mode 100644 index 0000000..6c0f75b Binary files /dev/null and b/frontend/public/images/icons/transac-icon.png differ diff --git a/frontend/public/images/icons/transaction-icon.svg b/frontend/public/images/icons/transaction-icon.svg new file mode 100644 index 0000000..41bdfd2 --- /dev/null +++ b/frontend/public/images/icons/transaction-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/images/icons/user-control-icon.png b/frontend/public/images/icons/user-control-icon.png new file mode 100644 index 0000000..5b8f92d Binary files /dev/null and b/frontend/public/images/icons/user-control-icon.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5273622..f122aa6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,13 +4,8 @@ import { Routes, Route} from 'react-router-dom'; import Login from "./pages/Login"; import Signup from "./pages/Signup"; import Dashboard from "./pages/Dashboard"; - - import {Toaster} from "react-hot-toast" - - - function App() { diff --git a/frontend/src/components/ActiveAppointments.jsx b/frontend/src/components/ActiveAppointments.jsx new file mode 100644 index 0000000..5d076de --- /dev/null +++ b/frontend/src/components/ActiveAppointments.jsx @@ -0,0 +1,49 @@ +import React from "react" +import placeHolder from "../../public/images/icons/customer-icon.png" + + +function ActiveAppointments(){ + +// const ListData = ({profilePic,initialDate, endDate, customerName, service, typeService}) => ( +//
+//
    +// profile-picture +//
    +// +//
+//
+ + +// ); + + + return( + + <> + + + + + ) + + +} + +export default ActiveAppointments; \ No newline at end of file diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..0346ec3 --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,26 @@ +import React from "react"; + +function Footer() { + return ( +
{/* Added mt-auto to push footer to the bottom */} +
+ Logo +
+
+ +

+ Copyright Metropolitan Pest Control Corporation. All + Rights Reserved +

+
+ © {new Date().getFullYear()} Developed by Push And Pull Technologies +
+
+) +} + +export default Footer; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 2f61246..fd41772 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,5 +1,6 @@ import React from "react"; + function Navbar() { return (
@@ -30,6 +31,7 @@ function Navbar() {
+ ); } diff --git a/frontend/src/components/NavbarDashboard.jsx b/frontend/src/components/NavbarDashboard.jsx new file mode 100644 index 0000000..9495ccf --- /dev/null +++ b/frontend/src/components/NavbarDashboard.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import SideMenu from "./SideMenu"; + + +function NavbarDashboard() { + return ( + <> +
+
+ + {/* */} +
+
+ Logo +
+ +
+
+
+ Tailwind CSS Navbar component +
+
+ +
+ +
+ + + + + + + + ); +} + +export default NavbarDashboard; diff --git a/frontend/src/components/SideMenu.jsx b/frontend/src/components/SideMenu.jsx index e69de29..6b26745 100644 --- a/frontend/src/components/SideMenu.jsx +++ b/frontend/src/components/SideMenu.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import appointmentIcon from '/images/icons/appointment-icon.png'; +import dashboardIcon from '/images/icons/dashboard-icon.png'; +import customerIcon from '/images/icons/customer-icon.png'; +import inventoryIcon from '/images/icons/inventory-icon.png'; +import billingIcon from '/images/icons/billing-icon.png'; +import payrollIcon from '/images/icons/payroll-icon.png'; +import userIcon from '/images/icons/user-control-icon.png'; + + + + +function SideMenu() { + +// Define the initial structure of menu items without icons +const rawMenuItems = [ + { label: 'Dashboard', link: '/dashboard' }, // Assuming a route for dashboard + { + label: 'Appointment Scheduler', + subItems: [ + { label: 'Scheduler', link: '#' }, + { label: 'Transaction Records', link: '#' }, + { label: 'Appointments', link: '#' }, + ] + }, + { + label: 'Customer Management', + subItems: [ + { label: 'Customer Records', link: '#' }, + { label: 'Transaction History', link: '#' }, + ] + }, + { + label: 'Inventory Tracking', + subItems: [ + { label: 'Release per Transaction', link: '#' }, + { label: 'In-Stocks', link: '#' }, + ] + }, + { + label: 'Billing & Invoicing', + subItems: [ + { label: 'Billing Records', link: '#' }, + { label: 'Invoice Records', link: '#' }, + ] + }, + { + label: 'Payroll Management', + subItems: [ + { label: 'Add Salary Details', link: '#' }, + { label: 'Payroll Calculator', link: '#' }, + { label: 'Payslips', link: '#' }, + ] + }, + { + label: 'User Control', + subItems: [ + { label: 'Roles and Access Control', link: '#' }, + ] + }, +]; + +// Map icons to menu items, creating a new array called 'menuItems' +const menuItems = rawMenuItems.map(item =>{ + let icon = null; + if(item.label === 'Dashboard') icon = dashboardIcon; + else if(item.label === 'Appointment Scheduler') icon = appointmentIcon; + else if(item.label === 'Customer Management') icon = customerIcon; + else if(item.label === 'Inventory Tracking') icon = inventoryIcon; + else if(item.label === 'Billing & Invoicing') icon = billingIcon; + else if(item.label === 'Payroll Management') icon = payrollIcon; + else if(item.label === 'User Control') icon = userIcon; + return { + ...item, + icon + }; +}); + + + return ( + <> + + + + + + ) +} + +export default SideMenu; \ No newline at end of file diff --git a/frontend/src/components/UpcomingAppointments.jsx b/frontend/src/components/UpcomingAppointments.jsx new file mode 100644 index 0000000..ae074fe --- /dev/null +++ b/frontend/src/components/UpcomingAppointments.jsx @@ -0,0 +1,45 @@ +import React from "react" +import placeHolder from "../../public/images/icons/customer-icon.png" + +function UpcomingAppointments() { + return ( + <> + + + + + + + + + + ) +} + + + + + + +export default UpcomingAppointments; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index bf5f135..370ad7b 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -4,6 +4,7 @@ import {BrowserRouter} from 'react-router-dom' import './index.css' import App from './App.jsx' + createRoot(document.getElementById('root')).render( diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index f17416f..38345e2 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,11 +1,148 @@ import React from 'react'; +import NavbarDashboard from '../components/NavbarDashboard'; +import SideMenu from '../components/SideMenu'; + +import salesIconUrl from '/images/icons/sale-icon.png'; +import transactionIconUrl from '/images/icons/transac-icon.png'; +import customerIconUrl from '/images/icons/active-icon.png'; +import indicatorGreen from '/images/icons/indicator-green.png'; +import indicatorRed from '/images/icons/indicator-red.png'; +import ActiveAppointments from '../components/ActiveAppointments'; +import UpcomingAppointments from '../components/UpcomingAppointments'; function Dashboard() { + // A simple component for individual stat cards + const StatCard = ({ title, value, desc, icon, indicator, indicatorAlt }) => ( +
{/* Added padding and background for clarity */} + {`${title} {/* Dynamic alt, using standard Tailwind w-16/h-16 */} +
{title}
+
{value}
+
+ + {desc && indicator && (
+ {indicatorAlt} + {desc}% +

Than the last month

+ + +
+ )} + {desc && !indicator && ( +
+

{desc}

+
+ )} + +
+
+ ); + const getStatCardData = (descValue) => { + const numericDesc = parseFloat(descValue); + + if(!isNaN(numericDesc) && !/[a-zA-Z]/.test(descValue)){ + const isPositiveTrend = numericDesc > 10; + const indicator = isPositiveTrend ? indicatorGreen : indicatorRed; + const indicatorAlt = isPositiveTrend ? "Increase indicator" : "Decrease indicator"; + return {desc: descValue, indicator, indicatorAlt }; + } + // If descValue is not a simple number, return it as desc and no indicator + // This ensures StatCard always gets a desc prop if one was intended. + return { desc: descValue, indicator: null, indicatorAlt: '' }; + }; + + const StatGraphCard_1 = ({ title, viewLink}) => ( +
{/* Added padding and background for clarity */} + +
+
{title}
+ + +
+
+
+ + +
+ + ); + + const StatGraphCard_2 = ({ title, viewLink}) => ( +
{/* Added padding and background for clarity */} + +
+
{title}
+ + +
+
+
+ + +
+ + ); + return ( -
-

Dashboard

+
{/* Use flex-col for overall page structure */} + +
{/* Flex container for SideMenu and main content */} + {/* SideMenu will take its defined width (w-64 from its own file) */} +
{/* Main content area */} +

Dashboard Overview

+ + {/* Container for multiple stats cards, using a responsive grid */} +
+ + + +
+ + + {/* Container for multiple stats cards, using a responsive grid */} +
+ console.log("View All Reservations")} //temporary function for view link + value="89,400" + desc="21% more than last month" /> + + console.log("View All Reservations")} //temporary function for view link + value="89,400" + desc="21% more than last month" /> + + + + +
+ + {/* You can add more dashboard sections/components here */} + + +
+ +
+
+ + ); } diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 8ac5e22..3383b4c 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,41 +1,156 @@ -import React from "react"; +import React, {useState, useEffect} from "react"; import Navbar from "../components/Navbar.jsx"; +import {Toaster, toast} from "react-hot-toast"; +import { useNavigate, useLocation, Link } from "react-router-dom"; +import Footer from "../components/Footer.jsx"; + function Login() { + //initialize to object + const navigate = useNavigate(); + const location = useLocation(); + + + const initialFormData = { + emp_id: "", + password: "", + } + + const [formData, setFormData] = useState(initialFormData); + + //for loading screen + const [loading, setLoading] = useState(false); + + + + useEffect(() =>{ + + if(location.state?.emp_id){ + setFormData((prevData) =>({ + ...prevData, + emp_id: location.state.emp_id + + })); + navigate(location.pathname, { replace: true, state: {} }) + } + },[location.state, navigate,location.pathname]); + + const handleChange = (e) =>{ + const {name, value} = e.target; + setFormData((prevData) => ({...prevData, [name]: value})); + } + + + //HANDLE LOGIN FUNCTION + const handleLogin = async (e) => { + e.preventDefault(); + setLoading(true); + + //basic validation + if(!formData.emp_id){ + toast.error("Employee ID is required"); + setLoading(false); + return; + } + + if(!formData.password){ + toast.error("Password is required"); + setLoading(false); + return; + } + + try { + const response =await fetch("http://localhost:5001/api/auth/login",{ + method: "POST", + headers: {"Content-Type" : "application/json"}, + body: JSON.stringify(formData), + + }); + + let result; + try{ + result = await response.json(); + } catch(jsonError){ + console.error("Error parsng JSON from login response:", jsonError); + const textResponse = await response.text(); + toast.error(`Login failed: Server returned an unexpected response. Status: ${response.status}. ${textResponse}`); + setLoading(false); + return; + } + if(response.ok){ + toast.success(result.message|| "Login successful!"); + + //Store token and user data in LocalStorage + if(result.token){ + localStorage.setItem("authToken", result.token); + } + if(result.employee){ + localStorage.setItem("user",JSON.stringify(result.employee)); + } + + + + console.log("Logged in employee", result.employee); + + navigate("/dashboard"); + } else { + toast.error(result.message ||"Login Failed. Please check your credentials."); + + } + + } catch (networkError) { + console.error("Network error during login:", networkError); + toast.error("Network error. Please check your internet connection."); + + } finally{ + setLoading(false); + } + } + + + + + + + + return ( <> - + {/* Hero Image */}
Hero Section
{/* Login Section */}
-
+

LOGIN

-
+ {/* Username Field */}
@@ -53,51 +168,43 @@ function Login() { name="password" required placeholder="Enter your password" - className="w-full mt-1 px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-700" - /> + className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-700" + + value={formData.password} + onChange={handleChange} + />
{/* Submit Button */}
+

Don't have an account?{" "} - Sign up - +

- {/* Footer */} -
-
- Logo -
-
- -

- Copyright Metropolitan Pest Control Corporation. All - Rights Reserved -

-
+