From d570ea245424ec5c18fe08295bdd2fc695393c54 Mon Sep 17 00:00:00 2001 From: Kibreab Chanyalew Date: Thu, 24 Apr 2025 23:02:32 +0300 Subject: [PATCH 01/11] users endpoint with admin functionalities. --- .gitignore | 3 + backend/.gitignore | 3 + backend/app.js | 37 + backend/controllers/users.js | 47 + backend/docker-compose.yml | 24 + backend/index.js | 9 + backend/package-lock.json | 1968 +++++++++++++++++ backend/package.json | 27 + .../20250424152853_init/migration.sql | 75 + backend/prisma/migrations/migration_lock.toml | 3 + backend/prisma/schema.prisma | 74 + backend/seed.js | 111 + backend/utils/config.js | 10 + backend/utils/logger.js | 13 + backend/utils/login.js | 54 + backend/utils/middleware.js | 105 + 16 files changed, 2563 insertions(+) create mode 100644 .gitignore create mode 100644 backend/.gitignore create mode 100644 backend/app.js create mode 100644 backend/controllers/users.js create mode 100644 backend/docker-compose.yml create mode 100644 backend/index.js create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/prisma/migrations/20250424152853_init/migration.sql create mode 100644 backend/prisma/migrations/migration_lock.toml create mode 100644 backend/prisma/schema.prisma create mode 100644 backend/seed.js create mode 100644 backend/utils/config.js create mode 100644 backend/utils/logger.js create mode 100644 backend/utils/login.js create mode 100644 backend/utils/middleware.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b06f9bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +part4 +.env +node_modules diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..11ddd8d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,3 @@ +node_modules +# Keep environment variables out of version control +.env diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 0000000..2546ca5 --- /dev/null +++ b/backend/app.js @@ -0,0 +1,37 @@ +const express = require("express"); +const { info, error } = require("./utils/logger"); +const app = express(); +const cors = require("cors"); +const middleweare = require('./utils/middleware') +const userRouter = require('./controllers/users'); +const reservationRouter = require('./controllers/reservations'); +const loginRouter = require("./utils/login"); + + +app.use(cors()); +app.use(express.json()); +app.use(middleweare.requestLogger) +app.use(middleweare.getTokenFrom) + + + +// app.use(middleweare.unknownEndpoint) +app.get('/', (req, rep) => { + rep.send('

Hello World

') +}) + + + +//routers + + +// app.use('/api/reservations', middleweare.identifyUser, reservationRouter) +app.use('/api/users', userRouter) +app.use('/api/login', loginRouter) + +app.use(middleweare.unknownEndpoint) +app.use(middleweare.errorHandler) + +module.exports = app + + diff --git a/backend/controllers/users.js b/backend/controllers/users.js new file mode 100644 index 0000000..cb1d649 --- /dev/null +++ b/backend/controllers/users.js @@ -0,0 +1,47 @@ +const userRouter = require('express').Router() +const {errorHandler} = require('../utils/middleware') +const { error } = require('../utils/logger') +const bcrypt = require('bcrypt') +const { PrismaClient } = require('@prisma/client') +const prisma = new PrismaClient(); +const { isAdmin, identifyUser } = require('../utils/middleware') + + + +userRouter.get('/',identifyUser, isAdmin, async(req, res) => { + try { + const users = await prisma.user.findMany(); + res.json(users); + } catch (error) { + res.status(500).json({ error: 'Could not retrieve users' }); + } +}); + +userRouter.get('/:id', identifyUser, async(req,res) => { + const { id } = req.params; + try{ + const foundUser = await prisma.user.findUnique({ + where: { id: parseInt(id) }, + }) + + if (!foundUser){ + return(res.status(404).json({error: "No such user found"})) + } + if(foundUser.role ==="ADMIN" && req.user.role !== "ADMIN"){ + return(res.status(403).json({error: "Forbidden stuff"})) + } + res.json(foundUser); + }catch(e){ + console.error(error); + res.status(500).json({ error: 'Could not retrieve user' }); + } +}) + +// might implement a way to add new users as an admin maybe? + +userRouter.post('/', identifyUser, isAdmin, async(req, res) => { + // TODO +}) + + +module.exports = userRouter; diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..d454c06 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,24 @@ +# version: '3.8' +services: + dev-db: + image: postgres:13 + ports: + - 5434:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 123 + POSTGRES_DB: nest + networks: + - freecodecamp + test-db: + image: postgres:13 + ports: + - 5435:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 123 + POSTGRES_DB: nest + networks: + - freecodecamp +networks: + freecodecamp: diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..9561d9f --- /dev/null +++ b/backend/index.js @@ -0,0 +1,9 @@ +const { PORT } = require("./utils/config"); +const { info, error } = require("./utils/logger"); +const app = require('./app') + + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); + \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..9432630 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1968 @@ +{ + "name": "unispace-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unispace-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.6.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-validator": "^7.0.1", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "nodemon": "^3.1.4" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@prisma/client": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.6.0.tgz", + "integrity": "sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.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", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..71f6319 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "unispace-backend", + "version": "1.0.0", + "description": "Backend for the Unispace classroom reservation system", + "main": "index.js", + "scripts": { + "start": "node index.js", + "db:seed": "node seed.js", + "dev": "nodemon index.js", + "prisma:migrate": "npx prisma migrate dev", + "prisma:generate": "npx prisma generate" + }, + "author": "Your Name", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.6.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-validator": "^7.0.1", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "nodemon": "^3.1.4" + } +} diff --git a/backend/prisma/migrations/20250424152853_init/migration.sql b/backend/prisma/migrations/20250424152853_init/migration.sql new file mode 100644 index 0000000..a2bb4da --- /dev/null +++ b/backend/prisma/migrations/20250424152853_init/migration.sql @@ -0,0 +1,75 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('STUDENT', 'REPRESENTATIVE', 'ADMIN'); + +-- CreateTable +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'STUDENT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "buildings" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "floorId" INTEGER NOT NULL, + "status" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "buildings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "floors" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "floors_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "classrooms" ( + "id" SERIAL NOT NULL, + "floorId" INTEGER NOT NULL, + "buildingId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "classrooms_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "reservations" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "classroomId" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "reservations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + +-- AddForeignKey +ALTER TABLE "buildings" ADD CONSTRAINT "buildings_floorId_fkey" FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_floorId_fkey" FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_buildingId_fkey" FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "reservations" ADD CONSTRAINT "reservations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "reservations" ADD CONSTRAINT "reservations_classroomId_fkey" FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..4a134b6 --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,74 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Role { + STUDENT + REPRESENTATIVE + ADMIN // Added Admin role +} + +model User { + id Int @id @default(autoincrement()) + username String @unique + email String // Added email + password String + role Role @default(STUDENT) + reservations Reservation[] + createdAt DateTime @default(now()) // Added createdAt + + @@map("users") +} + +model Building { + id Int @id @default(autoincrement()) + name String + floorId Int // Foreign Key + floor Floor @relation(fields: [floorId], references: [id]) + status String? // Nullable status + createdAt DateTime @default(now()) + classrooms Classroom[] + + @@map("buildings") +} + +model Floor { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + buildings Building[] + classrooms Classroom[] + + @@map("floors") +} + +model Classroom { + id Int @id @default(autoincrement()) + floorId Int // Foreign Key + floor Floor @relation(fields: [floorId], references: [id]) + buildingId Int // Foreign Key + building Building @relation(fields: [buildingId], references: [id]) + name String + createdAt DateTime @default(now()) + reservations Reservation[] + + @@map("classrooms") +} + +model Reservation { + id Int @id @default(autoincrement()) + userId Int // Foreign Key + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + classroomId Int // Foreign Key + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade) + startTime DateTime + endTime DateTime + createdAt DateTime @default(now()) + + @@map("reservations") +} diff --git a/backend/seed.js b/backend/seed.js new file mode 100644 index 0000000..e5b90bb --- /dev/null +++ b/backend/seed.js @@ -0,0 +1,111 @@ +// seed.js +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const bcrypt = require('bcrypt'); + +async function main() { + // Create Users + const hashedPassword = await bcrypt.hash('password123', 10); // Hash the password + + const adminUser = await prisma.user.create({ + data: { + username: 'admin', + email: 'admin@example.com', + password: hashedPassword, + role: 'ADMIN', + }, + }); + + const repUser = await prisma.user.create({ + data: { + username: 'rep1', + email: 'rep1@example.com', + password: hashedPassword, + role: 'REPRESENTATIVE', + }, + }); + + const studentUser = await prisma.user.create({ + data: { + username: 'student1', + email: 'student1@example.com', + password: hashedPassword, + role: 'STUDENT', + }, + }); + + // Create Floors + const floor1 = await prisma.floor.create({ + data: { + name: '1st Floor', + }, + }); + + const floor2 = await prisma.floor.create({ + data: { + name: '2nd Floor', + }, + }); + + // Create Buildings + const buildingA = await prisma.building.create({ + data: { + name: 'Building A', + floorId: floor1.id, + }, + }); + + const buildingB = await prisma.building.create({ + data: { + name: 'Building B', + floorId: floor2.id, + }, + }); + + // Create Classrooms + const classroom101 = await prisma.classroom.create({ + data: { + name: 'Room 101', + floorId: floor1.id, + buildingId: buildingA.id, + }, + }); + + const classroom201 = await prisma.classroom.create({ + data: { + name: 'Room 201', + floorId: floor2.id, + buildingId: buildingB.id, + }, + }); + + // Create Reservations + await prisma.reservation.create({ + data: { + userId: repUser.id, + classroomId: classroom101.id, + startTime: new Date(2025, 0, 1, 9, 0, 0), // January 1, 2025, 9:00 AM + endTime: new Date(2025, 0, 1, 10, 0, 0), + }, + }); + + await prisma.reservation.create({ + data: { + userId: studentUser.id, + classroomId: classroom201.id, + startTime: new Date(2025, 0, 2, 14, 0, 0), // January 2, 2025, 2:00 PM + endTime: new Date(2025, 0, 16, 0, 0), + }, + }); + + console.log('Seed data inserted successfully!'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/utils/config.js b/backend/utils/config.js new file mode 100644 index 0000000..e9d9264 --- /dev/null +++ b/backend/utils/config.js @@ -0,0 +1,10 @@ +require('dotenv').config() + +const PORT = process.env.PORT +const MONGODB_URL = process.env.NODE_ENV === 'test' ? process.env.TEST_URL : process.env.MONGODB_URL + + +module.exports = { + PORT, + MONGODB_URL +} \ No newline at end of file diff --git a/backend/utils/logger.js b/backend/utils/logger.js new file mode 100644 index 0000000..537eec6 --- /dev/null +++ b/backend/utils/logger.js @@ -0,0 +1,13 @@ + +const info = (...params) => { + if (process.env.NODE_ENV !== 'test') + console.log(...params) + } + +const error = (...params) => { + console.error(...params) + } + +module.exports = { + info, error +} \ No newline at end of file diff --git a/backend/utils/login.js b/backend/utils/login.js new file mode 100644 index 0000000..0448a43 --- /dev/null +++ b/backend/utils/login.js @@ -0,0 +1,54 @@ +const jwt = require('jsonwebtoken'); +const loginRouter = require('express').Router(); +const bcrypt = require('bcrypt'); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +loginRouter.post('/', async (req, rep) => { + const { username, password } = req.body; + + if (!(username && password)) { + return rep.status(401).json({ + error: "Password or Username field empty" + }); + } + + try { + const user = await prisma.user.findUnique({ + where: { username }, + }); + + const correctPass = user === null ? false : await bcrypt.compare(password, user.password); + + if (!(user && correctPass)) { + console.log(user); + console.log(password); + console.log(correctPass); + return rep.status(400).json({ + error: 'username or password incorrect' + }); + } + + const userToken = { + username: user.username, + id: user.id, + role: user.role, // Include the user's role in the token payload + }; + + const token = jwt.sign(userToken, process.env.SECRET); + + rep.status(200).send({ + token, + username: user.username, + name: user.name, + role: user.role, // Optionally send the role back in the response + }); + } catch (error) { + console.error('Error during login:', error); + rep.status(500).json({ error: 'Internal server error during login' }); + } finally { + await prisma.$disconnect(); + } +}); + +module.exports = loginRouter; diff --git a/backend/utils/middleware.js b/backend/utils/middleware.js new file mode 100644 index 0000000..a9171ec --- /dev/null +++ b/backend/utils/middleware.js @@ -0,0 +1,105 @@ +const logger = require('./logger') +const jwt = require('jsonwebtoken') +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +const requestLogger = (request, response, next) => { + logger.info('Method:', request.method) + logger.info('Path: ', request.path) + logger.info('Body: ', request.body) + logger.info('---') + next() +} + +const unknownEndpoint = (request, response) => { + response.status(404).send({ error: 'unknown endpoint' }) +} + +const getTokenFrom = (request, response, next) => { + // console.log(request) + const auth = request.get('authorization') + if (auth && auth.startsWith('Bearer ')){ + request.token = auth.replace('Bearer ', '') + } + next() + } + +const identifyUser = async (req, res, next) => { + const token = req.token; + + if (!token) { + return res.status(401).json({ error: 'Unauthorized - No token provided' }); + } + + try { + const decoded = jwt.verify(token, process.env.SECRET); + const userId = decoded.id; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized - Invalid user' }); + } + + req.user = { + id: user.id, + username: user.username, + role: user.role, + }; + next(); + } catch (error) { + console.error('Error verifying token:', error); + return res.status(403).json({ error: 'Forbidden - Invalid token' }); + } finally { + await prisma.$disconnect(); + } +}; + +const isRep = (req, res, next) => { + if (req.user && req.user.role === 'REPRESENTATIVE') { + next(); + } else { + return res.status(403).json({ error: 'Forbidden - Representatives only.' }); + } +}; + +const isAdmin = (req, res, next) => { + if(req.user && req.user.role === 'ADMIN'){ + next(); + }else{ + return(res.status(403).json({ error: 'Forbidden - Admin Only.' })) + } +} + + +const errorHandler = (error, request, response, next) => { + logger.error(error) + + if (error.name === 'CastError') { + return response.status(400).send({ error: 'malformatted id' }) + } else if (error.name === 'ValidationError') { + return response.status(400).json({ error: error.message }) + }else if(error.name === 'PasswordErr'){ + return response.status(400).json({ error: error.message }) + }else if (error.name === 'MongoServerError' && error.code === 11000) { + return response.status(400).json({ error: 'Username Taken' }); + }else if (error.name === 'JsonWebTokenError'){ + return( response.status(401).json({ + error: 'inivalid token' + })) + } + + + next(error) +} +module.exports = { + requestLogger, + getTokenFrom, + unknownEndpoint, + identifyUser, + errorHandler, + isRep, + isAdmin +} From eef49fec6115e04afc5f840f09ab812e75306940 Mon Sep 17 00:00:00 2001 From: Kibreab Chanyalew Date: Thu, 24 Apr 2025 23:08:08 +0300 Subject: [PATCH 02/11] readme added --- backend/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 backend/README.md diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..024050c --- /dev/null +++ b/backend/README.md @@ -0,0 +1,7 @@ +To start the backend first make sure to have started docker with docker up dev-db -d or docker-compose up dev-db -d +Then make sure to npx prisma migrate dev --name init +and finally npx prisma generate && npm run db:seed + + +make sure to npm install after git clone +finally npm run dev for backend to start... From a15305988716f116d9c866a450f1cfd4831e9a14 Mon Sep 17 00:00:00 2001 From: YITBAREK ALEMU <160623517+jaeckult@users.noreply.github.com> Date: Sat, 26 Apr 2025 22:57:01 +0300 Subject: [PATCH 03/11] Added signup and reservation --- backend/app.js | 8 +- backend/controllers/classrooms.js | 27 ++ backend/{utils => controllers}/login.js | 0 backend/controllers/reservations.js | 352 ++++++++++++++ backend/controllers/signup.js | 58 +++ backend/package-lock.json | 597 +++++++++++++++++++++++- backend/package.json | 27 +- unispace/src/LoginPage.js | 103 +++- unispace/src/SchedulePage.js | 210 ++++++++- unispace/src/SignupPage.js | 128 ++++- 10 files changed, 1428 insertions(+), 82 deletions(-) create mode 100644 backend/controllers/classrooms.js rename backend/{utils => controllers}/login.js (100%) create mode 100644 backend/controllers/reservations.js create mode 100644 backend/controllers/signup.js diff --git a/backend/app.js b/backend/app.js index 2546ca5..81e3970 100644 --- a/backend/app.js +++ b/backend/app.js @@ -5,7 +5,9 @@ const cors = require("cors"); const middleweare = require('./utils/middleware') const userRouter = require('./controllers/users'); const reservationRouter = require('./controllers/reservations'); -const loginRouter = require("./utils/login"); +const loginRouter = require("./controllers/login"); +const signupRouter = require("./controllers/signup"); +const classroomRouter = require("./controllers/classrooms"); app.use(cors()); @@ -25,9 +27,11 @@ app.get('/', (req, rep) => { //routers -// app.use('/api/reservations', middleweare.identifyUser, reservationRouter) app.use('/api/users', userRouter) app.use('/api/login', loginRouter) +app.use('/api/signup', signupRouter) +app.use('/api/reservations', reservationRouter) +app.use('/api/classrooms', classroomRouter) app.use(middleweare.unknownEndpoint) app.use(middleweare.errorHandler) diff --git a/backend/controllers/classrooms.js b/backend/controllers/classrooms.js new file mode 100644 index 0000000..5749e58 --- /dev/null +++ b/backend/controllers/classrooms.js @@ -0,0 +1,27 @@ +const express = require('express'); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const { identifyUser } = require('../utils/middleware'); + +const classroomRouter = express.Router(); + +// GET /api/classrooms - Fetch all classrooms +classroomRouter.get('/', identifyUser, async (req, res) => { + try { + const classrooms = await prisma.classroom.findMany({ + select: { + name: true, + floorId: true, + buildingId:true + } + }); + res.status(200).json(classrooms); + } catch (error) { + console.error('Error fetching classrooms:', error); + res.status(500).json({ error: 'Internal server error while fetching classrooms' }); + } finally { + await prisma.$disconnect(); + } +}); + +module.exports = classroomRouter; \ No newline at end of file diff --git a/backend/utils/login.js b/backend/controllers/login.js similarity index 100% rename from backend/utils/login.js rename to backend/controllers/login.js diff --git a/backend/controllers/reservations.js b/backend/controllers/reservations.js new file mode 100644 index 0000000..d064b06 --- /dev/null +++ b/backend/controllers/reservations.js @@ -0,0 +1,352 @@ +const express = require('express'); +const reservationRouter = express.Router(); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const { identifyUser } = require('../utils/middleware'); + +reservationRouter.get('/', identifyUser, async (req, res) => { + const { userId, classroomId, startTime, endTime } = req.query; + const userRole = req.user.role; + const currentUserId = req.user.id; + + try { + const filters = {}; + if (userRole !== 'ADMIN' && userRole !== 'Student') { + filters.userId = currentUserId; // Representatives only see their own reservations + } else if (userId) { + const parsedUserId = parseInt(userId); + if (isNaN(parsedUserId)) { + return res.status(400).json({ error: 'Invalid userId format' }); + } + filters.userId = parsedUserId; // Admins can filter by userId + } + + + if (classroomId) { + const parsedClassroomId = parseInt(classroomId); + if (isNaN(parsedClassroomId)) { + return res.status(400).json({ error: 'Invalid classroomId format' }); + } + filters.classroomId = parsedClassroomId; + } + + + if (startTime) { + const start = new Date(startTime); + if (isNaN(start)) { + return res.status(400).json({ error: 'Invalid startTime format' }); + } + filters.startTime = { gte: start }; + } + + if (endTime) { + const end = new Date(endTime); + if (isNaN(end)) { + return res.status(400).json({ error: 'Invalid endTime format' }); + } + filters.endTime = { lte: end }; + } + + const reservations = await prisma.reservation.findMany({ + where: filters, + orderBy: { startTime: 'asc' }, + include: { + user: { select: { username: true, email: true } }, + classroom: { select: { name: true} } + } + }); + + res.status(200).json(reservations); + } catch (error) { + console.error('Error fetching reservations:', error); + res.status(500).json({ error: 'Internal server error while fetching reservations' }); + } finally { + await prisma.$disconnect(); + } +}); + +reservationRouter.post('/', identifyUser, async (req, res) => { + const { classroomId, startTime, endTime } = req.body; + const userId = req.user.id; + if (!(classroomId && startTime && endTime)) { + return res.status(400).json({ + error: 'Classroom ID, start time, and end time are required' + }); + } + + try { + const start = new Date(startTime); + const end = new Date(endTime); + const now = new Date(); + + if (isNaN(start) || isNaN(end)) { + return res.status(400).json({ + error: 'Invalid start or end time format' + }); + } + + if (start >= end) { + return res.status(400).json({ + error: 'Start time must be before end time' + }); + } + + if (start < now) { + return res.status(400).json({ + error: 'Start time must be in the future' + }); + } + + + const user = await prisma.user.findUnique({ + where: { id: userId } + }); + + if (!user) { + return res.status(404).json({ + error: 'User not found' + }); + } + + const classroom = await prisma.classroom.findUnique({ + where: { id: classroomId } + }); + + if (!classroom) { + return res.status(404).json({ + error: 'Classroom not found' + }); + } + + const conflictingReservation = await prisma.reservation.findFirst({ + where: { + classroomId, + OR: [ + { + AND: [ + { startTime: { lte: start } }, + { endTime: { gt: start } } + ] + }, + { + AND: [ + { startTime: { lt: end } }, + { endTime: { gte: end } } + ] + }, + { + AND: [ + { startTime: { gte: start } }, + { endTime: { lte: end } } + ] + } + ] + } + }); + + if (conflictingReservation) { + return res.status(400).json({ + error: 'Classroom is already reserved for the requested time slot' + }); + } + + + const newReservation = await prisma.reservation.create({ + data: { + userId, + classroomId, + startTime: start, + endTime: end + } + }); + + res.status(201).json({ + id: newReservation.id, + userId: newReservation.userId, + classroomId: newReservation.classroomId, + startTime: newReservation.startTime, + endTime: newReservation.endTime, + createdAt: newReservation.createdAt + }); + } catch (error) { + console.error('Error creating reservation:', error); + res.status(500).json({ error: 'Internal server error during reservation creation' }); + } finally { + await prisma.$disconnect(); + } +}); + +reservationRouter.delete('/:id', identifyUser, async (req, res) => { + const { id } = req.params; + const userId = req.user.id; + const userRole = req.user.role; + + + const reservationId = parseInt(id); + if (isNaN(reservationId)) { + return res.status(400).json({ error: 'Invalid reservation ID format' }); + } + + try { + const reservation = await prisma.reservation.findUnique({ + where: { id: reservationId }, + include: { + user: { select: { username: true, email: true } }, + classroom: { select: { name: true} } + } + }); + + if (!reservation) { + return res.status(404).json({ error: 'Reservation not found' }); + } + + + if (userRole !== 'ADMIN' && reservation.userId !== userId) { + return res.status(403).json({ error: 'Unauthorized to delete this reservation' }); + } + + + await prisma.reservation.delete({ + where: { id: reservationId } + }); + + res.status(200).json({ + message: 'Reservation deleted successfully', + reservation + }); + } catch (error) { + console.error('Error deleting reservation:', error); + res.status(500).json({ error: 'Internal server error while deleting reservation' }); + } finally { + await prisma.$disconnect(); + } +}); + +reservationRouter.patch('/:id', identifyUser, async (req, res) => { + const { id } = req.params; + const { classroomId, startTime, endTime } = req.body; + const userId = req.user.id; + const userRole = req.user.role; + + + const reservationId = parseInt(id); + if (isNaN(reservationId)) { + return res.status(400).json({ error: 'Invalid reservation ID format' }); + } + + + if (!classroomId && !startTime && !endTime) { + return res.status(400).json({ error: 'At least one field (classroomId, startTime, endTime) must be provided' }); + } + + try { + + const reservation = await prisma.reservation.findUnique({ + where: { id: reservationId }, + include: { + user: { select: { username: true, email: true } }, + classroom: { select: { name: true} } + } + }); + + if (!reservation) { + return res.status(404).json({ error: 'Reservation not found' }); + } + + + if (userRole !== 'ADMIN' && reservation.userId !== userId) { + return res.status(403).json({ error: 'Unauthorized to update this reservation' }); + } + + + const updateData = {}; + + if (classroomId) { + const parsedClassroomId = parseInt(classroomId); + if (isNaN(parsedClassroomId)) { + return res.status(400).json({ error: 'Invalid classroomId format' }); + } + const classroom = await prisma.classroom.findUnique({ + where: { id: parsedClassroomId } + }); + if (!classroom) { + return res.status(404).json({ error: 'Classroom not found' }); + } + updateData.classroomId = parsedClassroomId; + } + let newStartTime = startTime ? new Date(startTime) : reservation.startTime; + let newEndTime = endTime ? new Date(endTime) : reservation.endTime; + + if (startTime && isNaN(newStartTime)) { + return res.status(400).json({ error: 'Invalid startTime format' }); + } + if (endTime && isNaN(newEndTime)) { + return res.status(400).json({ error: 'Invalid endTime format' }); + } + + if ((startTime || endTime) && newStartTime >= newEndTime) { + return res.status(400).json({ error: 'Start time must be before end time' }); + } + + if ((startTime || endTime) && newStartTime < new Date()) { + return res.status(400).json({ error: 'Start time must be in the future' }); + } + + if (startTime || endTime || classroomId) { + const checkClassroomId = classroomId || reservation.classroomId; + const conflictingReservation = await prisma.reservation.findFirst({ + where: { + classroomId: checkClassroomId, + id: { not: reservationId }, // Exclude the current reservation + OR: [ + { + AND: [ + { startTime: { lte: newStartTime } }, + { endTime: { gt: newStartTime } } + ] + }, + { + AND: [ + { startTime: { lt: newEndTime } }, + { endTime: { gte: newEndTime } } + ] + }, + { + AND: [ + { startTime: { gte: newStartTime } }, + { endTime: { lte: newEndTime } } + ] + } + ] + } + }); + + if (conflictingReservation) { + return res.status(400).json({ error: 'Updated time slot conflicts with an existing reservation' }); + } + + if (startTime) updateData.startTime = newStartTime; + if (endTime) updateData.endTime = newEndTime; + } + + const updatedReservation = await prisma.reservation.update({ + where: { id: reservationId }, + data: updateData, + include: { + user: { select: { username: true, email: true } }, + classroom: { select: { name: true, capacity: true } } + } + }); + + res.status(200).json({ + message: 'Reservation updated successfully', + reservation: updatedReservation + }); + } catch (error) { + console.error('Error updating reservation:', error); + res.status(500).json({ error: 'Internal server error while updating reservation' }); + } finally { + await prisma.$disconnect(); + } + }); +module.exports = reservationRouter; \ No newline at end of file diff --git a/backend/controllers/signup.js b/backend/controllers/signup.js new file mode 100644 index 0000000..b602f2d --- /dev/null +++ b/backend/controllers/signup.js @@ -0,0 +1,58 @@ +const express = require('express'); +const signupRouter = express.Router(); +const bcrypt = require('bcrypt'); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +signupRouter.post('/', async (req, res) => { + const { username, email, password, role } = req.body; + + if (!(username && email && password)) { + return res.status(400).json({ + error: 'Username, email, and password are required' + }); + } + + try { + const existingUser = await prisma.user.findFirst({ + where: { + OR: [ + { username }, + { email } + ] + } + }); + + if (existingUser) { + return res.status(400).json({ + error: existingUser.username === username ? 'Username already taken' : 'Email already taken' + }); + } + + const saltRounds = 10; + const passwordHash = await bcrypt.hash(password, saltRounds); + + + const newUser = await prisma.user.create({ + data: { + username, + email, + password: passwordHash, + role: role || 'STUDENT' // Default to 'STUDENT' if role is not provided + } + }); + + res.status(201).json({ + username: newUser.username, + email: newUser.email, + role: newUser.role + }); + } catch (error) { + console.error('Error during signup:', error); + res.status(500).json({ error: 'Internal server error during signup' }); + } finally { + await prisma.$disconnect(); + } +}); + +module.exports = signupRouter; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 9432630..406ba15 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,7 +18,433 @@ "jsonwebtoken": "^9.0.2" }, "devDependencies": { - "nodemon": "^3.1.4" + "nodemon": "^3.1.4", + "prisma": "^6.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -63,6 +489,67 @@ } } }, + "node_modules/@prisma/config": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.6.0.tgz", + "integrity": "sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "esbuild": ">=0.12 <1", + "esbuild-register": "3.6.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.6.0.tgz", + "integrity": "sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.6.0.tgz", + "integrity": "sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.6.0", + "@prisma/engines-version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", + "@prisma/fetch-engine": "6.6.0", + "@prisma/get-platform": "6.6.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a.tgz", + "integrity": "sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.6.0.tgz", + "integrity": "sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.6.0", + "@prisma/engines-version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", + "@prisma/get-platform": "6.6.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.6.0.tgz", + "integrity": "sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.6.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -523,6 +1010,85 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-register/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild-register/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1456,6 +2022,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prisma": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.6.0.tgz", + "integrity": "sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.6.0", + "@prisma/engines": "6.6.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/backend/package.json b/backend/package.json index 71f6319..2eeb539 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,26 +2,27 @@ "name": "unispace-backend", "version": "1.0.0", "description": "Backend for the Unispace classroom reservation system", - "main": "index.js", + "main": "index.js", "scripts": { - "start": "node index.js", - "db:seed": "node seed.js", - "dev": "nodemon index.js", + "start": "node index.js", + "db:seed": "node seed.js", + "dev": "nodemon index.js", "prisma:migrate": "npx prisma migrate dev", "prisma:generate": "npx prisma generate" }, - "author": "Your Name", + "author": "Your Name", "license": "ISC", "dependencies": { - "@prisma/client": "^6.6.0", - "bcrypt": "^5.1.1", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-validator": "^7.0.1", - "jsonwebtoken": "^9.0.2" + "@prisma/client": "^6.6.0", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-validator": "^7.0.1", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { - "nodemon": "^3.1.4" + "nodemon": "^3.1.4", + "prisma": "^6.6.0" } } diff --git a/unispace/src/LoginPage.js b/unispace/src/LoginPage.js index 0b6ddc2..d86ca85 100644 --- a/unispace/src/LoginPage.js +++ b/unispace/src/LoginPage.js @@ -1,36 +1,97 @@ -import React from 'react'; -//import { useNavigate } from 'react-router-dom'; -import { Link } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; import './LoginPage.css'; -import './styles.css'; // Import index.css +import './styles.css'; import Bubbles from './Bubbles.js'; function LoginPage() { - //const navigate = useNavigate(); + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + username: '', + password: '' + }); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + // Handle input changes + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + // Handle form submission + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setSuccess(''); + + try { + const response = await fetch('http://localhost:9000/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Login failed'); + } + + // Store token and user data in localStorage + localStorage.setItem('token', data.token); + localStorage.setItem('username', data.username); + localStorage.setItem('role', data.role); + + // Success: Show message and navigate to home + setSuccess('Login successful! Redirecting to home...'); + setTimeout(() => { + navigate('./Home'); + }, 2000); // Redirect after 2 seconds + } catch (err) { + setError(err.message || 'An error occurred during login'); + } + }; + return ( -
- {/* Use the Bubbles component here */} +
+

UNISPACE

-
-
-
- - - - - +
+
+ + + + + + + {error &&

{error}

} + {success &&

{success}

}

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

); - } - - -export default LoginPage; +export default LoginPage; \ No newline at end of file diff --git a/unispace/src/SchedulePage.js b/unispace/src/SchedulePage.js index 6301c23..8845057 100644 --- a/unispace/src/SchedulePage.js +++ b/unispace/src/SchedulePage.js @@ -1,51 +1,213 @@ -import React from "react"; -import './index.css'; // Styles for this page -import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import './schedule.css'; +import './styles.css'; function SchedulePage() { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + classroomId: '', + startTime: '', + endTime: '' + }); + const [reservations, setReservations] = useState([]); + const [classrooms, setClassrooms] = useState([]); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + // Fetch classrooms and reservations on mount + useEffect(() => { + const fetchClassrooms = async () => { + try { + const response = await fetch('http://localhost:9000/api/classrooms', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const data = await response.json(); + if (response.ok) { + setClassrooms(data); + } else { + throw new Error(data.error || 'Failed to fetch classrooms'); + } + } catch (err) { + setError(err.message); + } + }; + + const fetchReservations = async () => { + try { + const response = await fetch('http://localhost:9000/api/reservations', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const data = await response.json(); + if (response.ok) { + setReservations(data); + } else { + throw new Error(data.error || 'Failed to fetch reservations'); + } + } catch (err) { + setError(err.message); + } + }; + + fetchClassrooms(); + fetchReservations(); + }, []); + + // Handle input changes + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + // Handle form submission + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setSuccess(''); + + // Combine date and time for startTime and endTime + const [startDate, startTime] = formData.startTime.split('T'); + const [endDate, endTime] = formData.endTime.split('T'); + const formattedStartTime = startDate && startTime ? `${startDate}T${startTime}:00Z` : ''; + const formattedEndTime = endDate && endTime ? `${endDate}T${endTime}:00Z` : ''; + + if (!formattedStartTime || !formattedEndTime) { + setError('Please provide both start and end times'); + return; + } + + const payload = { + classroomId: parseInt(formData.classroomId), + startTime: formattedStartTime, + endTime: formattedEndTime + }; + + try { + const response = await fetch('http://localhost:9000/api/reservations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify(payload) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Reservation failed'); + } + + // Success: Show message and refresh reservations + setSuccess('Reservation successful!'); + setFormData({ classroomId: '', startTime: '', endTime: '' }); + // Refresh reservations + const reservationsResponse = await fetch('http://localhost:9000/api/reservations', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + const reservationsData = await reservationsResponse.json(); + if (reservationsResponse.ok) { + setReservations(reservationsData); + } + } catch (err) { + setError(err.message || 'An error occurred during reservation'); + } + }; + return (
+
    +
  • + Home +
  • +
  • + Class Status +
  • +
  • + Class Queue +
  • +
+
-
-
-

E-Block 002

+
+
+

Classroom Reservations

Class Queue

-

Class is reserved from 9:00am to 11:00am

+ {reservations.length > 0 ? ( +
    + {reservations.map((reservation) => ( +
  • + {reservation.classroom.name}: {new Date(reservation.startTime).toLocaleString()} to {new Date(reservation.endTime).toLocaleString()} +
  • + ))} +
+ ) : ( +

No reservations found.

+ )}

Reserve Class

-
+ + + {error &&

{error}

} + {success &&

{success}

}
-
+
); } -export default SchedulePage; +export default SchedulePage; \ No newline at end of file diff --git a/unispace/src/SignupPage.js b/unispace/src/SignupPage.js index c401dca..c630a3f 100644 --- a/unispace/src/SignupPage.js +++ b/unispace/src/SignupPage.js @@ -1,29 +1,116 @@ -import React from 'react'; -//import { useNavigate } from 'react-router-dom'; -import { Link } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; import './SignupPage.css'; -import './styles.css'; // Import index.css +import './styles.css'; import Bubbles from './Bubbles.js'; function SignupPage() { - //const navigate = useNavigate(); + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + username: '', + email: '', + name: '', + password: '', + role: 'STUDENT' // Default role + }); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + // Handle input changes + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + // Handle form submission + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + setSuccess(''); + + try { + const response = await fetch('http://localhost:9000/api/signup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Signup failed'); + } + + // Success: Show message and navigate to login + setSuccess('Signup successful! Redirecting to login...'); + setTimeout(() => { + navigate('/'); + }, 2000); // Redirect after 2 seconds + } catch (err) { + setError(err.message || 'An error occurred during signup'); + } + }; + return ( -
- {/* Use the Bubbles component here */} +
+

UNISPACE

-
-
-
- - - - - - - +
+
+ + + + + + + + + + + + + {error &&

{error}

} + {success &&

{success}

}

- Already have an account?{' '} - Login + Already have an account? Login

@@ -32,5 +119,4 @@ function SignupPage() { ); } - -export default SignupPage; +export default SignupPage; \ No newline at end of file From e19a3c2f94350395c929234015207ad7b1604b7d Mon Sep 17 00:00:00 2001 From: Kibreab Chanyalew Date: Mon, 28 Apr 2025 09:25:47 +0300 Subject: [PATCH 04/11] enforcment change --- backend/controllers/signup.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/controllers/signup.js b/backend/controllers/signup.js index b602f2d..e033862 100644 --- a/backend/controllers/signup.js +++ b/backend/controllers/signup.js @@ -3,10 +3,11 @@ const signupRouter = express.Router(); const bcrypt = require('bcrypt'); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); +const { identifyUser } = require('../utils/middleware') -signupRouter.post('/', async (req, res) => { +signupRouter.post('/', identifyUser, async (req, res) => { const { username, email, password, role } = req.body; - + const userRole = req.user.role; if (!(username && email && password)) { return res.status(400).json({ error: 'Username, email, and password are required' @@ -32,6 +33,9 @@ signupRouter.post('/', async (req, res) => { const saltRounds = 10; const passwordHash = await bcrypt.hash(password, saltRounds); + if (role === "REPRESENTATIVE" && userRole !== "ADMIN"){ + return res.status(403).json({ error:"Can't make a representative without admin privellage"}); + } const newUser = await prisma.user.create({ data: { @@ -55,4 +59,4 @@ signupRouter.post('/', async (req, res) => { } }); -module.exports = signupRouter; \ No newline at end of file +module.exports = signupRouter; From 9adcc280a71092c58b066842c26933ffc23ff541 Mon Sep 17 00:00:00 2001 From: Kibreab Chanyalew Date: Mon, 28 Apr 2025 09:40:00 +0300 Subject: [PATCH 05/11] endpoint to add new classrooms --- backend/controllers/classrooms.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/controllers/classrooms.js b/backend/controllers/classrooms.js index 5749e58..230ffa3 100644 --- a/backend/controllers/classrooms.js +++ b/backend/controllers/classrooms.js @@ -1,7 +1,7 @@ const express = require('express'); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); -const { identifyUser } = require('../utils/middleware'); +const { identifyUser, isAdmin } = require('../utils/middleware'); const classroomRouter = express.Router(); @@ -24,4 +24,28 @@ classroomRouter.get('/', identifyUser, async (req, res) => { } }); -module.exports = classroomRouter; \ No newline at end of file +classroomRouter.post('/', identifyUser, isAdmin, async(req, res) => { + const {floorId, buildingId, name } = req.body; + + if (!floorId || !buildingId || !name ) { + return res.status(400).json({error: "requires all of the following, floor ID, Building ID, Name of classroom"}) + } + try { + const newClass = await prisma.classroom.create({ + data: { + floorId: parseInt(floorId), + buildingId: parseInt(buildingId), + name, + }, + }); + + res.status(201).json(newClass); + }catch(e){ + consol.log("Error making class:, ", error); + res.status(500).json({ error: 'Internal server error while creating classroom' }); + }finally{ + await prisma.$disconnect(); + } +}); + +module.exports = classroomRouter; From c4e25c3ed12c69a7311cc4c1347253e4962faa4e Mon Sep 17 00:00:00 2001 From: Kibreab Chanyalew Date: Mon, 28 Apr 2025 10:33:11 +0300 Subject: [PATCH 06/11] add reps via admin only endpooint havent tested it but should work --- backend/controllers/signup.js | 7 +---- backend/controllers/users.js | 50 ++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/backend/controllers/signup.js b/backend/controllers/signup.js index e033862..9f605a2 100644 --- a/backend/controllers/signup.js +++ b/backend/controllers/signup.js @@ -5,9 +5,8 @@ const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const { identifyUser } = require('../utils/middleware') -signupRouter.post('/', identifyUser, async (req, res) => { +signupRouter.post('/', async (req, res) => { const { username, email, password, role } = req.body; - const userRole = req.user.role; if (!(username && email && password)) { return res.status(400).json({ error: 'Username, email, and password are required' @@ -33,10 +32,6 @@ signupRouter.post('/', identifyUser, async (req, res) => { const saltRounds = 10; const passwordHash = await bcrypt.hash(password, saltRounds); - if (role === "REPRESENTATIVE" && userRole !== "ADMIN"){ - return res.status(403).json({ error:"Can't make a representative without admin privellage"}); - } - const newUser = await prisma.user.create({ data: { username, diff --git a/backend/controllers/users.js b/backend/controllers/users.js index cb1d649..4e300c1 100644 --- a/backend/controllers/users.js +++ b/backend/controllers/users.js @@ -35,13 +35,57 @@ userRouter.get('/:id', identifyUser, async(req,res) => { console.error(error); res.status(500).json({ error: 'Could not retrieve user' }); } -}) +}); // might implement a way to add new users as an admin maybe? userRouter.post('/', identifyUser, isAdmin, async(req, res) => { - // TODO -}) + const { username, email, password, role } = req.body; + + if (!(username && email && password)) { + return res.status(400).json({ + error: 'Username, email, and password are required' + }); + } + + try { + const existingUser = await prisma.user.findFirst({ + where: { + OR: [ + { username }, + { email } + ] + } + }); + if (existingUser) { + return res.status(400).json({ error: existingUser.username === username ? 'Username already taken' : 'Email already taken' }); + } + + const saltRounds = 10; + const passwordHash = await bcrypt.hash(password, saltRounds); + + const newUser = await prisma.user.create({ + data: { + username, + email, + password: passwordHash, + role: role + } + }); + + res.status(201).json({ + username: newUser.username, + email: newUser.email, + role: newUser.role + }); + + }catch (error){ + console.error('Error during signup:', error); + res.status(500).json({ error: 'Internal server error during signup' }); + }finally { + await prisma.$disconnect(); + } +}); module.exports = userRouter; From 01b4ca8b1f6dfe31862e35d28ff66b76fb8ab2b0 Mon Sep 17 00:00:00 2001 From: YITBAREK ALEMU <160623517+jaeckult@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:34:50 +0300 Subject: [PATCH 07/11] occupancy-notification-conversion_of_reservation_to_occupancy --- backend/app.js | 29 +- backend/controllers/classrooms.js | 7 +- backend/controllers/notification.js | 143 ++++++++++ backend/controllers/occupancy.js | 237 ++++++++++++++++ backend/controllers/reservations.js | 27 +- backend/controllers/users.js | 46 ++- backend/package-lock.json | 268 +++++++++++++++++- backend/package.json | 7 +- .../20250424152853_init/migration.sql | 155 ++++++---- .../migration.sql | 5 + backend/prisma/schema.prisma | 104 ++++--- backend/scripts/convertReservation.js | 90 ++++++ backend/seed.js | 226 ++++++++------- backend/utils/middleware.js | 38 +-- 14 files changed, 1154 insertions(+), 228 deletions(-) create mode 100644 backend/controllers/notification.js create mode 100644 backend/controllers/occupancy.js create mode 100644 backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql create mode 100644 backend/scripts/convertReservation.js diff --git a/backend/app.js b/backend/app.js index 81e3970..d7d1a22 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,4 +1,5 @@ const express = require("express"); +const http = require("http"); const { info, error } = require("./utils/logger"); const app = express(); const cors = require("cors"); @@ -8,12 +9,25 @@ const reservationRouter = require('./controllers/reservations'); const loginRouter = require("./controllers/login"); const signupRouter = require("./controllers/signup"); const classroomRouter = require("./controllers/classrooms"); +const occupancyRouter = require("./controllers/occupancy"); +const { notificationRouter } = require("./controllers/notification"); + + +const {Server} = require("socket.io"); +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: 'http://localhost:9000', //Should be updated + methods: ['GET', 'POST'], + }, +}); app.use(cors()); app.use(express.json()); app.use(middleweare.requestLogger) app.use(middleweare.getTokenFrom) +app.set('io', io); @@ -25,13 +39,24 @@ app.get('/', (req, rep) => { //routers - - app.use('/api/users', userRouter) app.use('/api/login', loginRouter) app.use('/api/signup', signupRouter) app.use('/api/reservations', reservationRouter) app.use('/api/classrooms', classroomRouter) +app.use('/api/occupancy', occupancyRouter) +app.use('/api/notifications', notificationRouter) + + +io.on('connection', (socket) => { + console.log('User connected:', socket.id); + socket.on('joinUserRoom', (userId) => { + socket.join(`user:${userId}`); + console.log(`User ${userId} joined room user:${userId}`); + }); + socket.on('disconnect', () => console.log('User disconnected:', socket.id)); +}); + app.use(middleweare.unknownEndpoint) app.use(middleweare.errorHandler) diff --git a/backend/controllers/classrooms.js b/backend/controllers/classrooms.js index 230ffa3..7060b5f 100644 --- a/backend/controllers/classrooms.js +++ b/backend/controllers/classrooms.js @@ -1,11 +1,11 @@ const express = require('express'); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); -const { identifyUser, isAdmin } = require('../utils/middleware'); +const { identifyUser, rbacMiddleware } = require('../utils/middleware'); const classroomRouter = express.Router(); -// GET /api/classrooms - Fetch all classrooms +// GET /api/classrooms - Fetch all classrooms -FOR ALL ROLES classroomRouter.get('/', identifyUser, async (req, res) => { try { const classrooms = await prisma.classroom.findMany({ @@ -24,7 +24,8 @@ classroomRouter.get('/', identifyUser, async (req, res) => { } }); -classroomRouter.post('/', identifyUser, isAdmin, async(req, res) => { +//create classrooms +classroomRouter.post('/', identifyUser, rbacMiddleware(['ADMIN']), async(req, res) => { const {floorId, buildingId, name } = req.body; if (!floorId || !buildingId || !name ) { diff --git a/backend/controllers/notification.js b/backend/controllers/notification.js new file mode 100644 index 0000000..5c8bd57 --- /dev/null +++ b/backend/controllers/notification.js @@ -0,0 +1,143 @@ +const express = require('express'); +const notificationRouter = express.Router(); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const { getTokenFrom, identifyUser, rbacMiddleware } = require('../utils/middleware'); + +const createNotification = async (userId, message, io) => { + try { + const notification = await prisma.notification.create({ + data: { + userId, + message, + isRead: false, + }, + }); + io.to(`user:${userId}`).emit('newNotification', notification); + console.log(`Created notification for user ${userId}: ${message}`); + return notification; + } catch (error) { + console.error('Error creating notification:', error); + throw error; + } +}; + +notificationRouter.post('/', getTokenFrom, identifyUser, rbacMiddleware(['ADMIN', 'TEACHER']), async (req, res) => { + const { userId, message } = req.body; + + if (!userId || isNaN(parseInt(userId))) { + return res.status(400).json({ error: 'Valid userId is required' }); + } + if (!message || typeof message !== 'string' || message.trim() === '') { + return res.status(400).json({ error: 'Valid message is required' }); + } + + try { + const user = await prisma.user.findUnique({ + where: { id: parseInt(userId) }, + }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const io = req.app.get('io'); + const notification = await createNotification(parseInt(userId), message, io); + res.status(201).json(notification); + } catch (error) { + console.error('Error creating notification:', error); + res.status(500).json({ error: 'Internal server error during notification creation' }); + } finally { + await prisma.$disconnect(); + } + }); + +// GET /api/notifications - Fetch notifications +notificationRouter.get('/', getTokenFrom, identifyUser, rbacMiddleware(['ADMIN', 'STUDENT', 'REPRESENTATIVE']), async (req, res) => { + const userId = req.user.id; + const role = req.user.role; + + try { + let notifications; + if (role === 'ADMIN') { + notifications = await prisma.notification.findMany({ + include: { user: { select: { id: true, username: true } } }, + orderBy: { createdAt: 'desc' }, + }); + } else { + notifications = await prisma.notification.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + }); + } + res.status(200).json(notifications); + } catch (error) { + console.error('Error fetching notifications:', error); + res.status(500).json({ error: 'Internal server error during notification fetch' }); + } finally { + await prisma.$disconnect(); + } +}); + +// PATCH /api/notifications/:id/read - Mark notification as read +notificationRouter.patch('/:id/read', getTokenFrom, identifyUser, rbacMiddleware(['ADMIN', 'STUDENT', 'REPRESENTATIVE']), async (req, res) => { + const { id } = req.params; + const userId = req.user.id; + const role = req.user.role; + + try { + const notification = await prisma.notification.findUnique({ + where: { id: parseInt(id) }, + }); + if (!notification) { + return res.status(404).json({ error: 'Notification not found' }); + } + if (notification.userId !== userId && role !== 'ADMIN') { + return res.status(403).json({ error: 'Forbidden - Unauthorized to modify this notification' }); + } + + const updatedNotification = await prisma.notification.update({ + where: { id: parseInt(id) }, + data: { isRead: true }, + }); + res.status(200).json(updatedNotification); + } catch (error) { + console.error('Error marking notification as read:', error); + res.status(500).json({ error: 'Internal server error during notification update' }); + } finally { + await prisma.$disconnect(); + } +}); + +// DELETE /api/notifications/:id - Delete notification +notificationRouter.delete('/:id', getTokenFrom, identifyUser, rbacMiddleware(['ADMIN', 'STUDENT', 'REPRESENTATIVE']), async (req, res) => { + const { id } = req.params; + const userId = req.user.id; + const role = req.user.role; + + try { + const notification = await prisma.notification.findUnique({ + where: { id: parseInt(id) }, + }); + if (!notification) { + return res.status(404).json({ error: 'Notification not found' }); + } + if (notification.userId !== userId && role !== 'ADMIN') { + return res.status(403).json({ error: 'Forbidden - Unauthorized to delete this notification' }); + } + + await prisma.notification.delete({ + where: { id: parseInt(id) }, + }); + res.status(200).json({ message: 'Notification deleted' }); + } catch (error) { + console.error('Error deleting notification:', error); + res.status(500).json({ error: 'Internal server error during notification deletion' }); + } finally { + await prisma.$disconnect(); + } +}); + +module.exports = { + notificationRouter, + createNotification, +}; \ No newline at end of file diff --git a/backend/controllers/occupancy.js b/backend/controllers/occupancy.js new file mode 100644 index 0000000..1811d1a --- /dev/null +++ b/backend/controllers/occupancy.js @@ -0,0 +1,237 @@ +const express = require('express'); +const occupancyRouter = express.Router(); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const { identifyUser, rbacMiddleware } = require('../utils/middleware'); + +//All users can retrieve this endpoint +occupancyRouter.get('/', identifyUser, async (req, res) => { + const { userId } = req.query; + const authUserId = req.user.id; + const role = req.user.role; + + try { + const occupancies = await prisma.occupancy.findMany({ + include: { classroom: true, user: { select: { id: true, username: true } } }, + }); + res.status(200).send(occupancies); + } catch (error) { + console.error('Error fetching occupancies:', error); + res.status(500).json({ error: 'Internal server error during occupancy fetch' }); + } finally { + await prisma.$disconnect(); + } +}); + + +//All users can retrieve this endpoint +occupancyRouter.get('/classroom/:classroomId', identifyUser, async (req, res) => { + const { classroomId } = req.params; + + if (!classroomId || isNaN(parseInt(classroomId))) { + return res.status(400).json({ error: 'Valid classroomId is required' }); + } + + try { + const classroom = await prisma.classroom.findUnique({ + where: { id: parseInt(classroomId) }, + }); + if (!classroom) { + return res.status(404).json({ error: 'Classroom not found' }); + } + + const occupancies = await prisma.occupancy.findMany({ + where: { classroomId: parseInt(classroomId) }, + include: { classroom: true, user: { select: { id: true, username: true } } }, + }); + res.status(200).send(occupancies); + } catch (error) { + console.error('Error fetching occupancies by classroom:', error); + res.status(500).json({ error: 'Internal server error during occupancy fetch' }); + } finally { + await prisma.$disconnect(); + } +}); + + +//ADMIN, TEACHER, REPRESENTATIVE roles only +occupancyRouter.post('/', identifyUser, rbacMiddleware(['ADMIN', 'TEACHER', 'REPRESENTATIVE']), async (req, res) => { + const { classroomId, startTime, endTime, status } = req.body; + const userId = req.user.id; + + if (!classroomId || isNaN(parseInt(classroomId))) { + return res.status(400).json({ error: 'Valid classroomId is required' }); + } + if (!(startTime && endTime)) { + return res.status(400).json({ error: 'Start time and end time are required' }); + } + + try { + const start = new Date(startTime); + const end = new Date(endTime); + const now = new Date(); + + if (isNaN(start) || isNaN(end)) { + return res.status(400).json({ error: 'Invalid start or end time format' }); + } + if (start >= end) { + return res.status(400).json({ error: 'Start time must be before end time' }); + } + if (start < now) { + return res.status(400).json({ error: 'Start time must be in the future' }); + } + + const classroom = await prisma.classroom.findUnique({ + where: { id: parseInt(classroomId) }, + }); + if (!classroom) { + return res.status(404).json({ error: 'Classroom not found' }); + } + + const conflictingOccupancy = await prisma.occupancy.findFirst({ + where: { + classroomId: parseInt(classroomId), + OR: [ + { AND: [{ startTime: { lte: start } }, { endTime: { gt: start } }] }, + { AND: [{ startTime: { lt: end } }, { endTime: { gte: end } }] }, + { AND: [{ startTime: { gte: start } }, { endTime: { lte: end } }] }, + ], + }, + }); + if (conflictingOccupancy) { + return res.status(400).json({ error: 'Classroom is already occupied for the requested time slot' }); + } + + const newOccupancy = await prisma.occupancy.create({ + data: { + classroomId: parseInt(classroomId), + userId, + startTime: start, + endTime: end, + status: status || 'occupied', + }, + include: { classroom: true, user: { select: { id: true, username: true } } }, + }); + + res.status(200).send(newOccupancy); + } catch (error) { + console.error('Error creating occupancy:', error); + res.status(500).json({ error: 'Internal server error during occupancy creation' }); + } finally { + await prisma.$disconnect(); + } +}); + + + +//ADMIN, TEACHER, REPRESENTATIVE role only +occupancyRouter.patch('/:id', identifyUser, rbacMiddleware(['ADMIN', 'TEACHER', 'REPRESENTATIVE']), async (req, res) => { + const { id } = req.params; + const { classroomId, startTime, endTime, status } = req.body; + const userId = req.user.id; + + try { + const occupancy = await prisma.occupancy.findUnique({ + where: { id: parseInt(id) }, + }); + if (!occupancy) { + return res.status(404).json({ error: 'Occupancy not found' }); + } + if (occupancy.userId !== userId && req.user.role !== 'ADMIN') { + return res.status(403).json({ error: 'Forbidden - Unauthorized to modify this occupancy' }); + } + + const updateData = {}; + if (classroomId) { + if (isNaN(parseInt(classroomId))) { + return res.status(400).json({ error: 'Valid classroomId is required' }); + } + const classroom = await prisma.classroom.findUnique({ + where: { id: parseInt(classroomId) }, + }); + if (!classroom) { + return res.status(404).json({ error: 'Classroom not found' }); + } + updateData.classroomId = parseInt(classroomId); + } + if (startTime) updateData.startTime = new Date(startTime); + if (endTime) updateData.endTime = new Date(endTime); + if (status) updateData.status = status; + + if (startTime || endTime) { + const start = startTime ? new Date(startTime) : occupancy.startTime; + const end = endTime ? new Date(endTime) : occupancy.endTime; + const now = new Date(); + + if (isNaN(start) || isNaN(end)) { + return res.status(400).json({ error: 'Invalid start or end time format' }); + } + if (start >= end) { + return res.status(400).json({ error: 'Start time must be before end time' }); + } + if (start < now) { + return res.status(400).json({ error: 'Start time must be in the future' }); + } + + const targetClassroomId = classroomId ? parseInt(classroomId) : occupancy.classroomId; + const conflictingOccupancy = await prisma.occupancy.findFirst({ + where: { + classroomId: targetClassroomId, + id: { not: parseInt(id) }, + OR: [ + { AND: [{ startTime: { lte: start } }, { endTime: { gt: start } }] }, + { AND: [{ startTime: { lt: end } }, { endTime: { gte: end } }] }, + { AND: [{ startTime: { gte: start } }, { endTime: { lte: end } }] }, + ], + }, + }); + if (conflictingOccupancy) { + return res.status(400).json({ error: 'Classroom is already occupied for the requested time slot' }); + } + } + + const updatedOccupancy = await prisma.occupancy.update({ + where: { id: parseInt(id) }, + data: updateData, + include: { classroom: true, user: { select: { id: true, username: true } } }, + }); + + res.status(200).send(updatedOccupancy); + } catch (error) { + console.error('Error updating occupancy:', error); + res.status(500).json({ error: 'Internal server error during occupancy update' }); + } finally { + await prisma.$disconnect(); + } +}); + + +//ADMIN, TEACHER, REPRESENTATIVE role only +occupancyRouter.delete('/:id', identifyUser, rbacMiddleware(['ADMIN', 'TEACHER', 'REPRESENTATIVE']), async (req, res) => { + const { id } = req.params; + const userId = req.user.id; + + try { + const occupancy = await prisma.occupancy.findUnique({ + where: { id: parseInt(id) }, + }); + if (!occupancy) { + return res.status(404).json({ error: 'Occupancy not found' }); + } + if (occupancy.userId !== userId && req.user.role !== 'ADMIN') { + return res.status(403).json({ error: 'Forbidden - Unauthorized to delete this occupancy' }); + } + + await prisma.occupancy.delete({ + where: { id: parseInt(id) }, + }); + res.status(200).send({ message: 'Occupancy deleted' }); + } catch (error) { + console.error('Error deleting occupancy:', error); + res.status(500).json({ error: 'Internal server error during occupancy deletion' }); + } finally { + await prisma.$disconnect(); + } +}); + +module.exports = occupancyRouter; \ No newline at end of file diff --git a/backend/controllers/reservations.js b/backend/controllers/reservations.js index d064b06..3b951cf 100644 --- a/backend/controllers/reservations.js +++ b/backend/controllers/reservations.js @@ -2,7 +2,9 @@ const express = require('express'); const reservationRouter = express.Router(); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); -const { identifyUser } = require('../utils/middleware'); +const { identifyUser, rbacMiddleware } = require('../utils/middleware'); +const convertReservationsToOccupancies = require('../scripts/convertReservation'); + reservationRouter.get('/', identifyUser, async (req, res) => { const { userId, classroomId, startTime, endTime } = req.query; @@ -11,14 +13,12 @@ reservationRouter.get('/', identifyUser, async (req, res) => { try { const filters = {}; - if (userRole !== 'ADMIN' && userRole !== 'Student') { - filters.userId = currentUserId; // Representatives only see their own reservations - } else if (userId) { + if (userId) { const parsedUserId = parseInt(userId); if (isNaN(parsedUserId)) { return res.status(400).json({ error: 'Invalid userId format' }); } - filters.userId = parsedUserId; // Admins can filter by userId + filters.userId = parsedUserId; } @@ -65,7 +65,7 @@ reservationRouter.get('/', identifyUser, async (req, res) => { } }); -reservationRouter.post('/', identifyUser, async (req, res) => { +reservationRouter.post('/', identifyUser, rbacMiddleware(['ADMIN', 'REPRESENTATIVE', 'TEACHER']), async (req, res) => { const { classroomId, startTime, endTime } = req.body; const userId = req.user.id; if (!(classroomId && startTime && endTime)) { @@ -176,7 +176,7 @@ reservationRouter.post('/', identifyUser, async (req, res) => { } }); -reservationRouter.delete('/:id', identifyUser, async (req, res) => { +reservationRouter.delete('/:id', identifyUser, rbacMiddleware(['ADMIN', 'REPRESENTATIVE', 'TEACHER']), async (req, res) => { const { id } = req.params; const userId = req.user.id; const userRole = req.user.role; @@ -222,7 +222,7 @@ reservationRouter.delete('/:id', identifyUser, async (req, res) => { } }); -reservationRouter.patch('/:id', identifyUser, async (req, res) => { +reservationRouter.patch('/:id', identifyUser, rbacMiddleware(['ADMIN', 'REPRESENTATIVE', 'TEACHER']), async (req, res) => { const { id } = req.params; const { classroomId, startTime, endTime } = req.body; const userId = req.user.id; @@ -349,4 +349,15 @@ reservationRouter.patch('/:id', identifyUser, async (req, res) => { await prisma.$disconnect(); } }); + + //manual reservation to occupancy conversion endpoint + reservationRouter.post('/convert', identifyUser, rbacMiddleware(['ADMIN', 'REPRESENTATIVE']), async (req, res) => { + try { + await convertReservationsToOccupancies(); + res.status(200).json({ message: 'Reservations converted to occupancies successfully' }); + } catch (error) { + console.error('Error converting reservations:', error); + res.status(500).json({ error: 'Internal server error during conversion' }); + } + }); module.exports = reservationRouter; \ No newline at end of file diff --git a/backend/controllers/users.js b/backend/controllers/users.js index 4e300c1..d0bfd90 100644 --- a/backend/controllers/users.js +++ b/backend/controllers/users.js @@ -4,11 +4,11 @@ const { error } = require('../utils/logger') const bcrypt = require('bcrypt') const { PrismaClient } = require('@prisma/client') const prisma = new PrismaClient(); -const { isAdmin, identifyUser } = require('../utils/middleware') +const { identifyUser, rbacMiddleware } = require('../utils/middleware') -userRouter.get('/',identifyUser, isAdmin, async(req, res) => { +userRouter.get('/',identifyUser, rbacMiddleware(['ADMIN']), async(req, res) => { try { const users = await prisma.user.findMany(); res.json(users); @@ -39,7 +39,7 @@ userRouter.get('/:id', identifyUser, async(req,res) => { // might implement a way to add new users as an admin maybe? -userRouter.post('/', identifyUser, isAdmin, async(req, res) => { +userRouter.post('/', rbacMiddleware(['ADMIN']), async(req, res) => { const { username, email, password, role } = req.body; if (!(username && email && password)) { @@ -88,4 +88,44 @@ userRouter.post('/', identifyUser, isAdmin, async(req, res) => { }); +//Admins can make others admins through this route OR change roles to whatever they like +userRouter.patch('/:id', identifyUser, rbacMiddleware(['ADMIN']), async (req, res) => { + const { id } = req.params; + const { role } = req.body; + + if (!role) { + return res.status(400).json({ error: 'Role is required' }); + } + if (!['STUDENT', 'TEACHER', 'REPRESENTATIVE', 'ADMIN'].includes(role)) { + return res.status(400).json({ error: 'Invalid role' }); + } + + try { + const user = await prisma.user.findUnique({ + where: { id: parseInt(id) }, + }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const updatedUser = await prisma.user.update({ + where: { id: parseInt(id) }, + data: { role }, + }); + + res.status(200).json({ + id: updatedUser.id, + username: updatedUser.username, + email: updatedUser.email, + role: updatedUser.role, + }); + } catch (error) { + console.error('Error updating user role:', error); + res.status(500).json({ error: 'Internal server error during role update' }); + } finally { + await prisma.$disconnect(); + } + }); + + module.exports = userRouter; diff --git a/backend/package-lock.json b/backend/package-lock.json index 406ba15..c58c395 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,9 +13,12 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.19.2", + "express": "^4.21.2", "express-validator": "^7.0.1", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "luxon": "^3.6.1", + "node-cron": "^3.0.3", + "socket.io": "^4.8.1" }, "devDependencies": { "nodemon": "^3.1.4", @@ -550,6 +553,30 @@ "@prisma/debug": "6.6.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -659,6 +686,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -980,6 +1016,67 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1659,6 +1756,15 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -1831,6 +1937,18 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2348,6 +2466,116 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2480,6 +2708,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2504,6 +2738,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", @@ -2553,6 +2796,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 2eeb539..c7123fd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,9 +17,12 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.19.2", + "express": "^4.21.2", "express-validator": "^7.0.1", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "luxon": "^3.6.1", + "node-cron": "^3.0.3", + "socket.io": "^4.8.1" }, "devDependencies": { "nodemon": "^3.1.4", diff --git a/backend/prisma/migrations/20250424152853_init/migration.sql b/backend/prisma/migrations/20250424152853_init/migration.sql index a2bb4da..1110519 100644 --- a/backend/prisma/migrations/20250424152853_init/migration.sql +++ b/backend/prisma/migrations/20250424152853_init/migration.sql @@ -1,75 +1,122 @@ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('STUDENT', 'REPRESENTATIVE', 'ADMIN'); - --- CreateTable +-- Drop existing Role enum and recreate with TEACHER +DROP TYPE IF EXISTS "Role"; +CREATE TYPE "Role" AS ENUM ('STUDENT', 'REPRESENTATIVE', 'ADMIN', 'TEACHER'); + +-- Drop tables if they exist to ensure clean migration +DROP TABLE IF EXISTS "notification" CASCADE; +DROP TABLE IF EXISTS "occupancy" CASCADE; +DROP TABLE IF EXISTS "reservations" CASCADE; +DROP TABLE IF EXISTS "classrooms" CASCADE; +DROP TABLE IF EXISTS "buildings" CASCADE; +DROP TABLE IF EXISTS "floors" CASCADE; +DROP TABLE IF EXISTS "users" CASCADE; + +-- CreateTable: users CREATE TABLE "users" ( - "id" SERIAL NOT NULL, - "username" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "role" "Role" NOT NULL DEFAULT 'STUDENT', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "users_pkey" PRIMARY KEY ("id") + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'STUDENT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "users_pkey" PRIMARY KEY ("id") ); --- CreateTable +-- CreateTable: buildings CREATE TABLE "buildings" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "floorId" INTEGER NOT NULL, - "status" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "buildings_pkey" PRIMARY KEY ("id") + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "status" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "buildings_pkey" PRIMARY KEY ("id") ); --- CreateTable +-- CreateTable: floors CREATE TABLE "floors" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "floors_pkey" PRIMARY KEY ("id") + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "buildingId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "floors_pkey" PRIMARY KEY ("id") ); --- CreateTable +-- CreateTable: classrooms CREATE TABLE "classrooms" ( - "id" SERIAL NOT NULL, - "floorId" INTEGER NOT NULL, - "buildingId" INTEGER NOT NULL, - "name" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "classrooms_pkey" PRIMARY KEY ("id") + "id" SERIAL NOT NULL, + "floorId" INTEGER NOT NULL, + "buildingId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "classrooms_pkey" PRIMARY KEY ("id") ); --- CreateTable +-- CreateTable: reservations CREATE TABLE "reservations" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "classroomId" INTEGER NOT NULL, - "startTime" TIMESTAMP(3) NOT NULL, - "endTime" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "reservations_pkey" PRIMARY KEY ("id") + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "classroomId" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "reservations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: occupancy +CREATE TABLE "occupancy" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "classroomId" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "status" TEXT NOT NULL DEFAULT 'occupied', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "occupancy_pkey" PRIMARY KEY ("id"), + CONSTRAINT "Occupancy_unique" UNIQUE ("classroomId", "startTime", "endTime", "userId") ); --- CreateIndex +-- CreateTable: notification +CREATE TABLE "notification" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "message" TEXT NOT NULL, + "isRead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "notification_pkey" PRIMARY KEY ("id") +); + +-- Create Index CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); --- AddForeignKey -ALTER TABLE "buildings" ADD CONSTRAINT "buildings_floorId_fkey" FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +-- Add Foreign Keys +ALTER TABLE "floors" +ADD CONSTRAINT "floors_buildingId_fkey" +FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "classrooms" +ADD CONSTRAINT "classrooms_floorId_fkey" +FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "classrooms" +ADD CONSTRAINT "classrooms_buildingId_fkey" +FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "reservations" +ADD CONSTRAINT "reservations_userId_fkey" +FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_floorId_fkey" FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "reservations" +ADD CONSTRAINT "reservations_classroomId_fkey" +FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_buildingId_fkey" FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "occupancy" +ADD CONSTRAINT "occupancy_userId_fkey" +FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "reservations" ADD CONSTRAINT "reservations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "occupancy" +ADD CONSTRAINT "occupancy_classroomId_fkey" +FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "reservations" ADD CONSTRAINT "reservations_classroomId_fkey" FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "notification" +ADD CONSTRAINT "notification_userId_fkey" +FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql b/backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql new file mode 100644 index 0000000..8954e04 --- /dev/null +++ b/backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "occupancy" ALTER COLUMN "updatedAt" DROP DEFAULT; + +-- RenameIndex +ALTER INDEX "Occupancy_unique" RENAME TO "occupancy_classroomId_startTime_endTime_userId_key"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4a134b6..6078448 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -10,65 +10,89 @@ datasource db { enum Role { STUDENT REPRESENTATIVE - ADMIN // Added Admin role + ADMIN + TEACHER } model User { - id Int @id @default(autoincrement()) - username String @unique - email String // Added email - password String - role Role @default(STUDENT) - reservations Reservation[] - createdAt DateTime @default(now()) // Added createdAt - + id Int @id @default(autoincrement()) + username String @unique + email String + password String + role Role @default(STUDENT) + reservations Reservation[] + occupancies Occupancy[] + notifications Notification[] + createdAt DateTime @default(now()) @@map("users") } model Building { - id Int @id @default(autoincrement()) - name String - floorId Int // Foreign Key - floor Floor @relation(fields: [floorId], references: [id]) - status String? // Nullable status - createdAt DateTime @default(now()) - classrooms Classroom[] - + id Int @id @default(autoincrement()) + name String + floors Floor[] + classrooms Classroom[] + status String? + createdAt DateTime @default(now()) @@map("buildings") } model Floor { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - buildings Building[] - classrooms Classroom[] - + id Int @id @default(autoincrement()) + name String + buildingId Int + building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade) + classrooms Classroom[] + createdAt DateTime @default(now()) @@map("floors") } model Classroom { - id Int @id @default(autoincrement()) - floorId Int // Foreign Key - floor Floor @relation(fields: [floorId], references: [id]) - buildingId Int // Foreign Key - building Building @relation(fields: [buildingId], references: [id]) - name String - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + floorId Int + floor Floor @relation(fields: [floorId], references: [id], onDelete: Cascade) + buildingId Int + building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade) + name String + createdAt DateTime @default(now()) reservations Reservation[] - + occupancies Occupancy[] @@map("classrooms") } model Reservation { - id Int @id @default(autoincrement()) - userId Int // Foreign Key - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - classroomId Int // Foreign Key - classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade) - startTime DateTime - endTime DateTime - createdAt DateTime @default(now()) - + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + classroomId Int + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade) + startTime DateTime + endTime DateTime + createdAt DateTime @default(now()) @@map("reservations") } + +model Occupancy { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + classroomId Int + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade) + startTime DateTime + endTime DateTime + status String @default("occupied") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([classroomId, startTime, endTime, userId], name: "Occupancy_unique") + @@map("occupancy") +} + +model Notification { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + message String + isRead Boolean @default(false) + createdAt DateTime @default(now()) + @@map("notification") +} diff --git a/backend/scripts/convertReservation.js b/backend/scripts/convertReservation.js new file mode 100644 index 0000000..7ff072b --- /dev/null +++ b/backend/scripts/convertReservation.js @@ -0,0 +1,90 @@ +const { PrismaClient } = require('@prisma/client'); +const { DateTime } = require('luxon'); +const cron = require('node-cron'); + +const prisma = new PrismaClient(); + +// Utility: Sleep function to avoid hammering DB +const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); + +async function convertReservationsToOccupancies() { + const now = DateTime.utc().toJSDate(); + const batchSize = 10; + let hasMore = true; + + console.log('\nStarting reservation-to-occupancy job at:', now.toISOString()); + console.log('Local time (EAT):', new Date().toLocaleString('en-ET', { timeZone: 'Africa/Addis_Ababa' })); + + try { + while (hasMore) { + const reservations = await prisma.reservation.findMany({ + where: { + startTime: { + lte: now, + }, + }, + include: { + classroom: true, + user: { select: { id: true, username: true } }, + }, + take: batchSize, + orderBy: { startTime: 'asc' }, + }); + + if (reservations.length === 0) { + console.log('No more reservations to process.'); + break; + } + + for (const reservation of reservations) { + await prisma.$transaction(async (tx) => { + const existing = await tx.occupancy.findFirst({ + where: { + classroomId: reservation.classroomId, + userId: reservation.userId, + startTime: reservation.startTime, + endTime: reservation.endTime, + }, + }); + + if (existing) { + console.log(`Occupancy already exists for reservation ${reservation.id}, deleting reservation...`); + await tx.reservation.delete({ where: { id: reservation.id } }); + return; + } + + await tx.occupancy.create({ + data: { + classroomId: reservation.classroomId, + userId: reservation.userId, + startTime: reservation.startTime, + endTime: reservation.endTime, + status: 'occupied', + }, + }); + + await tx.reservation.delete({ where: { id: reservation.id } }); + console.log(`Converted reservation ${reservation.id} to occupancy.`); + }); + } + + if (reservations.length < batchSize) { + hasMore = false; + } else { + await sleep(200); + } + } + + console.log('🎉 Finished all processing at:', new Date().toISOString()); + } catch (error) { + console.error('Error during reservation conversion:', error); + } +} + +// Run every 1 minute (change to '*/5 * * * *' for 5-min interval) +cron.schedule('*/5 * * * *', async () => { + console.log('\nCron job triggered at:', new Date().toISOString()); + await convertReservationsToOccupancies(); +}); + +module.exports = convertReservationsToOccupancies; diff --git a/backend/seed.js b/backend/seed.js index e5b90bb..fdcd28f 100644 --- a/backend/seed.js +++ b/backend/seed.js @@ -1,111 +1,147 @@ -// seed.js const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const bcrypt = require('bcrypt'); async function main() { - // Create Users - const hashedPassword = await bcrypt.hash('password123', 10); // Hash the password - - const adminUser = await prisma.user.create({ - data: { - username: 'admin', - email: 'admin@example.com', - password: hashedPassword, - role: 'ADMIN', - }, - }); - - const repUser = await prisma.user.create({ - data: { - username: 'rep1', - email: 'rep1@example.com', - password: hashedPassword, - role: 'REPRESENTATIVE', - }, - }); - - const studentUser = await prisma.user.create({ - data: { - username: 'student1', - email: 'student1@example.com', - password: hashedPassword, - role: 'STUDENT', - }, - }); + // Clear existing data (in proper order to avoid foreign key issues) + await prisma.occupancy.deleteMany(); + await prisma.reservation.deleteMany(); + await prisma.classroom.deleteMany(); + await prisma.building.deleteMany(); + await prisma.floor.deleteMany(); + await prisma.user.deleteMany(); - // Create Floors - const floor1 = await prisma.floor.create({ - data: { - name: '1st Floor', - }, - }); + // Create Users + const hashedPassword = await bcrypt.hash('password123', 10); - const floor2 = await prisma.floor.create({ - data: { - name: '2nd Floor', - }, - }); + const adminUser = await prisma.user.create({ + data: { + username: 'admin', + email: 'admin@example.com', + password: hashedPassword, + role: 'ADMIN', + }, + }); - // Create Buildings - const buildingA = await prisma.building.create({ - data: { - name: 'Building A', - floorId: floor1.id, - }, - }); + const repUser = await prisma.user.create({ + data: { + username: 'rep1', + email: 'rep1@example.com', + password: hashedPassword, + role: 'REPRESENTATIVE', + }, + }); - const buildingB = await prisma.building.create({ - data: { - name: 'Building B', - floorId: floor2.id, - }, - }); - - // Create Classrooms - const classroom101 = await prisma.classroom.create({ - data: { - name: 'Room 101', - floorId: floor1.id, - buildingId: buildingA.id, - }, - }); + const studentUser = await prisma.user.create({ + data: { + username: 'student1', + email: 'student1@example.com', + password: hashedPassword, + role: 'STUDENT', + }, + }); - const classroom201 = await prisma.classroom.create({ - data: { - name: 'Room 201', - floorId: floor2.id, - buildingId: buildingB.id, - }, - }); - - // Create Reservations - await prisma.reservation.create({ - data: { - userId: repUser.id, - classroomId: classroom101.id, - startTime: new Date(2025, 0, 1, 9, 0, 0), // January 1, 2025, 9:00 AM - endTime: new Date(2025, 0, 1, 10, 0, 0), + const teacherUser = await prisma.user.create({ + data: { + username: 'teacher1', + email: 'teacher1@example.com', + password: hashedPassword, + role: 'TEACHER', + }, + }); + + // Create Floors and Buildings + const floor1 = await prisma.floor.create({ + data: { + name: '1st Floor', + building: { + create: { + name: 'Building A', + status: 'active', }, - }); - - await prisma.reservation.create({ - data: { - userId: studentUser.id, - classroomId: classroom201.id, - startTime: new Date(2025, 0, 2, 14, 0, 0), // January 2, 2025, 2:00 PM - endTime: new Date(2025, 0, 16, 0, 0), + }, + }, + include: { building: true }, + }); + + const floor2 = await prisma.floor.create({ + data: { + name: '2nd Floor', + building: { + create: { + name: 'Building B', + status: 'active', }, - }); + }, + }, + include: { building: true }, + }); + + // Create Classrooms + const classroom101 = await prisma.classroom.create({ + data: { + name: 'Room 101', + floorId: floor1.id, + buildingId: floor1.building.id, + }, + }); + + const classroom201 = await prisma.classroom.create({ + data: { + name: 'Room 201', + floorId: floor2.id, + buildingId: floor2.building.id, + }, + }); + + // Create Reservations + await prisma.reservation.create({ + data: { + userId: repUser.id, + classroomId: classroom101.id, + startTime: new Date(2025, 0, 1, 9, 0, 0), + endTime: new Date(2025, 0, 1, 10, 0, 0), + }, + }); + + await prisma.reservation.create({ + data: { + userId: studentUser.id, + classroomId: classroom201.id, + startTime: new Date(2025, 0, 2, 14, 0, 0), + endTime: new Date(2025, 0, 2, 16, 0, 0), + }, + }); + + // Create Occupancies + await prisma.occupancy.create({ + data: { + userId: studentUser.id, + classroomId: classroom101.id, + startTime: new Date('2025-05-01T08:00:00Z'), + endTime: new Date('2025-05-01T10:00:00Z'), + status: 'occupied', + }, + }); + + await prisma.occupancy.create({ + data: { + userId: repUser.id, + classroomId: classroom201.id, + startTime: new Date('2025-05-01T10:00:00Z'), + endTime: new Date('2025-05-01T12:00:00Z'), + status: 'occupied', + }, + }); - console.log('Seed data inserted successfully!'); + console.log('Seed data inserted successfully!'); } main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); + .catch((e) => { + console.error('❌ Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/utils/middleware.js b/backend/utils/middleware.js index a9171ec..28682b9 100644 --- a/backend/utils/middleware.js +++ b/backend/utils/middleware.js @@ -57,21 +57,6 @@ const identifyUser = async (req, res, next) => { } }; -const isRep = (req, res, next) => { - if (req.user && req.user.role === 'REPRESENTATIVE') { - next(); - } else { - return res.status(403).json({ error: 'Forbidden - Representatives only.' }); - } -}; - -const isAdmin = (req, res, next) => { - if(req.user && req.user.role === 'ADMIN'){ - next(); - }else{ - return(res.status(403).json({ error: 'Forbidden - Admin Only.' })) - } -} const errorHandler = (error, request, response, next) => { @@ -90,16 +75,31 @@ const errorHandler = (error, request, response, next) => { error: 'inivalid token' })) } + next(error) +}; + +const rbacMiddleware = (allowedRoles) => { + return (req, res, next) => { + if (!req.user || !req.user.role) { + return res.status(401).json({ error: 'Unauthorized - User not authenticated' }); + } + + const userRole = req.user.role; + + if (!allowedRoles.includes(userRole)) { + return res.status(403).json({ error: `Forbidden - ${userRole} role not authorized` }); + } + + next(); + }; +}; - next(error) -} module.exports = { requestLogger, getTokenFrom, unknownEndpoint, identifyUser, errorHandler, - isRep, - isAdmin + rbacMiddleware } From 9c99349862bc049ee69f09c6fa094c9f45d35f2a Mon Sep 17 00:00:00 2001 From: YITBAREK ALEMU <160623517+jaeckult@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:42:56 +0300 Subject: [PATCH 08/11] occupancy-notification-conversion_of_reservation_to_occupancy --- unispace/src/SchedulePage.js | 137 +++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/unispace/src/SchedulePage.js b/unispace/src/SchedulePage.js index 8845057..6a1834e 100644 --- a/unispace/src/SchedulePage.js +++ b/unispace/src/SchedulePage.js @@ -17,45 +17,49 @@ function SchedulePage() { // Fetch classrooms and reservations on mount useEffect(() => { - const fetchClassrooms = async () => { - try { - const response = await fetch('http://localhost:9000/api/classrooms', { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('token')}` - } - }); - const data = await response.json(); - if (response.ok) { - setClassrooms(data); - } else { - throw new Error(data.error || 'Failed to fetch classrooms'); + fetchData(); + }, []); + + const fetchData = async () => { + await fetchClassrooms(); + await fetchReservations(); + }; + + const fetchClassrooms = async () => { + try { + const response = await fetch('http://localhost:9000/api/classrooms', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` } - } catch (err) { - setError(err.message); + }); + const data = await response.json(); + if (response.ok) { + setClassrooms(data); + } else { + throw new Error(data.error || 'Failed to fetch classrooms'); } - }; + } catch (err) { + setError(err.message); + } + }; - const fetchReservations = async () => { - try { - const response = await fetch('http://localhost:9000/api/reservations', { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('token')}` - } - }); - const data = await response.json(); - if (response.ok) { - setReservations(data); - } else { - throw new Error(data.error || 'Failed to fetch reservations'); + const fetchReservations = async () => { + try { + const response = await fetch('http://localhost:9000/api/reservations', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` } - } catch (err) { - setError(err.message); + }); + const data = await response.json(); + if (response.ok) { + setReservations(data); + } else { + throw new Error(data.error || 'Failed to fetch reservations'); } - }; - - fetchClassrooms(); - fetchReservations(); - }, []); + } catch (err) { + setError(err.message); + } + }; // Handle input changes const handleChange = (e) => { @@ -71,21 +75,15 @@ function SchedulePage() { setError(''); setSuccess(''); - // Combine date and time for startTime and endTime - const [startDate, startTime] = formData.startTime.split('T'); - const [endDate, endTime] = formData.endTime.split('T'); - const formattedStartTime = startDate && startTime ? `${startDate}T${startTime}:00Z` : ''; - const formattedEndTime = endDate && endTime ? `${endDate}T${endTime}:00Z` : ''; - - if (!formattedStartTime || !formattedEndTime) { + if (!formData.startTime || !formData.endTime) { setError('Please provide both start and end times'); return; } const payload = { classroomId: parseInt(formData.classroomId), - startTime: formattedStartTime, - endTime: formattedEndTime + startTime: formData.startTime, + endTime: formData.endTime }; try { @@ -104,21 +102,37 @@ function SchedulePage() { throw new Error(data.error || 'Reservation failed'); } - // Success: Show message and refresh reservations setSuccess('Reservation successful!'); setFormData({ classroomId: '', startTime: '', endTime: '' }); - // Refresh reservations - const reservationsResponse = await fetch('http://localhost:9000/api/reservations', { + await fetchReservations(); + } catch (err) { + setError(err.message || 'An error occurred during reservation'); + } + }; + + // Handle reservation deletion + const handleDelete = async (id) => { + const confirmDelete = window.confirm('Are you sure you want to delete this reservation?'); + if (!confirmDelete) return; + + try { + const response = await fetch(`http://localhost:9000/api/reservations/${id}`, { + method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); - const reservationsData = await reservationsResponse.json(); - if (reservationsResponse.ok) { - setReservations(reservationsData); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to delete reservation'); } + + setSuccess('Reservation deleted successfully!'); + await fetchReservations(); } catch (err) { - setError(err.message || 'An error occurred during reservation'); + setError(err.message || 'An error occurred during deletion'); } }; @@ -126,30 +140,27 @@ function SchedulePage() {
+

Classroom Reservations

+

Class Queue

{reservations.length > 0 ? (
    {reservations.map((reservation) => ( -
  • +
  • {reservation.classroom.name}: {new Date(reservation.startTime).toLocaleString()} to {new Date(reservation.endTime).toLocaleString()} +
  • ))}
@@ -158,6 +169,7 @@ function SchedulePage() { )}
+

Reserve Class

@@ -178,6 +190,7 @@ function SchedulePage() { ))} + + + {error &&

{error}

} {success &&

{success}

} @@ -210,4 +225,4 @@ function SchedulePage() { ); } -export default SchedulePage; \ No newline at end of file +export default SchedulePage; From 111af8be05f0e05eaa8c4804519ad648100b2521 Mon Sep 17 00:00:00 2001 From: YITBAREK ALEMU <160623517+jaeckult@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:46:48 +0300 Subject: [PATCH 09/11] occupancy-notification-conversion_of_reservation_to_occupancy --- .gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b06f9bd..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -part4 -.env -node_modules From 18d6f558f140cbb3e2a615725c40ce978653f901 Mon Sep 17 00:00:00 2001 From: YITBAREK ALEMU <160623517+jaeckult@users.noreply.github.com> Date: Fri, 2 May 2025 15:39:35 +0300 Subject: [PATCH 10/11] added profile --- backend/app.js | 2 + backend/controllers/notification.js | 3 +- backend/controllers/profile.js | 209 ++++++++++++++++++ .../20250424152853_init/migration.sql | 28 ++- .../migration.sql | 78 +++++++ backend/prisma/schema.prisma | 73 +++--- backend/seed.js | 5 +- 7 files changed, 363 insertions(+), 35 deletions(-) create mode 100644 backend/controllers/profile.js create mode 100644 backend/prisma/migrations/20250501203806_init_corrected/migration.sql diff --git a/backend/app.js b/backend/app.js index d7d1a22..14e7646 100644 --- a/backend/app.js +++ b/backend/app.js @@ -14,6 +14,7 @@ const { notificationRouter } = require("./controllers/notification"); const {Server} = require("socket.io"); +const profileRouter = require("./controllers/profile"); const server = http.createServer(app); const io = new Server(server, { cors: { @@ -46,6 +47,7 @@ app.use('/api/reservations', reservationRouter) app.use('/api/classrooms', classroomRouter) app.use('/api/occupancy', occupancyRouter) app.use('/api/notifications', notificationRouter) +app.use('/api/profiles', profileRouter) io.on('connection', (socket) => { diff --git a/backend/controllers/notification.js b/backend/controllers/notification.js index 5c8bd57..a3ffe9e 100644 --- a/backend/controllers/notification.js +++ b/backend/controllers/notification.js @@ -140,4 +140,5 @@ notificationRouter.delete('/:id', getTokenFrom, identifyUser, rbacMiddleware(['A module.exports = { notificationRouter, createNotification, -}; \ No newline at end of file +}; + diff --git a/backend/controllers/profile.js b/backend/controllers/profile.js new file mode 100644 index 0000000..8965cf8 --- /dev/null +++ b/backend/controllers/profile.js @@ -0,0 +1,209 @@ +const express = require('express'); +const profileRouter = express.Router(); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const { identifyUser, rbacMiddleware } = require('../utils/middleware'); +const convertReservationsToOccupancies = require('../scripts/convertReservation'); + +// GET /api/profiles - Fetch all profiles +profileRouter.get('/', identifyUser, async (req, res) => { + try { + const profiles = await prisma.profile.findMany({ + include: { + user: { + select: { + id: true, + email: true, + } + }, + } + }); + if (!profiles || profiles.length === 0) { + return res.status(404).json({ + success: false, + message: 'No profiles found' + }); + } + return res.status(200).json({ + success: true, + data: profiles, + count: profiles.length + }); + + } catch (error) { + console.error('Error fetching profiles:', error); + if (error.name === 'PrismaClientKnownRequestError') { + return res.status(400).json({ + success: false, + message: 'Database error occurred', + error: error.message + }); + } + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message + }); + } finally { + await prisma.$disconnect(); + } +}); + +// GET /api/profiles/:id - Fetch a single profile by ID +profileRouter.get('/:id', identifyUser, async (req, res) => { + try { + const profileId = parseInt(req.params.id); + const profile = await prisma.profile.findUnique({ + where: { id: profileId }, + include: { + user: { + select: { + id: true, + email: true, + } + }, + } + }); + + if (!profile) { + return res.status(404).json({ + success: false, + message: 'Profile not found' + }); + } + + return res.status(200).json({ + success: true, + data: profile + }); + + } catch (error) { + console.error('Error fetching profile:', error); + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message + }); + } finally { + await prisma.$disconnect(); + } +}); + +// POST /api/profiles - Create a new profile +profileRouter.post('/', identifyUser, async (req, res) => { + try { + const { userId, firstName, lastName, phone, address } = req.body; + if (!userId || !firstName || !lastName) { + return res.status(400).json({ + success: false, + message: 'Missing required fields' + }); + } + const newProfile = await prisma.profile.create({ + data: { + userId, + firstName, + lastName, + phone, + address, + } + }); + + return res.status(201).json({ + success: true, + data: newProfile, + message: 'Profile created successfully' + }); + + } catch (error) { + console.error('Error creating profile:', error); + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message + }); + } finally { + await prisma.$disconnect(); + } +}); + +// PUT /api/profiles/:id - Update a profile +profileRouter.put('/:id', identifyUser, async (req, res) => { + try { + const profileId = parseInt(req.params.id); + const { firstName, lastName, phone, address } = req.body; + const existingProfile = await prisma.profile.findUnique({ + where: { id: profileId } + }); + + if (!existingProfile) { + return res.status(404).json({ + success: false, + message: 'Profile not found' + }); + } + + const updatedProfile = await prisma.profile.update({ + where: { id: profileId }, + data: { + firstName, + lastName, + phone, + address, + } + }); + + return res.status(200).json({ + success: true, + data: updatedProfile, + message: 'Profile updated successfully' + }); + + } catch (error) { + console.error('Error updating profile:', error); + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message + }); + } finally { + await prisma.$disconnect(); + } +}); + +// DELETE /api/profiles/:id - Delete a profile +profileRouter.delete('/:id', identifyUser, rbacMiddleware(['ADMIN']), async (req, res) => { + try { + const profileId = parseInt(req.params.id); + const existingProfile = await prisma.profile.findUnique({ + where: { id: profileId } + }); + + if (!existingProfile) { + return res.status(404).json({ + success: false, + message: 'Profile not found' + }); + } + await prisma.profile.delete({ + where: { id: profileId } + }); + + return res.status(200).json({ + success: true, + message: 'Profile deleted successfully' + }); + + } catch (error) { + console.error('Error deleting profile:', error); + return res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message + }); + } finally { + await prisma.$disconnect(); + } +}); + +module.exports = profileRouter; \ No newline at end of file diff --git a/backend/prisma/migrations/20250424152853_init/migration.sql b/backend/prisma/migrations/20250424152853_init/migration.sql index 1110519..b2abea7 100644 --- a/backend/prisma/migrations/20250424152853_init/migration.sql +++ b/backend/prisma/migrations/20250424152853_init/migration.sql @@ -7,8 +7,9 @@ DROP TABLE IF EXISTS "notification" CASCADE; DROP TABLE IF EXISTS "occupancy" CASCADE; DROP TABLE IF EXISTS "reservations" CASCADE; DROP TABLE IF EXISTS "classrooms" CASCADE; -DROP TABLE IF EXISTS "buildings" CASCADE; DROP TABLE IF EXISTS "floors" CASCADE; +DROP TABLE IF EXISTS "buildings" CASCADE; +DROP TABLE IF EXISTS "profile" CASCADE; DROP TABLE IF EXISTS "users" CASCADE; -- CreateTable: users @@ -79,14 +80,29 @@ CREATE TABLE "occupancy" ( CREATE TABLE "notification" ( "id" SERIAL NOT NULL, "userId" INTEGER NOT NULL, + "reservationId" INTEGER, "message" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'INFO', "isRead" BOOLEAN NOT NULL DEFAULT false, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "notification_pkey" PRIMARY KEY ("id") ); --- Create Index +-- CreateTable: profile +CREATE TABLE "profile" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "phone" TEXT, + "address" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "profile_pkey" PRIMARY KEY ("id") +); + +-- Create Indexes CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); -- Add Foreign Keys ALTER TABLE "floors" @@ -120,3 +136,11 @@ FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON U ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "notification" +ADD CONSTRAINT "notification_reservationId_fkey" +FOREIGN KEY ("reservationId") REFERENCES "reservations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "profile" +ADD CONSTRAINT "profile_userId_fkey" +FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/backend/prisma/migrations/20250501203806_init_corrected/migration.sql b/backend/prisma/migrations/20250501203806_init_corrected/migration.sql new file mode 100644 index 0000000..849f954 --- /dev/null +++ b/backend/prisma/migrations/20250501203806_init_corrected/migration.sql @@ -0,0 +1,78 @@ +/* + Warnings: + + - You are about to drop the `reservations` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `users` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "notification" DROP CONSTRAINT "notification_reservationId_fkey"; + +-- DropForeignKey +ALTER TABLE "notification" DROP CONSTRAINT "notification_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "occupancy" DROP CONSTRAINT "occupancy_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "profile" DROP CONSTRAINT "profile_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "reservations" DROP CONSTRAINT "reservations_classroomId_fkey"; + +-- DropForeignKey +ALTER TABLE "reservations" DROP CONSTRAINT "reservations_userId_fkey"; + +-- DropTable +DROP TABLE "reservations"; + +-- DropTable +DROP TABLE "users"; + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'STUDENT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Reservation" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "classroomId" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Reservation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_classroomId_fkey" FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "occupancy" ADD CONSTRAINT "occupancy_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification" ADD CONSTRAINT "notification_reservationId_fkey" FOREIGN KEY ("reservationId") REFERENCES "Reservation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profile" ADD CONSTRAINT "profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6078448..b4c579a 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -15,16 +15,16 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - username String @unique - email String - password String - role Role @default(STUDENT) - reservations Reservation[] - occupancies Occupancy[] - notifications Notification[] - createdAt DateTime @default(now()) - @@map("users") + id Int @id @default(autoincrement()) + username String @unique + email String @unique + password String + role Role @default(STUDENT) + createdAt DateTime @default(now()) + profiles Profile[] + reservations Reservation[] + occupancies Occupancy[] + notifications Notification[] } model Building { @@ -41,7 +41,7 @@ model Floor { id Int @id @default(autoincrement()) name String buildingId Int - building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade) + building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade, onUpdate: Cascade) classrooms Classroom[] createdAt DateTime @default(now()) @@map("floors") @@ -50,9 +50,9 @@ model Floor { model Classroom { id Int @id @default(autoincrement()) floorId Int - floor Floor @relation(fields: [floorId], references: [id], onDelete: Cascade) + floor Floor @relation(fields: [floorId], references: [id], onDelete: Cascade, onUpdate: Cascade) buildingId Int - building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade) + building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade, onUpdate: Cascade) name String createdAt DateTime @default(now()) reservations Reservation[] @@ -61,23 +61,23 @@ model Classroom { } model Reservation { - id Int @id @default(autoincrement()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - classroomId Int - classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade) - startTime DateTime - endTime DateTime - createdAt DateTime @default(now()) - @@map("reservations") + id Int @id @default(autoincrement()) + userId Int + classroomId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) + startTime DateTime + endTime DateTime + createdAt DateTime @default(now()) + notifications Notification[] } model Occupancy { id Int @id @default(autoincrement()) userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) classroomId Int - classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade) + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) startTime DateTime endTime DateTime status String @default("occupied") @@ -88,11 +88,26 @@ model Occupancy { } model Notification { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + reservationId Int? + reservation Reservation? @relation(fields: [reservationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + message String + type String @default("INFO") + isRead Boolean @default(false) + createdAt DateTime @default(now()) + @@map("notification") +} + +model Profile { id Int @id @default(autoincrement()) userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - message String - isRead Boolean @default(false) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + firstName String + lastName String + phone String? + address String? createdAt DateTime @default(now()) - @@map("notification") -} + @@map("profile") +} \ No newline at end of file diff --git a/backend/seed.js b/backend/seed.js index fdcd28f..cd6655c 100644 --- a/backend/seed.js +++ b/backend/seed.js @@ -50,7 +50,6 @@ async function main() { }, }); - // Create Floors and Buildings const floor1 = await prisma.floor.create({ data: { name: '1st Floor', @@ -77,7 +76,7 @@ async function main() { include: { building: true }, }); - // Create Classrooms + const classroom101 = await prisma.classroom.create({ data: { name: 'Room 101', @@ -139,7 +138,7 @@ async function main() { main() .catch((e) => { - console.error('❌ Seed failed:', e); + console.error('Seed failed:', e); process.exit(1); }) .finally(async () => { From 3e8e119ae7a6841e15c5eaa2bc88cc3c59afd0f0 Mon Sep 17 00:00:00 2001 From: YITBAREK ALEMU <160623517+jaeckult@users.noreply.github.com> Date: Fri, 2 May 2025 22:11:35 +0300 Subject: [PATCH 11/11] triggering-notification --- backend/package-lock.json | 72 ++++----- backend/package.json | 4 +- .../20250424152853_init/migration.sql | 146 ------------------ .../migration.sql | 5 - .../migration.sql | 78 ---------- .../20250502190105_init/migration.sql | 140 +++++++++++++++++ backend/prisma/schema.prisma | 121 ++++++++------- backend/scripts/triggerNotification.js | 74 +++++++++ 8 files changed, 317 insertions(+), 323 deletions(-) delete mode 100644 backend/prisma/migrations/20250424152853_init/migration.sql delete mode 100644 backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql delete mode 100644 backend/prisma/migrations/20250501203806_init_corrected/migration.sql create mode 100644 backend/prisma/migrations/20250502190105_init/migration.sql create mode 100644 backend/scripts/triggerNotification.js diff --git a/backend/package-lock.json b/backend/package-lock.json index c58c395..456a408 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^6.6.0", + "@prisma/client": "^6.7.0", "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", @@ -22,7 +22,7 @@ }, "devDependencies": { "nodemon": "^3.1.4", - "prisma": "^6.6.0" + "prisma": "^6.7.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -471,9 +471,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.6.0.tgz", - "integrity": "sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", + "integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -493,9 +493,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.6.0.tgz", - "integrity": "sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz", + "integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -504,53 +504,53 @@ } }, "node_modules/@prisma/debug": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.6.0.tgz", - "integrity": "sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz", + "integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.6.0.tgz", - "integrity": "sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz", + "integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.6.0", - "@prisma/engines-version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", - "@prisma/fetch-engine": "6.6.0", - "@prisma/get-platform": "6.6.0" + "@prisma/debug": "6.7.0", + "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "@prisma/fetch-engine": "6.7.0", + "@prisma/get-platform": "6.7.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a.tgz", - "integrity": "sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==", + "version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz", + "integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.6.0.tgz", - "integrity": "sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz", + "integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.6.0", - "@prisma/engines-version": "6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a", - "@prisma/get-platform": "6.6.0" + "@prisma/debug": "6.7.0", + "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "@prisma/get-platform": "6.7.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.6.0.tgz", - "integrity": "sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz", + "integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.6.0" + "@prisma/debug": "6.7.0" } }, "node_modules/@socket.io/component-emitter": { @@ -2141,15 +2141,15 @@ } }, "node_modules/prisma": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.6.0.tgz", - "integrity": "sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz", + "integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.6.0", - "@prisma/engines": "6.6.0" + "@prisma/config": "6.7.0", + "@prisma/engines": "6.7.0" }, "bin": { "prisma": "build/index.js" diff --git a/backend/package.json b/backend/package.json index c7123fd..9d0d790 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,7 +13,7 @@ "author": "Your Name", "license": "ISC", "dependencies": { - "@prisma/client": "^6.6.0", + "@prisma/client": "^6.7.0", "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", @@ -26,6 +26,6 @@ }, "devDependencies": { "nodemon": "^3.1.4", - "prisma": "^6.6.0" + "prisma": "^6.7.0" } } diff --git a/backend/prisma/migrations/20250424152853_init/migration.sql b/backend/prisma/migrations/20250424152853_init/migration.sql deleted file mode 100644 index b2abea7..0000000 --- a/backend/prisma/migrations/20250424152853_init/migration.sql +++ /dev/null @@ -1,146 +0,0 @@ --- Drop existing Role enum and recreate with TEACHER -DROP TYPE IF EXISTS "Role"; -CREATE TYPE "Role" AS ENUM ('STUDENT', 'REPRESENTATIVE', 'ADMIN', 'TEACHER'); - --- Drop tables if they exist to ensure clean migration -DROP TABLE IF EXISTS "notification" CASCADE; -DROP TABLE IF EXISTS "occupancy" CASCADE; -DROP TABLE IF EXISTS "reservations" CASCADE; -DROP TABLE IF EXISTS "classrooms" CASCADE; -DROP TABLE IF EXISTS "floors" CASCADE; -DROP TABLE IF EXISTS "buildings" CASCADE; -DROP TABLE IF EXISTS "profile" CASCADE; -DROP TABLE IF EXISTS "users" CASCADE; - --- CreateTable: users -CREATE TABLE "users" ( - "id" SERIAL NOT NULL, - "username" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "role" "Role" NOT NULL DEFAULT 'STUDENT', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "users_pkey" PRIMARY KEY ("id") -); - --- CreateTable: buildings -CREATE TABLE "buildings" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "status" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "buildings_pkey" PRIMARY KEY ("id") -); - --- CreateTable: floors -CREATE TABLE "floors" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "buildingId" INTEGER NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "floors_pkey" PRIMARY KEY ("id") -); - --- CreateTable: classrooms -CREATE TABLE "classrooms" ( - "id" SERIAL NOT NULL, - "floorId" INTEGER NOT NULL, - "buildingId" INTEGER NOT NULL, - "name" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "classrooms_pkey" PRIMARY KEY ("id") -); - --- CreateTable: reservations -CREATE TABLE "reservations" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "classroomId" INTEGER NOT NULL, - "startTime" TIMESTAMP(3) NOT NULL, - "endTime" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "reservations_pkey" PRIMARY KEY ("id") -); - --- CreateTable: occupancy -CREATE TABLE "occupancy" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "classroomId" INTEGER NOT NULL, - "startTime" TIMESTAMP(3) NOT NULL, - "endTime" TIMESTAMP(3) NOT NULL, - "status" TEXT NOT NULL DEFAULT 'occupied', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "occupancy_pkey" PRIMARY KEY ("id"), - CONSTRAINT "Occupancy_unique" UNIQUE ("classroomId", "startTime", "endTime", "userId") -); - --- CreateTable: notification -CREATE TABLE "notification" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "reservationId" INTEGER, - "message" TEXT NOT NULL, - "type" TEXT NOT NULL DEFAULT 'INFO', - "isRead" BOOLEAN NOT NULL DEFAULT false, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "notification_pkey" PRIMARY KEY ("id") -); - --- CreateTable: profile -CREATE TABLE "profile" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "phone" TEXT, - "address" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "profile_pkey" PRIMARY KEY ("id") -); - --- Create Indexes -CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); -CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); - --- Add Foreign Keys -ALTER TABLE "floors" -ADD CONSTRAINT "floors_buildingId_fkey" -FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "classrooms" -ADD CONSTRAINT "classrooms_floorId_fkey" -FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "classrooms" -ADD CONSTRAINT "classrooms_buildingId_fkey" -FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "reservations" -ADD CONSTRAINT "reservations_userId_fkey" -FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "reservations" -ADD CONSTRAINT "reservations_classroomId_fkey" -FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "occupancy" -ADD CONSTRAINT "occupancy_userId_fkey" -FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "occupancy" -ADD CONSTRAINT "occupancy_classroomId_fkey" -FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "notification" -ADD CONSTRAINT "notification_userId_fkey" -FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "notification" -ADD CONSTRAINT "notification_reservationId_fkey" -FOREIGN KEY ("reservationId") REFERENCES "reservations"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "profile" -ADD CONSTRAINT "profile_userId_fkey" -FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; \ No newline at end of file diff --git a/backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql b/backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql deleted file mode 100644 index 8954e04..0000000 --- a/backend/prisma/migrations/20250429143650_add_role_enum_and_core_tables/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "occupancy" ALTER COLUMN "updatedAt" DROP DEFAULT; - --- RenameIndex -ALTER INDEX "Occupancy_unique" RENAME TO "occupancy_classroomId_startTime_endTime_userId_key"; diff --git a/backend/prisma/migrations/20250501203806_init_corrected/migration.sql b/backend/prisma/migrations/20250501203806_init_corrected/migration.sql deleted file mode 100644 index 849f954..0000000 --- a/backend/prisma/migrations/20250501203806_init_corrected/migration.sql +++ /dev/null @@ -1,78 +0,0 @@ -/* - Warnings: - - - You are about to drop the `reservations` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `users` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "notification" DROP CONSTRAINT "notification_reservationId_fkey"; - --- DropForeignKey -ALTER TABLE "notification" DROP CONSTRAINT "notification_userId_fkey"; - --- DropForeignKey -ALTER TABLE "occupancy" DROP CONSTRAINT "occupancy_userId_fkey"; - --- DropForeignKey -ALTER TABLE "profile" DROP CONSTRAINT "profile_userId_fkey"; - --- DropForeignKey -ALTER TABLE "reservations" DROP CONSTRAINT "reservations_classroomId_fkey"; - --- DropForeignKey -ALTER TABLE "reservations" DROP CONSTRAINT "reservations_userId_fkey"; - --- DropTable -DROP TABLE "reservations"; - --- DropTable -DROP TABLE "users"; - --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "username" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "role" "Role" NOT NULL DEFAULT 'STUDENT', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Reservation" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "classroomId" INTEGER NOT NULL, - "startTime" TIMESTAMP(3) NOT NULL, - "endTime" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Reservation_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- AddForeignKey -ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_classroomId_fkey" FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "occupancy" ADD CONSTRAINT "occupancy_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "notification" ADD CONSTRAINT "notification_reservationId_fkey" FOREIGN KEY ("reservationId") REFERENCES "Reservation"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "profile" ADD CONSTRAINT "profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20250502190105_init/migration.sql b/backend/prisma/migrations/20250502190105_init/migration.sql new file mode 100644 index 0000000..b57f6d5 --- /dev/null +++ b/backend/prisma/migrations/20250502190105_init/migration.sql @@ -0,0 +1,140 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('STUDENT', 'REPRESENTATIVE', 'ADMIN', 'TEACHER'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'STUDENT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "buildings" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "status" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "buildings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "floors" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "buildingId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "floors_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "classrooms" ( + "id" SERIAL NOT NULL, + "floorId" INTEGER NOT NULL, + "buildingId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "classrooms_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Reservation" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "classroomId" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Reservation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "occupancy" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "classroomId" INTEGER NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "status" TEXT NOT NULL DEFAULT 'occupied', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "occupancy_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "notification" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "reservationId" INTEGER, + "message" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'INFO', + "isRead" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "occupancyId" INTEGER NOT NULL, + + CONSTRAINT "notification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "profile" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "phone" TEXT, + "address" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "profile_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "occupancy_classroomId_startTime_endTime_userId_key" ON "occupancy"("classroomId", "startTime", "endTime", "userId"); + +-- AddForeignKey +ALTER TABLE "floors" ADD CONSTRAINT "floors_buildingId_fkey" FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_floorId_fkey" FOREIGN KEY ("floorId") REFERENCES "floors"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "classrooms" ADD CONSTRAINT "classrooms_buildingId_fkey" FOREIGN KEY ("buildingId") REFERENCES "buildings"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reservation" ADD CONSTRAINT "Reservation_classroomId_fkey" FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "occupancy" ADD CONSTRAINT "occupancy_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "occupancy" ADD CONSTRAINT "occupancy_classroomId_fkey" FOREIGN KEY ("classroomId") REFERENCES "classrooms"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification" ADD CONSTRAINT "notification_reservationId_fkey" FOREIGN KEY ("reservationId") REFERENCES "Reservation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notification" ADD CONSTRAINT "notification_occupancyId_fkey" FOREIGN KEY ("occupancyId") REFERENCES "occupancy"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profile" ADD CONSTRAINT "profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index b4c579a..e028b0e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -15,99 +15,108 @@ enum Role { } model User { - id Int @id @default(autoincrement()) - username String @unique - email String @unique - password String - role Role @default(STUDENT) - createdAt DateTime @default(now()) - profiles Profile[] - reservations Reservation[] - occupancies Occupancy[] + id Int @id @default(autoincrement()) + username String @unique + email String @unique + password String + role Role @default(STUDENT) + createdAt DateTime @default(now()) + profiles Profile[] + reservations Reservation[] + occupancies Occupancy[] notifications Notification[] } model Building { - id Int @id @default(autoincrement()) - name String - floors Floor[] - classrooms Classroom[] - status String? - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + name String + floors Floor[] + classrooms Classroom[] + status String? + createdAt DateTime @default(now()) + @@map("buildings") } model Floor { - id Int @id @default(autoincrement()) - name String - buildingId Int - building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade, onUpdate: Cascade) - classrooms Classroom[] - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + name String + buildingId Int + building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade, onUpdate: Cascade) + classrooms Classroom[] + createdAt DateTime @default(now()) + @@map("floors") } model Classroom { - id Int @id @default(autoincrement()) - floorId Int - floor Floor @relation(fields: [floorId], references: [id], onDelete: Cascade, onUpdate: Cascade) - buildingId Int - building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade, onUpdate: Cascade) - name String - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + floorId Int + floor Floor @relation(fields: [floorId], references: [id], onDelete: Cascade, onUpdate: Cascade) + buildingId Int + building Building @relation(fields: [buildingId], references: [id], onDelete: Cascade, onUpdate: Cascade) + name String + createdAt DateTime @default(now()) reservations Reservation[] occupancies Occupancy[] + @@map("classrooms") } model Reservation { - id Int @id @default(autoincrement()) - userId Int - classroomId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) - startTime DateTime - endTime DateTime - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + userId Int + classroomId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) + startTime DateTime + endTime DateTime + createdAt DateTime @default(now()) notifications Notification[] } model Occupancy { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) classroomId Int - classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) + classroom Classroom @relation(fields: [classroomId], references: [id], onDelete: Cascade, onUpdate: Cascade) startTime DateTime endTime DateTime - status String @default("occupied") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + status String @default("occupied") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + notification Notification[] + @@unique([classroomId, startTime, endTime, userId], name: "Occupancy_unique") @@map("occupancy") } model Notification { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) reservationId Int? reservation Reservation? @relation(fields: [reservationId], references: [id], onDelete: Cascade, onUpdate: Cascade) message String - type String @default("INFO") - isRead Boolean @default(false) - createdAt DateTime @default(now()) + type String @default("INFO") + isRead Boolean @default(false) + createdAt DateTime @default(now()) + occupancy Occupancy @relation(fields: [occupancyId], references: [id]) + occupancyId Int + @@map("notification") } model Profile { - id Int @id @default(autoincrement()) - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - firstName String - lastName String - phone String? - address String? - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + firstName String + lastName String + phone String? + address String? + createdAt DateTime @default(now()) + @@map("profile") -} \ No newline at end of file +} diff --git a/backend/scripts/triggerNotification.js b/backend/scripts/triggerNotification.js new file mode 100644 index 0000000..859b955 --- /dev/null +++ b/backend/scripts/triggerNotification.js @@ -0,0 +1,74 @@ +const { PrismaClient } = require('@prisma/client'); +const { DateTime } = require('luxon'); +const cron = require('node-cron'); + +const prisma = new PrismaClient(); + +// Utility: Sleep function to avoid hammering DB +const sleepInMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function triggerNotification() { + const now = DateTime.utc(); + const batchSize = 10; + let hasMore = true; + + try { + while (hasMore) { + const occupancies = await prisma.occupancy.findMany({ + where: { + startTime: { + lte: now.toJSDate(), + }, + status: 'occupied', + // Ensure no notification has been sent yet + notifications: { + none: {}, + }, + }, + include: { + classroom: true, + user: { select: { id: true, username: true } }, + }, + take: batchSize, + orderBy: { startTime: 'asc' }, + }); + + if (occupancies.length === 0) { + console.log('No occupancies to notify.'); + break; + } + + for (const occupancy of occupancies) { + await prisma.$transaction(async (tx) => { + await tx.notification.create({ + data: { + userId: occupancy.userId, + message: `Your occupancy for classroom ${occupancy.classroom.name} has started.`, + }, + }); + console.log(`Sent notification for occupancy ${occupancy.id}.`); + }); + } + + if (occupancies.length < batchSize) { + hasMore = false; + } else { + await sleepInMs(200); + } + } + + console.log('🎉 Finished notification processing at:', new Date().toISOString()); + } catch (error) { + console.error('Error during notification triggering:', error); + } finally { + await prisma.$disconnect(); + } +} + +// Run every 5 minutes +cron.schedule('*/5 * * * *', async () => { + console.log('\nNotification cron job triggered at:', new Date().toISOString()); + await triggerNotification(); +}); + +module.exports = triggerNotification; \ No newline at end of file