diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..38e14ba --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main, abhishek] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + client/package-lock.json + server/package-lock.json + socket/package-lock.json + + - name: Install + run: | + npm ci + npm --prefix client ci + npm --prefix server ci + npm --prefix socket ci + + - name: Lint + run: npm run lint + + - name: Build client + # CI=true in GitHub Actions makes react-scripts treat ESLint warnings as errors. + run: npm run build diff --git a/.gitignore b/.gitignore index efb969f..cb38da9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,175 +1,27 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock -# public - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories +# Dependencies node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files +# Environment files (copy the .env.example in each app instead) .env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ +.env.* +!.env.example -# FuseBox cache -.fusebox/ +# Build output +client/build/ -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# React -.DS_STORE -node_modules -scripts/flow/*/.flowconfig -.flowconfig -*~ -*.pyc -.grunt -_SpecRunner.html -__benchmarks__ -build/ -remote-repo/ +# Coverage coverage/ -.module-cache -fixtures/dom/public/react-dom.js -fixtures/dom/public/react.js -test/the-files-to-test.generated.js -*.log* -chrome-user-data -*.sublime-project -*.sublime-workspace -.idea -*.iml -.vscode + +# Editor / OS +.DS_Store +.idea/ +.vscode/ *.swp *.swo - -packages/react-devtools-core/dist -packages/react-devtools-extensions/chrome/build -packages/react-devtools-extensions/chrome/*.crx -packages/react-devtools-extensions/chrome/*.pem -packages/react-devtools-extensions/firefox/build -packages/react-devtools-extensions/firefox/*.xpi -packages/react-devtools-extensions/firefox/*.pem -packages/react-devtools-extensions/shared/build -packages/react-devtools-extensions/.tempUserDataDir -packages/react-devtools-inline/dist -packages/react-devtools-shell/dist -packages/react-devtools-timeline/dist -packages/react-devtools-standalone/build -packages/react-devtools-standalone/*.zip -packages/react-devtools-standalone/*.pem -packages/react-devtools-standalone/*.p12 diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..351b29f --- /dev/null +++ b/.mailmap @@ -0,0 +1,8 @@ +# Consolidates author identities for `git shortlog` / `git log --use-mailmap`. +# Does not rewrite history. +Shrish Raj Gupta ShrishRajGupta +Shrish Raj Gupta Shrish Raj Gupta <97391054+ShrishRajGupta@users.noreply.github.com> +Shrish Raj Gupta shrish-browserstack +Shreyansh Jaiswal Shreyansh +Shreyansh Jaiswal Shreyansh Jaiswal <97983172+CodeBuster09@users.noreply.github.com> +Abhishek Yadav Abhishek diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/README.md b/README.md index 3552986..6de53aa 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,138 @@ -
- +
+ # Workplace ![Workplace](/client/public/icons8-job-64.png) + + A job portal where employers post roles and job seekers apply, chat in real time, and build a resume — all in one place. + + ![](https://img.shields.io/badge/Maintained-Yes-indigo) + ![](https://img.shields.io/github/stars/ShrishRajGupta/Workplace.svg) + ![](https://img.shields.io/github/issues/ShrishRajGupta/Workplace) + ![](https://img.shields.io/github/last-commit/ShrishRajGupta/Workplace) +
## Table of Contents -
- Expand -
    - -- [Features](#Features) -- [Images](#Images) -- [TechStack](#TechStack) -- [Installation](#installation) -- [Maintainers](#Maintainers) -- [Technologies](#technologies) + +- [Features](#features) +- [Screenshots](#screenshots) +- [Tech Stack](#tech-stack) +- [Architecture](#architecture) +- [Getting Started](#getting-started) +- [Scripts](#scripts) +- [Maintainers](#maintainers) - [License](#license) - [Contributing](#contributing) -
-
- -## Images -
-Profile page -Demo img -Resume Builder - - -
- ## Features - Job posting and management for employers - Real-time chat between employers and job seekers -- Real-time notifications for job applications and **critical email** updates +- Email notifications for sign-up and account events - Inbuilt resume builder for job seekers - Job searching and application for job seekers -- Profile management for both employers and job seekers - - - - - -![](https://img.shields.io/badge/Maintained-Yes-indigo) -![](https://img.shields.io/github/stars/ShrishRajGupta/Workplace.svg) -![](https://img.shields.io/github/issues/ShrishRajGupta/Workplace) -![](https://img.shields.io/github/last-commit/ShrishRajGupta/Workplace) - -#### [Documentation](https://github.com/ShrishRajGupta/Workplace/blob/abhishek/README.md) -#### [Request Feature](https://github.com/ShrishRajGupta/Workplace/issues) - - - -## TechStack -👾 -
- Client - - - -
- -
- Server - -
- -
-Database - - - -
-
- -## Requirements -[Workplace](https://github.com/ShrishRajGupta/Workplace) is a NodeJS application based. - -For codebase use the package manager [npm](https://www.npmjs.com/) to install [Workplace](https://github.com/ShrishRajGupta/Workplace). - -
- -## Installation +- Profile management (education, work experience, skills, photo) for both roles + +## Screenshots + +
+Profile page +Chat +Resume Builder +Login page +
+ +## Tech Stack + +| Layer | Technology | +|---|---| +| Client | [React](https://reactjs.org/) (Create React App), [Material UI](https://mui.com/), [socket.io-client](https://socket.io/) | +| API server | [Node.js](https://nodejs.org/en/), [Express](https://expressjs.com/), [Mongoose](https://mongoosejs.com/), JWT auth, [Nodemailer](https://nodemailer.com/) | +| Socket server | [socket.io](https://socket.io/) | +| Data | [MongoDB](https://www.mongodb.com/), [Cloudinary](https://cloudinary.com) (image storage) | + +## Architecture + +Workplace runs as three processes. In development they all run from one command (see below). + +| Directory | Process | Default port | Purpose | +|---|---|---|---| +| `client/` | CRA dev server | 3000 | React UI. API calls are proxied to the server via the `proxy` field in `client/package.json`. | +| `server/` | Express | 3001 | REST API, auth, MongoDB, Cloudinary uploads, email. | +| `socket/` | socket.io | 8900 | Real-time chat delivery. Chat history itself is persisted through the REST API. | + +All three must be running for the full app to work. Without `socket/`, everything except live chat still works. + +## Getting Started + +### Prerequisites + +- Node.js 20+ (see `.nvmrc`; `nvm use` picks it up) +- A MongoDB instance (local, or a free [Atlas](https://www.mongodb.com/atlas) cluster) +- A [Cloudinary](https://cloudinary.com/) account (profile photos) +- A Gmail account with an [App Password](https://support.google.com/accounts/answer/185833) (transactional email) + +### 1. Clone and install + ```bash git clone https://github.com/ShrishRajGupta/Workplace.git -npm install +cd Workplace +npm run install:all ``` -### Setup Environment Variables -Create a .env file in the root directory and add the following variables: +### 2. Configure environment + +Each app reads its own `.env` from its own directory. Copy the examples and fill them in: ```bash -ACCESS_TOKEN # Secret key for JWT +cp server/.env.example server/.env # MONGO_URL, ACCESS_TOKEN, Cloudinary, Gmail +cp socket/.env.example socket/.env # PORT, CLIENT_URL (defaults work for local dev) +cp client/.env.example client/.env # REACT_APP_PUBLIC_FOLDER (optional) +``` -CLOUDINARY_APIKEY # Cloudinary API Key -CLOUDINARY_APISECRET # Cloudinary API Secret -CLOUDINARY_CLOUDNAME # Cloudinary Cloud Name +The comments in each `.env.example` explain every variable. -MONGO_URL # MongoDB URL -GMAIL_USER # Gmail Email -GMAIL_PASS # Gmail Password +### 3. Run -``` -#### Setup Cloudinary -- Create an account on [Cloudinary](https://cloudinary.com/) -- Get the API Key, API Secret, and Cloud Name -- Add the API Key, API Secret, and Cloud Name to the .env file - -#### Setup MongoDB -- Create an account on [MongoDB](https://www.mongodb.com/) -- Create a new cluster -- Get the MongoDB URL -- Add the MongoDB URL to the .env file - -#### Setup Client ```bash -cd client -npm install -npm start -``` -### Start the Server -```bash -cd server -npm install -npm start +npm run dev ``` -## Maintainers 👨‍💻 -
-

+This starts the client, server and socket server together with colour-coded logs. Open http://localhost:3000. + +To run a single app instead, `cd` into it and use `npm start` (client) or `npm run dev` (server, socket). + +## Scripts + +Run from the repository root: + +| Script | What it does | +|---|---| +| `npm run install:all` | Installs dependencies for the root, `client`, `server` and `socket` | +| `npm run dev` | Starts all three apps with hot reload | +| `npm run build` | Production build of the client into `client/build/` | +| `npm run lint` | ESLint over the client, syntax check over the server and socket | + +CI runs `lint` and `build` on every push and pull request (`.github/workflows/ci.yml`). + +## Maintainers + +

-
-- ### [Shrish Raj Gupta](https://github.com/ShrishRajGupta) [](https://www.linkedin.com/in/shrishrajgupta/) -- ### [Abhishek Yadav](https://github.com/AbhishekYMNNIT) [](https://www.linkedin.com/in/abhishekyadav123/) -- ### [Shreyansh Jaiswal](https://github.com/CodeBuster09) []( https://www.linkedin.com/in/shreyanshjaiswal09/) +- [Shrish Raj Gupta](https://github.com/ShrishRajGupta) [](https://www.linkedin.com/in/shrishrajgupta/) +- [Abhishek Yadav](https://github.com/AbhishekYMNNIT) [](https://www.linkedin.com/in/abhishekyadav123/) +- [Shreyansh Jaiswal](https://github.com/CodeBuster09) [](https://www.linkedin.com/in/shreyanshjaiswal09/) -## Licence 🍁 -### [**MIT**](/LICENSE) © [Shrish Raj Gupta](https://github.com/ShrishRajGupta) +## License -## Contributing 💙 +[MIT](/LICENSE) © [Shrish Raj Gupta](https://github.com/ShrishRajGupta) -PR's are welcome !Found a Bug ? +## Contributing -Create an [Issue](https://github.com/ShrishRajGupta/Workplace/issues). +PRs are welcome. Found a bug or want a feature? Open an [issue](https://github.com/ShrishRajGupta/Workplace/issues). -## 💖 Like this project ? +If you like this project, leave a ⭐. -Leave a ⭐ If you think this project is cool. -

mona whisper

+

mona whisper

diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..81fa74d --- /dev/null +++ b/client/.env.example @@ -0,0 +1,8 @@ +# Copy to client/.env. Create React App only exposes variables prefixed REACT_APP_. + +# API origin. Leave empty in development: calls go through the CRA proxy +# ("proxy" in package.json -> http://localhost:3001) so the auth cookie is same-origin. +REACT_APP_API_URL= + +# Socket server for real-time chat +REACT_APP_SOCKET_URL=ws://localhost:8900 diff --git a/client/README.md b/client/README.md index 58beeac..e31f762 100644 --- a/client/README.md +++ b/client/README.md @@ -1,70 +1,43 @@ -# Getting Started with Create React App +# Workplace — client -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +React (Create React App) front end for [Workplace](../README.md). See the root README for the full setup; this file only covers the client. -## Available Scripts +## Run -In the project directory, you can run: +```bash +cp .env.example .env # optional, see below +npm install +npm start # http://localhost:3000 +``` -### `npm start` +API requests are proxied to the Express server on `http://localhost:3001` through the `proxy` field in `package.json`, so the server must be running. Real-time chat connects to the socket server on `ws://localhost:8900`. -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +## Scripts -The page will reload when you make changes.\ -You may also see any lint errors in the console. +| Script | What it does | +|---|---| +| `npm start` | Dev server with hot reload | +| `npm run build` | Production build into `build/` (gitignored) | +| `npm run lint` | ESLint over `src/` using the CRA config | -### `npm test` +## Environment -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +Only variables prefixed `REACT_APP_` reach the browser. See `.env.example`. -### `npm run build` +## Layout -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +``` +src/ +├── App.js # route tree (public, anonymous-only, protected + layout) +├── api/ # one axios instance (client.js) + per-domain calls (auth, users, posts, chat) +├── context/ # AuthContext: session user, login/register/logout, refreshUser +├── routes/ # ProtectedRoute / AnonymousRoute guards, AppLayout (navbar), NotFound +├── components/ # screens and feature components +├── widgets/ # home feed widgets +├── pages/messenger/ # real-time chat +├── utils/ # logger, avatar fallback +└── css/ # global stylesheets +``` -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can't go back!** - -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. - -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +All server calls go through `src/api/`; components never import axios directly. The auth cookie is +httpOnly and is sent automatically (same-origin via the proxy, or `withCredentials` cross-origin). diff --git a/client/package-lock.json b/client/package-lock.json index f467b5e..85a8cac 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,27 +16,21 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.1", - "bootstrap": "^5.3.3", - "bootstrap-3-card": "^0.2.0", - "js-cookie": "^3.0.5", - "link": "^2.1.0", - "query-string": "^8.1.0", "react": "^18.2.0", - "react-bootstrap": "^2.10.2", "react-dom": "^18.2.0", "react-feather": "^2.0.9", "react-hot-toast": "^2.4.1", - "react-redux": "^8.1.3", - "react-router": "^6.18.0", "react-router-dom": "^6.18.0", "react-scripts": "5.0.1", - "react-scroll-to-bottom": "^4.2.0", - "react-spinners": "^0.13.8", "react-to-print": "^2.14.4", "socket.io-client": "^4.7.2", - "timeago": "^1.6.7", - "timeago.js": "^4.0.2", - "web-vitals": "^2.1.4" + "timeago.js": "^4.0.2" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + }, + "engines": { + "node": ">=20" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -652,9 +646,18 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -1897,6 +1900,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1971,18 +1986,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.4.tgz", - "integrity": "sha512-zQyB4MJGM+rvd4pM58n26kf3xbiitw9MHzL8oLiBMKb8MCtVDfV5nDzzJWWzLMtbvKI9wN6XwJYl479qF4JluQ==", - "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -2358,26 +2361,6 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/css": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.1.3.tgz", - "integrity": "sha512-RSQP59qtCNTf5NWD6xM08xsQdCZmVYnX/panPYvB6LQAPKQB6GL49Njf0EMbS3CyDtrlWsBcmqBtysFvfWT3rA==", - "dependencies": { - "@emotion/babel-plugin": "^11.0.0", - "@emotion/cache": "^11.1.3", - "@emotion/serialize": "^1.0.0", - "@emotion/sheet": "^1.0.0", - "@emotion/utils": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - } - } - }, "node_modules/@emotion/hash": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", @@ -3755,20 +3738,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@react-aria/ssr": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz", - "integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" - } - }, "node_modules/@remix-run/router": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", @@ -3777,45 +3746,6 @@ "node": ">=14.0.0" } }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@restart/ui": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.9.tgz", - "integrity": "sha512-mUbygUsJcRurjZCt1f77gg4DpheD1D+Sc7J3JjAkysUj7t8m4EBJVOqWC9788Qtbc69cJ+HlJc6jBguKwS8Mcw==", - "dependencies": { - "@babel/runtime": "^7.21.0", - "@popperjs/core": "^2.11.6", - "@react-aria/ssr": "^3.5.0", - "@restart/hooks": "^0.4.9", - "@types/warning": "^3.0.0", - "dequal": "^2.0.3", - "dom-helpers": "^5.2.0", - "uncontrollable": "^8.0.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - } - }, - "node_modules/@restart/ui/node_modules/uncontrollable": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", - "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", - "peerDependencies": { - "react": ">=16.14.0" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4139,14 +4069,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@swc/helpers": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", - "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", @@ -4594,15 +4516,6 @@ "@types/node": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5041,16 +4954,6 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.5.tgz", "integrity": "sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA==" }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" - }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" - }, "node_modules/@types/ws": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", @@ -6344,37 +6247,6 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" }, - "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/bootstrap-3-card": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bootstrap-3-card/-/bootstrap-3-card-0.2.0.tgz", - "integrity": "sha512-1IaGxhGlZIx1P2wodzU2NA3BemJC6zBvd/7jK58EMYiI1fx+Sun00aePnSsF1gj+dti08FAuO3ZKGlzQdLADIA==", - "dependencies": { - "bootstrap-sass": "3.x" - } - }, - "node_modules/bootstrap-sass": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", - "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6640,11 +6512,6 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, - "node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, "node_modules/clean-css": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", @@ -7377,14 +7244,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, - "node_modules/decode-uri-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", - "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", - "engines": { - "node": ">=14.16" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -8977,17 +8836,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -10073,14 +9921,6 @@ "node": ">= 0.4" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -12609,19 +12449,6 @@ "jiti": "bin/jiti.js" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "engines": { - "node": ">=14" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12876,17 +12703,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/link": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/link/-/link-2.1.0.tgz", - "integrity": "sha512-6jgX7ejPBOQaKsFY/9aFEg0HW0JyFuMGDoN+KQX1W94t+8Fi5xwlSlGouRMDqDlrE46drqu4PYduAG7tUwdF7Q==", - "bin": { - "link": "dist/cli.js" - }, - "funding": { - "url": "https://github.com/privatenumber/link?sponsor=1" - } - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -13025,11 +12841,6 @@ "tmpl": "1.0.5" } }, - "node_modules/math-random": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-2.0.1.tgz", - "integrity": "sha512-oIEbWiVDxDpl5tIF4S6zYS9JExhh3bun3uLb3YAinHPTlRtW4g1S66LtJrJ4Npq8dgIa8CLK5iPVah5n4n0s2w==" - }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -15194,23 +15005,6 @@ "react-is": "^16.13.1" } }, - "node_modules/prop-types-extra": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", - "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", - "dependencies": { - "react-is": "^16.3.2", - "warning": "^4.0.0" - }, - "peerDependencies": { - "react": ">=0.14.0" - } - }, - "node_modules/prop-types-extra/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -15277,22 +15071,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", - "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", - "dependencies": { - "decode-uri-component": "^0.4.1", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -15406,40 +15184,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, - "node_modules/react-bootstrap": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.2.tgz", - "integrity": "sha512-UvB7mRqQjivdZNxJNEA2yOQRB7L9N43nBnKc33K47+cH90/ujmnMwatTCwQLu83gLhrzAl8fsa6Lqig/KLghaA==", - "dependencies": { - "@babel/runtime": "^7.22.5", - "@restart/hooks": "^0.4.9", - "@restart/ui": "^1.6.8", - "@types/react-transition-group": "^4.4.6", - "classnames": "^2.3.2", - "dom-helpers": "^5.2.1", - "invariant": "^2.2.4", - "prop-types": "^15.8.1", - "prop-types-extra": "^1.1.0", - "react-transition-group": "^4.4.5", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "@types/react": ">=16.14.8", - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-bootstrap/node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -15605,54 +15349,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, - "node_modules/react-redux": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", - "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", - "dependencies": { - "@babel/runtime": "^7.12.1", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/use-sync-external-store": "^0.0.3", - "hoist-non-react-statics": "^3.3.2", - "react-is": "^18.0.0", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^16.8 || ^17.0 || ^18.0", - "@types/react-dom": "^16.8 || ^17.0 || ^18.0", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0", - "react-native": ">=0.59", - "redux": "^4 || ^5.0.0-beta.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-redux/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" - }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15763,58 +15459,6 @@ } } }, - "node_modules/react-scroll-to-bottom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-scroll-to-bottom/-/react-scroll-to-bottom-4.2.0.tgz", - "integrity": "sha512-1WweuumQc5JLzeAR81ykRdK/cEv9NlCPEm4vSwOGN1qS2qlpGVTyMgdI8Y7ZmaqRmzYBGV5/xPuJQtekYzQFGg==", - "dependencies": { - "@babel/runtime-corejs3": "^7.15.4", - "@emotion/css": "11.1.3", - "classnames": "2.3.1", - "core-js": "3.18.3", - "math-random": "2.0.1", - "prop-types": "15.7.2", - "simple-update-in": "2.2.0" - }, - "peerDependencies": { - "react": ">= 16.8.6" - } - }, - "node_modules/react-scroll-to-bottom/node_modules/core-js": { - "version": "3.18.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz", - "integrity": "sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/react-scroll-to-bottom/node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "node_modules/react-scroll-to-bottom/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/react-spinners": { - "version": "0.13.8", - "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", - "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-to-print": { "version": "2.14.15", "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.14.15.tgz", @@ -16659,11 +16303,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, - "node_modules/simple-update-in": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/simple-update-in/-/simple-update-in-2.2.0.tgz", - "integrity": "sha512-FrW41lLiOs82jKxwq39UrE1HDAHOvirKWk4Nv8tqnFFFknVbTxcHZzDS4vt02qqdU/5+KNsQHWzhKHznDBmrww==" - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -16805,17 +16444,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -17560,14 +17188,6 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, - "node_modules/timeago": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/timeago/-/timeago-1.6.7.tgz", - "integrity": "sha512-FikcjN98+ij0siKH4VO4dZ358PR3oDDq4Vdl1+sN9gWz1/+JXGr3uZbUShYH/hL7bMhcTpPbplJU5Tej4b4jbQ==", - "dependencies": { - "jquery": ">=1.5.0 <4.0" - } - }, "node_modules/timeago.js": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", @@ -17840,20 +17460,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, "node_modules/underscore": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", @@ -17987,14 +17593,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18089,14 +17687,6 @@ "makeerror": "1.0.12" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -18117,11 +17707,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/web-vitals": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" - }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", diff --git a/client/package.json b/client/package.json index 99e5802..057f919 100644 --- a/client/package.json +++ b/client/package.json @@ -12,33 +12,22 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.1", - "bootstrap": "^5.3.3", - "bootstrap-3-card": "^0.2.0", - "js-cookie": "^3.0.5", - "link": "^2.1.0", - "query-string": "^8.1.0", "react": "^18.2.0", - "react-bootstrap": "^2.10.2", "react-dom": "^18.2.0", "react-feather": "^2.0.9", "react-hot-toast": "^2.4.1", - "react-redux": "^8.1.3", - "react-router": "^6.18.0", "react-router-dom": "^6.18.0", "react-scripts": "5.0.1", - "react-scroll-to-bottom": "^4.2.0", - "react-spinners": "^0.13.8", "react-to-print": "^2.14.4", "socket.io-client": "^4.7.2", - "timeago": "^1.6.7", - "timeago.js": "^4.0.2", - "web-vitals": "^2.1.4" + "timeago.js": "^4.0.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "lint": "eslint --max-warnings=0 src --ext .js,.jsx" }, "eslintConfig": { "extends": [ @@ -57,5 +46,11 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" } } diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index d896617..0000000 --- a/client/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -*{ - font-family: Helvetica, sans-serif; -} \ No newline at end of file diff --git a/client/src/App.js b/client/src/App.js index e8e32dc..7a5c536 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,50 +1,51 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; -import JobForm from "./components/jobpostform"; -import Navbar from "./components/navbar"; -import Dashboard from "./components/userdashboard"; +import { Toaster } from "react-hot-toast"; +import Homepage from "./widgets/homepage"; +import Login from "./components/login"; import RegistrationForm from "./components/register"; -import Home from "./components/home"; -import React, { useContext, useEffect, useState } from "react"; import MyForm from "./components/createProfile"; -import Login from "./components/login"; +import Home from "./components/home"; +import JobForm from "./components/jobpostform"; +import Dashboard from "./components/userdashboard"; import Allposts from "./widgets/allposts"; -import Homepage from "./widgets/homepage"; import Messenger from "./pages/messenger/messenger"; -import { AuthContext } from "./context/AuthContext";import Resume from "./components/ResumeBuilder/App"; import ApplyForm from "./components/applyform/applyform"; +import Resume from "./components/ResumeBuilder/App"; +import AppLayout from "./routes/AppLayout"; +import NotFound from "./routes/NotFound"; +import { AnonymousRoute, ProtectedRoute } from "./routes/ProtectedRoute"; function App() { - - const isLogged = localStorage.getItem("isLogged");const {user} = useContext(AuthContext); return (
- - - - } > - }> - }> - :<> }> - :<>}> - :<>}> - :<>}> - :<>}> - :<>}> - :}> - :<>}> - - -
- ); -} + + + } /> + }> + } /> + } /> + -{/* -Comment added by Shreyansh: - 1. Add twitterlogo, linkedinlogo in UserWidget.jsx - 2. Userwidget is added on homepage, pls correct if something bothers.. - */ + }> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } /> + + + +
+ ); } export default App; - diff --git a/client/src/api/auth.js b/client/src/api/auth.js new file mode 100644 index 0000000..4bbf965 --- /dev/null +++ b/client/src/api/auth.js @@ -0,0 +1,8 @@ +import api from "./client"; + +// All auth calls return the user object from the server envelope { success, message, user }. +export const login = (credentials) => api.post("/user/login", credentials).then((r) => r.data.user); +export const register = (details) => api.post("/user/register", details).then((r) => r.data.user); +export const logout = () => api.get("/user/logout"); +export const fetchMe = () => api.get("/user/profile").then((r) => r.data.user); +export const sendWelcomeEmail = () => api.post("/email/intro"); diff --git a/client/src/api/chat.js b/client/src/api/chat.js new file mode 100644 index 0000000..cb5fb09 --- /dev/null +++ b/client/src/api/chat.js @@ -0,0 +1,9 @@ +import api from "./client"; + +export const getConversations = (userId) => api.get(`/conversations/${userId}`).then((r) => r.data); +export const findConversation = (firstUserId, secondUserId) => + api.get(`/conversations/find/${firstUserId}/${secondUserId}`).then((r) => r.data); +export const createConversation = (receiverId) => api.post("/conversations", { receiverId }).then((r) => r.data); +export const getMessages = (conversationId) => api.get(`/messages/${conversationId}`).then((r) => r.data); +export const sendMessage = ({ conversationId, text }) => + api.post("/messages", { conversationId, text }).then((r) => r.data); diff --git a/client/src/api/client.js b/client/src/api/client.js new file mode 100644 index 0000000..64fbabb --- /dev/null +++ b/client/src/api/client.js @@ -0,0 +1,31 @@ +import axios from "axios"; + +// Single axios instance for the whole app. +// - baseURL is empty in development so calls go through the CRA proxy (client/package.json); +// set REACT_APP_API_URL when the API is served from another origin. +// - withCredentials sends the httpOnly auth cookie on cross-origin calls. +const api = axios.create({ + baseURL: process.env.REACT_APP_API_URL || "", + withCredentials: true, +}); + +let onUnauthorized = null; + +// AuthContext registers a callback so an expired/invalid session logs the user out everywhere. +export const setUnauthorizedHandler = (handler) => { + onUnauthorized = handler; +}; + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401 && onUnauthorized) onUnauthorized(); + return Promise.reject(error); + } +); + +// Human-readable message from an axios error (server envelope first, then axios/network). +export const getErrorMessage = (error, fallback = "Something went wrong") => + error?.response?.data?.message || error?.message || fallback; + +export default api; diff --git a/client/src/api/posts.js b/client/src/api/posts.js new file mode 100644 index 0000000..49b3727 --- /dev/null +++ b/client/src/api/posts.js @@ -0,0 +1,6 @@ +import api from "./client"; + +export const getAllPosts = () => api.get("/home").then((r) => r.data.posts); +export const getMyPosts = () => api.get("/user/allposts").then((r) => r.data.allposts); +export const getUserPosts = (userId) => api.get(`/user/allposts/${userId}`).then((r) => r.data.allposts); +export const createPost = (details) => api.post("/user/jobpostform", details).then((r) => r.data.post); diff --git a/client/src/api/users.js b/client/src/api/users.js new file mode 100644 index 0000000..8e1952b --- /dev/null +++ b/client/src/api/users.js @@ -0,0 +1,22 @@ +import api from "./client"; + +export const getUser = (userId) => api.get(`/user/profile/${userId}`).then((r) => r.data.user); +export const getFriends = (userId) => api.get(`/user/friends/${userId}`).then((r) => r.data); +export const searchUsers = (term) => api.get(`/search/${encodeURIComponent(term)}`).then((r) => r.data.user); + +export const sendConnectionRequest = (userId) => api.get(`/user/profile/${userId}/connect`).then((r) => r.data); +// Responds to a friend request; returns the updated current user. +export const respondToRequest = ({ from, to, requestId, action }) => + api.put(`/user/connect/${from}/${to}/${action}/${requestId}`).then((r) => r.data.user); + +export const createProfile = (details) => api.post("/user/createProfile", details).then((r) => r.data.user); +export const updateInfo = ({ name, about }) => api.post("/in/update", { name, about }).then((r) => r.data); +export const addEducation = (entry) => api.post("/in/addCollege", entry).then((r) => r.data.education); +export const addWorkExperience = (entry) => api.post("/in/addWorkEx", entry).then((r) => r.data.workexperience); +export const addSkill = (entry) => api.post("/in/addSkills", entry).then((r) => r.data.skills); + +export const uploadPhoto = (file) => { + const formData = new FormData(); + formData.append("photo", file); + return api.post("/in/add", formData).then((r) => r.data.url); +}; diff --git a/client/src/apiCalls.js b/client/src/apiCalls.js deleted file mode 100644 index 1000bc3..0000000 --- a/client/src/apiCalls.js +++ /dev/null @@ -1,12 +0,0 @@ -import axios from "axios"; - -export const loginCall = async (userCredential, dispatch) => { - dispatch({ type: "LOGIN_START" }); - try { - const res = await axios.post("/user/login", userCredential); - dispatch({ type: "LOGIN_SUCCESS", payload: res.data }); - } catch (err) { - dispatch({ type: "LOGIN_FAILURE", payload: err }); - } -}; - diff --git a/client/src/components/FlexBetween.jsx b/client/src/components/FlexBetween.jsx deleted file mode 100644 index e07ac48..0000000 --- a/client/src/components/FlexBetween.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import {Box} from "@mui/material"; -import {styled} from "@mui/system"; - -const FlexBetween = styled(Box)({ - display: "flex", - justifyContent: "space-between", - alignItems:"center", - backgroundColor:"#83A2FF" -}); - -export default FlexBetween; \ No newline at end of file diff --git a/client/src/components/ResumeBuilder/components/Body/Body.js b/client/src/components/ResumeBuilder/components/Body/Body.js index 8e4bacf..5080eef 100644 --- a/client/src/components/ResumeBuilder/components/Body/Body.js +++ b/client/src/components/ResumeBuilder/components/Body/Body.js @@ -7,17 +7,18 @@ import Resume from "../Resume/Resume"; import styles from "./Body.module.css"; +const colors = ["#239ce2", "#48bb78", "#0bc5ea", "#a0aec0", "#ed8936"]; +const sections = { + basicInfo: "Basic Info", + workExp: "Work Experience", + project: "Projects", + education: "Education", + achievement: "Achievements", + summary: "Summary", + other: "Other", +}; + function Body() { - const colors = ["#239ce2", "#48bb78", "#0bc5ea", "#a0aec0", "#ed8936"]; - const sections = { - basicInfo: "Basic Info", - workExp: "Work Experience", - project: "Projects", - education: "Education", - achievement: "Achievements", - summary: "Summary", - other: "Other", - }; const resumeRef = useRef(); const [activeColor, setActiveColor] = useState(colors[0]); diff --git a/client/src/components/ResumeBuilder/components/Editor/Editor.js b/client/src/components/ResumeBuilder/components/Editor/Editor.js index 62be829..5e7e151 100644 --- a/client/src/components/ResumeBuilder/components/Editor/Editor.js +++ b/client/src/components/ResumeBuilder/components/Editor/Editor.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { X } from "react-feather"; import InputControl from "../InputControl/InputControl"; @@ -8,6 +8,9 @@ import styles from "./Editor.module.css"; function Editor(props) { const sections = props.sections; const information = props.information; + // Latest information for effects that must only re-run on a section change. + const informationRef = useRef(information); + informationRef.current = information; const [activeSectionKey, setActiveSectionKey] = useState( Object.keys(sections)[0] @@ -478,6 +481,8 @@ function Editor(props) { })); break; } + default: + break; } }; @@ -516,7 +521,7 @@ function Editor(props) { }; useEffect(() => { - const activeInfo = information[sections[activeSectionKey]]; + const activeInfo = informationRef.current[sections[activeSectionKey]]; setActiveInformation(activeInfo); setSectionTitle(sections[activeSectionKey]); setActiveDetailIndex(0); @@ -561,33 +566,32 @@ function Editor(props) { summary: typeof activeInfo?.detail !== "object" ? activeInfo.detail : "", other: typeof activeInfo?.detail !== "object" ? activeInfo.detail : "", }); - }, [activeSectionKey]); + }, [activeSectionKey, sections]); useEffect(() => { setActiveInformation(information[sections[activeSectionKey]]); - }, [information]); + }, [information, sections, activeSectionKey]); + // Load the selected detail (work experience #n, project #n, ...) into the form. useEffect(() => { - const details = activeInformation?.details; - if (!details) return; - const activeInfo = information[sections[activeSectionKey]]; + if (!activeInfo?.details) return; + const detail = activeInfo.details[activeDetailIndex]; setValues({ - overview: activeInfo.details[activeDetailIndex]?.overview || "", - link: activeInfo.details[activeDetailIndex]?.link || "", - certificationLink: - activeInfo.details[activeDetailIndex]?.certificationLink || "", - companyName: activeInfo.details[activeDetailIndex]?.companyName || "", - location: activeInfo.details[activeDetailIndex]?.location || "", - startDate: activeInfo.details[activeDetailIndex]?.startDate || "", - endDate: activeInfo.details[activeDetailIndex]?.endDate || "", - points: activeInfo.details[activeDetailIndex]?.points || "", - title: activeInfo.details[activeDetailIndex]?.title || "", - linkedin: activeInfo.details[activeDetailIndex]?.linkedin || "", - github: activeInfo.details[activeDetailIndex]?.github || "", - college: activeInfo.details[activeDetailIndex]?.college || "", + overview: detail?.overview || "", + link: detail?.link || "", + certificationLink: detail?.certificationLink || "", + companyName: detail?.companyName || "", + location: detail?.location || "", + startDate: detail?.startDate || "", + endDate: detail?.endDate || "", + points: detail?.points || "", + title: detail?.title || "", + linkedin: detail?.linkedin || "", + github: detail?.github || "", + college: detail?.college || "", }); - }, [activeDetailIndex]); + }, [activeDetailIndex, activeSectionKey, information, sections]); return (
diff --git a/client/src/components/ResumeBuilder/components/Resume/Resume.js b/client/src/components/ResumeBuilder/components/Resume/Resume.js index d7a619b..94c3281 100644 --- a/client/src/components/ResumeBuilder/components/Resume/Resume.js +++ b/client/src/components/ResumeBuilder/components/Resume/Resume.js @@ -1,4 +1,5 @@ -import React, { forwardRef, useEffect, useRef, useState } from "react"; +import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react"; + import { AtSign, Calendar, @@ -11,6 +12,9 @@ import { import styles from "./Resume.module.css"; +// User-entered profile links may omit the protocol. +const externalHref = (value) => (/^https?:\/\//i.test(value) ? value : `https://${value}`); + const Resume = forwardRef((props, ref) => { const information = props.information; const sections = props.sections; @@ -19,6 +23,9 @@ const Resume = forwardRef((props, ref) => { const [columns, setColumns] = useState([[], []]); const [source, setSource] = useState(""); const [target, seTarget] = useState(""); + // The drop target is read when the drag ends, without re-running the swap on every hover. + const targetRef = useRef(target); + targetRef.current = target; const info = { workExp: information[sections.workExp], @@ -258,8 +265,9 @@ const Resume = forwardRef((props, ref) => { ), }; - const swapSourceTarget = (source, target) => { + const swapSourceTarget = useCallback((source, target) => { if (!source || !target) return; + setColumns((columns) => { const tempColumns = [[...columns[0]], [...columns[1]]]; let sourceRowIndex = tempColumns[0].findIndex((item) => item === source); @@ -282,19 +290,24 @@ const Resume = forwardRef((props, ref) => { tempColumns[targetColumnIndex][targetRowIndex] = tempSource; - setColumns(tempColumns); - }; + return tempColumns; + }); + }, []); useEffect(() => { setColumns([ [sections.project, sections.education, sections.summary], [sections.workExp, sections.achievement, sections.other], ]); - }, []); + }, [sections]); + // A drag has ended: swap the dragged section with the last hovered one, then reset so the + // same section can be dragged again. useEffect(() => { - swapSourceTarget(source, target); - }, [source]); + if (!source) return; + swapSourceTarget(source, targetRef.current); + setSource(""); + }, [source, swapSourceTarget]); useEffect(() => { const container = containerRef.current; @@ -312,28 +325,28 @@ const Resume = forwardRef((props, ref) => {
{info.basicInfo?.detail?.email ? ( - + {info.basicInfo?.detail?.email} ) : ( )} {info.basicInfo?.detail?.phone ? ( - + {info.basicInfo?.detail?.phone} ) : ( )} {info.basicInfo?.detail?.linkedin ? ( - + {info.basicInfo?.detail?.linkedin} ) : ( )} {info.basicInfo?.detail?.github ? ( - + {info.basicInfo?.detail?.github} ) : ( diff --git a/client/src/components/ResumeBuilder/resume.jsx b/client/src/components/ResumeBuilder/resume.jsx deleted file mode 100644 index cca2b29..0000000 --- a/client/src/components/ResumeBuilder/resume.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import axios from "axios"; -import "../../css/profile.css" -import { - Box, - Button, - Modal, - Typography, - } from "@mui/material"; -const home = "http://localhost:3001"; - - diff --git a/client/src/components/SearchResult.jsx b/client/src/components/SearchResult.jsx deleted file mode 100644 index c95f9ab..0000000 --- a/client/src/components/SearchResult.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useEffect, useState } from "react"; -import "../css/SearchResult.css"; -import { useNavigate } from "react-router-dom"; - -export const SearchResult = ({result}) => { - - const navigate = useNavigate(); - const [results, setResults] = ([]); - console.log(result); - const handleClick = async (e)=>{ - navigate(`/user/profile/${result._id}`) - - } - - return ( -
{result.username}
- ) -}; diff --git a/client/src/components/applyform/applyform.jsx b/client/src/components/applyform/applyform.jsx index 5e007c2..f08c978 100644 --- a/client/src/components/applyform/applyform.jsx +++ b/client/src/components/applyform/applyform.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; -import './applyform.css'; // Import the CSS file for styling +import { toast } from 'react-hot-toast'; +import './applyform.css'; const ApplyForm = () => { const [formData, setFormData] = useState({ @@ -20,8 +21,8 @@ const ApplyForm = () => { const handleSubmit = (e) => { e.preventDefault(); - // Handle form submission logic (e.g., send data to server) - console.log('Form data:', formData); + // Submitting applications is not wired to the API yet. + toast.error('Applications are not available yet.'); }; return ( diff --git a/client/src/components/chatOnline/chatonline.jsx b/client/src/components/chatOnline/chatonline.jsx index a516bc5..8c6e8e4 100644 --- a/client/src/components/chatOnline/chatonline.jsx +++ b/client/src/components/chatOnline/chatonline.jsx @@ -1,59 +1,48 @@ -import axios from "axios"; import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { getFriends } from "../../api/users"; +import { createConversation, findConversation } from "../../api/chat"; +import { getErrorMessage } from "../../api/client"; +import { avatarUrl, onAvatarError } from "../../utils/avatar"; +import logger from "../../utils/logger"; import "./chatonline.css"; export default function ChatOnline({ onlineUsers, currentId, setCurrentChat }) { const [friends, setFriends] = useState([]); - const [onlineFriends, setOnlineFriends] = useState([]); - const PF = process.env.REACT_APP_PUBLIC_FOLDER; useEffect(() => { - const getFriends = async () => { - const res = await axios.get("/user/friends/" + currentId); - setFriends(res.data); + let cancelled = false; + getFriends(currentId) + .then((data) => !cancelled && setFriends(data)) + .catch((err) => logger.error("Could not load friends:", err)); + return () => { + cancelled = true; }; - - getFriends(); }, [currentId]); - useEffect(() => { - - setOnlineFriends(friends.filter((f) => onlineUsers?.includes(f._id))); - - }, [friends, onlineUsers]); + const onlineFriends = friends.filter((f) => onlineUsers?.includes(f._id)); - const handleClick = async (user) => { + const handleClick = async (friend) => { try { - const res = await axios.get( - `/conversations/find/${currentId}/${user._id}` - ); - setCurrentChat(res.data); - } catch (err) { - console.log(err); + const conversation = (await findConversation(currentId, friend._id)) || (await createConversation(friend._id)); + setCurrentChat(conversation); + } catch (error) { + toast.error(getErrorMessage(error, "Could not open the conversation")); } }; return (
- - {onlineFriends.map((o) => ( - -
handleClick(o)}> + {onlineFriends.length === 0 && No friends online} + {onlineFriends.map((friend) => ( +
handleClick(friend)}>
- +
- {o?.username} + {friend.username}
))}
); -} \ No newline at end of file +} diff --git a/client/src/components/conversations/Conversation.jsx b/client/src/components/conversations/Conversation.jsx index 2ddaed8..e40701f 100644 --- a/client/src/components/conversations/Conversation.jsx +++ b/client/src/components/conversations/Conversation.jsx @@ -1,36 +1,34 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { getUser } from "../../api/users"; +import { avatarUrl, onAvatarError } from "../../utils/avatar"; +import logger from "../../utils/logger"; import "./Conversation.css"; -import { useEffect } from "react"; -import axios from "axios"; -export default function Conversation({conversation,currentUser}){ +export default function Conversation({ conversation, currentUser }) { + const [friend, setFriend] = useState(null); - const [user,setUser] = useState(null); - const PF = process.env.REACT_APP_PUBLIC_FOLDER; - useEffect(()=>{ - const friendId = conversation.members.find((m)=>m !== currentUser._id); - const getUser = async ()=>{ - try{ - const response = await axios.get(`/user/${friendId}`); - setUser(response.data.user); - } - catch(err){ - console.log(err); - } - } - getUser(); - },[currentUser,conversation]); + useEffect(() => { + const friendId = conversation.members.find((m) => m !== currentUser._id); + if (!friendId) return undefined; + let cancelled = false; + getUser(friendId) + .then((data) => !cancelled && setFriend(data)) + .catch((err) => logger.error("Could not load conversation partner:", err)); + return () => { + cancelled = true; + }; + }, [currentUser._id, conversation.members]); - return
- Profile - {user?.username} + return ( +
+ + {friend?.username ?? "…"}
-} \ No newline at end of file + ); +} diff --git a/client/src/components/createProfile.jsx b/client/src/components/createProfile.jsx index c17e411..810b06c 100644 --- a/client/src/components/createProfile.jsx +++ b/client/src/components/createProfile.jsx @@ -1,102 +1,48 @@ -import React, { useState } from 'react'; -import axios from "axios"; -import { useNavigate } from 'react-router-dom'; -import { useContext } from 'react'; -import { AuthContext } from '../context/AuthContext'; +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { createProfile } from "../api/users"; +import { getErrorMessage } from "../api/client"; + const MyForm = () => { - // State variables for form fields const navigate = useNavigate(); - const {user} = useContext(AuthContext); - const [formData,setFormData] = useState({ - username:"", - About:"", - Education:"", - workExperience:"", - Skills:"" - }); + const { updateUser } = useAuth(); + const [saving, setSaving] = useState(false); + const [formData, setFormData] = useState({ about: "" }); + const handleInputChange = (e) => { const { name, value } = e.target; - setFormData((prevData) => ({ - ...prevData, - [name]: value, - })); + setFormData((prev) => ({ ...prev, [name]: value })); }; - // Function to handle form submission + const handleSubmit = async (e) => { - - e.preventDefault(); - - try { - // Replace 'YOUR_API_ENDPOINT' with your actual API endpoint - const response = await axios.post('/user/createProfile',formData); - // Handle the response as needed - console.log(response); - if(response.status == 200){ - console.log(response.data); - navigate(`/user/profile/${response.data.user._id}`); - } - } catch (error) { - // Handle errors - console.log('Error making post request:', error); - } - - }; + e.preventDefault(); + setSaving(true); + try { + const user = await createProfile(formData); + updateUser(user); + navigate(`/user/profile/${user._id}`); + } catch (error) { + toast.error(getErrorMessage(error, "Could not save profile")); + } finally { + setSaving(false); + } + }; + return (
-

Create Profile

-
- - - - - - - - - - - -
+

Create Profile

+

Tell others a little about yourself. Education, work experience and skills can be added from your profile page.

+
+ + +
); }; diff --git a/client/src/components/home.jsx b/client/src/components/home.jsx index 4aec6d2..05f71a8 100644 --- a/client/src/components/home.jsx +++ b/client/src/components/home.jsx @@ -1,112 +1,102 @@ -import { useEffect, useState ,useContext} from "react"; -import { useNavigate } from "react-router-dom"; -import UserWidget from "../widgets/UserWidget" -import axios from "axios" -import { AuthContext } from "../context/AuthContext"; +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { toast } from "react-hot-toast"; +import UserWidget from "../widgets/UserWidget"; +import { useAuth } from "../context/AuthContext"; +import { getAllPosts } from "../api/posts"; +import { respondToRequest } from "../api/users"; +import { getErrorMessage } from "../api/client"; import "../css/home.css"; -const PostCard = (props)=>{ - return
-
-

JOB Title: {props.jobTitle}

- -
-

Company Name : {props.companyName}

-

WorkPlace : {props.workPlace}

-

Job Location : {props.jobLocation}

-

Job Type : {props.jobType}

-

Salary : {props.salary}

+const PostCard = ({ post, isOwn }) => ( +
+
+

JOB Title: {post.jobTitle}

+ {!isOwn && ( + + + + )}
-} +

Company Name : {post.companyName}

+

WorkPlace : {post.workPlace}

+

Job Location : {post.jobLocation}

+

Job Type : {post.jobType}

+

Salary : {post.salary}

+
+); -const Requests = (props)=>{ - const {user} = useContext(AuthContext); - const handleClick = async (e)=>{ - const value = e.target.value; - try{ - const response = await axios.put(`/user/connect/${props.from}/${props.to}/${value}/${props._id}`); - console.log(response); - } - catch(error){ - console.log(error); +const FriendRequest = ({ request, onAnswered }) => { + const [busy, setBusy] = useState(false); + + const answer = async (action) => { + setBusy(true); + try { + const updatedUser = await respondToRequest({ from: request.from, to: request.to, requestId: request._id, action }); + onAnswered(updatedUser); + toast.success(action === "Accept" ? `You are now connected with ${request.username}` : "Request declined"); + } catch (error) { + toast.error(getErrorMessage(error, "Could not update the request")); + setBusy(false); } - } - return
- -

You have a new friend Request from {props.username}

- + }; - - - + return ( +
+

You have a new friend Request from {request.username}

+ +
-} -const Home = ()=>{ - const {user} = useContext(AuthContext); - console.log(user); - - document.addEventListener('DOMContentLoaded', function() { - const loading = document.getElementById('loading'); - - // Show loading spinner before the window unloads - window.addEventListener('beforeunload', function() { - loading.style.display = 'flex'; - }); - - // Optional: Show loading spinner on link clicks - document.querySelectorAll('a').forEach(link => { - link.addEventListener('click', function(event) { - event.preventDefault(); - loading.style.display = 'flex'; - setTimeout(() => { - window.location.href = this.href; - }, 500); // Delay for visual effect - }); - }); - }); - const [posts,setposts] = useState([]); - const getPosts = async () => { - try { - let response = await axios.get("/home"); - if (response.status === 200) { - console.log(response.data); - const postsData = await response.data.posts; // Assuming 'allposts' is a Promise - setposts(postsData); - } - } catch (err) { - console.log(err); - } - }; - useEffect(()=>{ - getPosts(); - },[]); - return ( -
-
-
-
-
-
-
- -
+ ); +}; + +const Home = () => { + const { user, updateUser, refreshUser } = useAuth(); + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Requests may have arrived since login; refresh the session user when the feed opens. + useEffect(() => { + refreshUser().catch(() => {}); + }, [refreshUser]); -
-

Notifications

-
-
- { - (user.user.friendRequests !== undefined ?user.user.friendRequests.map(Requests):

You are not logged in

) - } -
- { - posts.map(PostCard) - } -
-
+ useEffect(() => { + let cancelled = false; + getAllPosts() + .then((data) => !cancelled && setPosts(data)) + .catch((err) => !cancelled && setError(getErrorMessage(err, "Could not load posts"))) + .finally(() => !cancelled && setLoading(false)); + return () => { + cancelled = true; + }; + }, []); + + const requests = user?.friendRequests ?? []; + + return ( +
+
+ +
+
+

Notifications

+
+ {requests.length === 0 &&

No pending connection requests.

} + {requests.map((request) => ( + + ))}
+

Latest jobs

+ {loading &&

Loading…

} + {error &&

{error}

} + {!loading && !error && posts.length === 0 &&

No jobs posted yet.

} + {posts.map((post) => ( + + ))}
- ) -} +
+ ); +}; -export default Home; \ No newline at end of file +export default Home; diff --git a/client/src/components/jobpostform.jsx b/client/src/components/jobpostform.jsx index b3cdefe..b2c8c06 100644 --- a/client/src/components/jobpostform.jsx +++ b/client/src/components/jobpostform.jsx @@ -1,109 +1,67 @@ -import axios from 'axios'; -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; +import { createPost } from "../api/posts"; +import { getErrorMessage } from "../api/client"; import "../css/JobForm.css"; + +const EMPTY_FORM = { jobTitle: "", companyName: "", workPlace: "", jobLocation: "", jobType: "", salary: "" }; + const JobForm = () => { - // State to hold form data const navigate = useNavigate(); - const [formData, setFormData] = useState({ - jobTitle: '', - companyName: '', - workPlace: '', - jobLocation: '', - jobType: '', - salary: '', - }); + const [formData, setFormData] = useState(EMPTY_FORM); + const [saving, setSaving] = useState(false); - // Function to handle form input changes const handleChange = (e) => { const { name, value } = e.target; - setFormData({ - ...formData, - [name]: value, - }); + setFormData((prev) => ({ ...prev, [name]: value })); }; - // Function to handle form submission - const handleSubmit = async (e)=>{ + const handleSubmit = async (e) => { e.preventDefault(); - try{ - const response = await axios.post("/user/jobpostform",formData); - if(response.status == 200){ - console.log(response.data); - navigate('/user/allposts'); - } - } - catch(err){ - console.log(err); + setSaving(true); + try { + await createPost(formData); + toast.success("Job posted"); + navigate("/user/allposts"); + } catch (error) { + toast.error(getErrorMessage(error, "Could not create the post")); + } finally { + setSaving(false); } - - } + }; + return (
- - - - - - - +
); }; export default JobForm; - diff --git a/client/src/components/login.jsx b/client/src/components/login.jsx index 6bf537c..252ee48 100644 --- a/client/src/components/login.jsx +++ b/client/src/components/login.jsx @@ -1,73 +1,53 @@ - -import React, { useContext, useState,useRef,useEffect } from "react"; -import axios from "axios"; +import React, { useRef } from "react"; import { toast } from "react-hot-toast"; -import { Navigate, useNavigate } from "react-router-dom"; -import {AuthContext} from "../context/AuthContext"; -import {loginCall} from "../apiCalls"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { getErrorMessage } from "../api/client"; import "../css/home.css"; -import { removeCookie, setCookie } from "../hooks/cookie"; -const server = `http://localhost:3001`; +const HERO_IMAGE = + "https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"; + const Login = () => { const email = useRef(); const password = useRef(); const navigate = useNavigate(); - const { user,dispatch } = useContext(AuthContext); - - const handleClick = async (e) => { - e.preventDefault(); - await loginCall( - { email: email.current.value, password: password.current.value }, - dispatch - ); + const location = useLocation(); + const { login, pending } = useAuth(); - removeCookie("user"); - // setCookie("user", {id:user.user._id,username: user.user.username}); - localStorage.setItem("isLogged", true); - - if(user){ - navigate(`/user/profile/${user.user._id}`); - const emailDispatch = await axios.post(`${server}/email/intro`, {userEmail: user.user.email, userName: user.user.username}); - console.log(emailDispatch.data); - } + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const user = await login({ email: email.current.value, password: password.current.value }); + const from = location.state?.from?.pathname; + navigate(from || `/user/profile/${user._id}`, { replace: true }); + } catch (error) { + toast.error(getErrorMessage(error, "Could not log in")); + } }; - + return ( -
- - +
+
-

Login

- -
- - - -

Don't have an Account ?

- { - navigate("/user/register"); - }} style={{cursor:"pointer" , color:"purple", fontWeight:"bold"}}>SignUp + navigate("/user/register")} + style={{ cursor: "pointer", color: "purple", fontWeight: "bold" }} + > + SignUp +
); }; -export default Login; \ No newline at end of file +export default Login; diff --git a/client/src/components/logout.jsx b/client/src/components/logout.jsx deleted file mode 100644 index 595b46b..0000000 --- a/client/src/components/logout.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import axios from "axios"; -import { removeCookie } from "../hooks/cookie"; -const home = "http://localhost:3001"; - - const LogoutFunc = async () => { - // setLoading(true); - try { - const response = await axios.get(`${home}/user/logout`) - if (response.status === 200) { - console.log(`logout success`) - removeCookie("token"); - removeCookie("authorization"); - localStorage.clear(); - window.location.href = "/"; - } - } catch (error) { - console.error("Error logging out", error); - } - }; - - export default LogoutFunc; - - diff --git a/client/src/components/messages/message.jsx b/client/src/components/messages/message.jsx index 393b642..a88db50 100644 --- a/client/src/components/messages/message.jsx +++ b/client/src/components/messages/message.jsx @@ -1,16 +1,15 @@ +import { format } from "timeago.js"; import "./message.css"; -import {format} from "timeago.js"; -export default function Message({message,own}){ - return
-
- - {message.text} -
-
-

{format(message.createdAt)}

-
+export default function Message({ message, own }) { + return ( +
+
+ {message.text} +
+
+

{format(message.createdAt)}

+
-} \ No newline at end of file + ); +} diff --git a/client/src/components/navbar.jsx b/client/src/components/navbar.jsx index 2e4c426..7956768 100644 --- a/client/src/components/navbar.jsx +++ b/client/src/components/navbar.jsx @@ -1,18 +1,28 @@ import * as React from "react"; +import { useEffect, useRef, useState } from "react"; import { styled, alpha } from "@mui/material/styles"; -import {AppBar,Box,Toolbar,IconButton,Typography,InputBase,Badge,Menu,MenuItem,} -from "@mui/material"; -import {Logout as LogoutIcon, Menu as MenuIcon,Search as SearchIcon,AccountCircle,Mail as MailIcon,Notifications as NotificationsIcon,MoreVert as MoreIcon,Home as HomeIcon,PictureAsPdf as PictureAsPdfIcon,} -from "@mui/icons-material"; - -import { useState } from "react"; -import axios from "axios"; +import { AppBar, Box, Toolbar, IconButton, Typography, InputBase, Badge, Menu, MenuItem } from "@mui/material"; +import { + Logout as LogoutIcon, + Menu as MenuIcon, + Search as SearchIcon, + AccountCircle, + Mail as MailIcon, + Notifications as NotificationsIcon, + MoreVert as MoreIcon, + Home as HomeIcon, + PictureAsPdf as PictureAsPdfIcon, +} from "@mui/icons-material"; import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { searchUsers } from "../api/users"; +import logger from "../utils/logger"; import "../css/navbar.css"; -import LogoutFunc from "./logout" import "../css/SearchResult.css"; import "../css/SearchResultsList.css"; +const SEARCH_DEBOUNCE_MS = 300; + const Search = styled("div")(({ theme }) => ({ position: "relative", borderRadius: theme.shape.borderRadius, @@ -29,14 +39,20 @@ const Search = styled("div")(({ theme }) => ({ }, })); -const SearchIconWrapper = styled("div")(({ theme }) => ({ padding: theme.spacing(0, 2), height: "100%", position: "absolute", pointerEvents: "none", display: "flex", alignItems: "center", justifyContent: "center", +const SearchIconWrapper = styled("div")(({ theme }) => ({ + padding: theme.spacing(0, 2), + height: "100%", + position: "absolute", + pointerEvents: "none", + display: "flex", + alignItems: "center", + justifyContent: "center", })); const StyledInputBase = styled(InputBase)(({ theme }) => ({ color: "inherit", "& .MuiInputBase-input": { padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon paddingLeft: `calc(1em + ${theme.spacing(4)})`, transition: theme.transitions.create("width"), width: "100%", @@ -46,50 +62,88 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({ }, })); +const SearchResultsList = ({ results, onSelect }) => ( +
+
+ {results.map((result) => ( +
onSelect(result)}> +
{result.username}
+
+ ))} +
+
+); + export default function PrimarySearchAppBar() { const [anchorEl, setAnchorEl] = React.useState(null); const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null); const navigate = useNavigate(); - + const { user, logout } = useAuth(); + const pendingRequests = user?.friendRequests?.length ?? 0; + const isMenuOpen = Boolean(anchorEl); const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); - const handleProfileMenuOpen = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleMobileMenuClose = () => { - setMobileMoreAnchorEl(null); - }; - + const handleProfileMenuOpen = (event) => setAnchorEl(event.currentTarget); + const handleMobileMenuClose = () => setMobileMoreAnchorEl(null); const handleMenuClose = () => { setAnchorEl(null); handleMobileMenuClose(); }; + const handleMobileMenuOpen = (event) => setMobileMoreAnchorEl(event.currentTarget); + + const goToProfile = () => { + handleMenuClose(); + if (user) navigate(`/user/profile/${user._id}`); + }; + const handleLogout = async () => { + await logout(); + navigate("/"); + }; - const handleMobileMenuOpen = (event) => { - setMobileMoreAnchorEl(event.currentTarget); + // Search: debounced request per keystroke, results cleared on selection. + const [input, setInput] = useState(""); + const [results, setResults] = useState([]); + const debounceRef = useRef(); + + useEffect(() => { + clearTimeout(debounceRef.current); + const term = input.trim(); + if (!term) { + setResults([]); + return undefined; + } + let cancelled = false; + debounceRef.current = setTimeout(() => { + searchUsers(term) + .then((users) => !cancelled && setResults(users)) + .catch((error) => logger.error("Search failed:", error)); + }, SEARCH_DEBOUNCE_MS); + return () => { + cancelled = true; + clearTimeout(debounceRef.current); + }; + }, [input]); + + const selectResult = (result) => { + setInput(""); + setResults([]); + navigate(`/user/profile/${result._id}`); }; const menuId = "primary-search-account-menu"; const renderMenu = ( - Profile - My account + Profile + Logout ); @@ -97,47 +151,29 @@ export default function PrimarySearchAppBar() { const renderMobileMenu = ( - - - - - + { handleMobileMenuClose(); navigate("/user/messenger"); }}> + +

Messages

- - - + { handleMobileMenuClose(); navigate("/home"); }}> + +

Notifications

- +

Profile

@@ -145,78 +181,14 @@ export default function PrimarySearchAppBar() {
); - const [input, setInput] = useState(""); - const [results,setResults] = useState([]); - const fetchData = async (value) => { - try { - if(value !== ""){ - const response = await axios.get(`/search/${value}`, null, { - headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }, - params: { - jobTitle: value, - }, - }); - - if (response.status === 200) { - console.log(response.data); - setResults(response.data.user); - } - } else { - console.log(results); - setResults([]); - } - - }catch (error) { - console.error("Error fetching data:", error); - } - }; - - const SearchResultsList = ({results})=> { - - return ( -
-
- { - results?.map((result) => { - const handleClick = async (e)=>{ - navigate(`/user/profile/${result._id}`); - setInput(""); - setResults([]); - } - return ( -
{result.username}
- ) - } - )} -
-
- )}; - - - const handleChange = (value) => { - setInput(value); - fetchData(value); - }; - return ( - + - + WorkPlace
@@ -228,42 +200,30 @@ export default function PrimarySearchAppBar() { placeholder="Search…" inputProps={{ "aria-label": "search" }} value={input} - onChange={(e) => handleChange(e.target.value)} - + onChange={(e) => setInput(e.target.value)} /> - { - - } - + {results.length > 0 && }
-

[navigate("/home")]}> + navigate("/home")}> -

- - - {/* Resume Bulider */} -

[navigate("/resume")]}> - -

- - - - - +
+ navigate("/resume")}> + + + navigate("/user/messenger")}> + navigate("/home")} > - + @@ -284,24 +244,9 @@ export default function PrimarySearchAppBar() {
- - {/* logout button */} - LogoutFunc()} - > - - - - - - - - + + + {renderMobileMenu} diff --git a/client/src/components/profile.jsx b/client/src/components/profile.jsx index accb20f..b31781a 100644 --- a/client/src/components/profile.jsx +++ b/client/src/components/profile.jsx @@ -1,24 +1,17 @@ -// Profile.js -import React, { useContext, useEffect, useState } from "react"; -import axios from "axios"; +import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { - Box, - Button, - Modal, - Typography, -} from "@mui/material"; - -import { - CloudUpload as CloudUploadIcon, -} from "@mui/icons-material"; - -import "../css/profile.css"; +import { toast } from "react-hot-toast"; +import { Box, Button, Modal, Typography } from "@mui/material"; +import { CloudUpload as CloudUploadIcon } from "@mui/icons-material"; +import { useAuth } from "../context/AuthContext"; +import { sendConnectionRequest, updateInfo, uploadPhoto } from "../api/users"; +import { getErrorMessage } from "../api/client"; +import { avatarUrl, onAvatarError } from "../utils/avatar"; import { Skills, WorkEx } from "./profile/userExp"; import CollegeDesc from "./profile/collegeDesc"; -import { AuthContext } from "../context/AuthContext"; +import "../css/profile.css"; -const style = { +export const modalStyle = { position: "absolute", top: "50%", left: "50%", @@ -29,6 +22,7 @@ const style = { boxShadow: 24, p: 4, }; + function LoadingSpinner() { return (
@@ -37,235 +31,149 @@ function LoadingSpinner() { ); } - -const Profile = ({User}) => { - const home = "http://localhost:3001"; - const [isLoading, setLoading] = useState(false); - const {user} = useContext(AuthContext); - - const [name, setName] = useState(null); - const [about, setAbout] = useState(null); - const [newUser, setNewAuthor] = useState({ - photo: "", - }); - - // Modals ------------------------------- - const [name_about_modal, setNameAboutModal] = useState(false); - const [photo_modal, setPhotoModal] = useState(false); - - // Toggle Functions ---------------------- - const handleOpen = () => setNameAboutModal(true); - const handleClose = () => setNameAboutModal(false); - const photoOpen = () => setPhotoModal(true); - const photoClose = () => setPhotoModal(false); - +// Profile card. `User` is the profile being viewed; `onUserUpdated(patch)` lets the parent +// merge edits into its copy (and the session user when it is the own profile). +const Profile = ({ User, onUserUpdated }) => { const navigate = useNavigate(); - const handleClick = async ()=>{ - try{ - const response = await axios.get(`/user/profile/${User._id}/connect`); - if(response.status === 200){ - console.log(response.data); - } - } - catch(err){ - console.log(err); - } + const { user: me } = useAuth(); + const isOwnProfile = me?._id === User._id; + const alreadyConnected = Boolean(me?.friends?.some((id) => String(id) === String(User._id))); + + const [uploading, setUploading] = useState(false); + const [photoFile, setPhotoFile] = useState(null); + const [name, setName] = useState(""); + const [about, setAbout] = useState(""); + const [requestSent, setRequestSent] = useState(false); + const [infoModalOpen, setInfoModalOpen] = useState(false); + const [photoModalOpen, setPhotoModalOpen] = useState(false); + + const handleConnect = async () => { + try { + await sendConnectionRequest(User._id); + setRequestSent(true); + toast.success(`Connection request sent to ${User.username}`); + } catch (error) { + toast.error(getErrorMessage(error, "Could not send the request")); } - - const updatePersonalInfo = (username) => { - fetch(`${home}/in/update/${username}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: name, - about: about, - }), - }) - .then((res) => res.json()) - .then((data) => { - console.log(data); - }); }; - const handleSubmit = async (e) => { + const handleInfoSubmit = async (e) => { e.preventDefault(); - setLoading(true); - const formData = new FormData(); - formData.append("photo", newUser.photo); - formData.append("user", User._id); - - const local = await axios.post(`${home}/in/add/`, formData); - User.photo = local.data.url; - setLoading(false); - photoClose(); + try { + const data = await updateInfo({ name, about }); + onUserUpdated({ name: data.name, about: data.about }); + setInfoModalOpen(false); + toast.success("Profile updated"); + } catch (error) { + toast.error(getErrorMessage(error, "Could not update the profile")); + } }; - const handlePhoto = (e) => { - setNewAuthor({ photo: e.target.files[0] }); - }; - // Add username to local storage - localStorage.setItem("username", User.username); - const [isFriend, setIsFriend] = useState(false); - const checkFriend = async () => { + const handlePhotoSubmit = async (e) => { + e.preventDefault(); + if (!photoFile) return; + setUploading(true); try { - const response = await axios.get(`/user/${user.user._id}`); - if (response.status === 200) { - console.log(response.data.user.friends.length); - if(response.data.user.friends.length > 0) - setIsFriend(true); - } + const url = await uploadPhoto(photoFile); + onUserUpdated({ photo: url }); + setPhotoModalOpen(false); + toast.success("Photo updated"); } catch (error) { - console.log(error); + toast.error(getErrorMessage(error, "Could not upload the photo")); + } finally { + setUploading(false); } - console.log(isFriend); - } + }; - useEffect(() => { - checkFriend(); - }, []); return ( -
+

Profile

- https://buffer.com/cdn-cgi/image/w=1000,fit=contain,q=90,f=auto/library/content/images/size/w1200/2023/10/free-images.jpg +
- { - User._id !== user.user._id? "": - } - - - -
- - Profile update - - {isLoading ? : null} -
- - - { - User._id !== user.user._id? "": - } - -
-
+ {isOwnProfile && ( + + )} + + setPhotoModalOpen(false)} aria-labelledby="photo-modal-title"> + + + Profile photo + + {uploading && } +
+ setPhotoFile(e.target.files[0])} /> + +
-

- {" "} -

Username {User.username}

-

+

Username {User.username}

Name {User.name}

About - {User.about}
- { - User._id !== user.user._id? "": - } - - - -
- - Profile update - - -
- setName(e.target.value)} - /> - setAbout(e.target.value)} - /> - -
-
-
+ {isOwnProfile && ( + + )} + + setInfoModalOpen(false)} aria-labelledby="info-modal-title"> + + + Profile update + +
+ setName(e.target.value)} /> + setAbout(e.target.value)} /> + +
-
{User.email}
+
{User.email}
-
- - { - User._id !== user.user._id? "": - } - { - User._id !== user.user._id? "": - } - -
- - {/* --------------------------------------------- */} + +
+ {!isOwnProfile && ( + + )} + {isOwnProfile && ( + <> + + + + )} +
+
- {/* Array of education */}

Education

- +
- {/* --------------------------------- */}
- {/* Array of work experience */}

Work Experience

- +
- - {/* --------------------------------- */}
- {/* array of skills */}

Skills

- +
); }; - export { Profile }; diff --git a/client/src/components/profile/collegeDesc.jsx b/client/src/components/profile/collegeDesc.jsx index cfbf08e..2f63cf0 100644 --- a/client/src/components/profile/collegeDesc.jsx +++ b/client/src/components/profile/collegeDesc.jsx @@ -1,102 +1,66 @@ +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; +import { Box, Button, Modal, Typography } from "@mui/material"; +import { Delete as DeleteIcon } from "@mui/icons-material"; +import { addEducation } from "../../api/users"; +import { getErrorMessage } from "../../api/client"; +import { modalStyle } from "../profile"; +import "../../css/profile.css"; -import React, { useContext, useEffect, useState } from "react"; -import axios from "axios"; -import "../../css/profile.css" -import { - Box, - Button, - Modal, - Typography, - } from "@mui/material"; +const CollegeDesc = ({ entries = [], editable, onUserUpdated }) => { + const [open, setOpen] = useState(false); -import { - Delete as DeleteIcon, - } from "@mui/icons-material"; - -const home = "http://localhost:3001"; - -const style = { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: 400, - bgcolor: "background.paper", - border: "2px solid #000", - boxShadow: 24, - p: 4, + const addCollege = async (e) => { + e.preventDefault(); + const [collegeName, degree, year] = Array.from(e.target.elements, (el) => el.value); + try { + const education = await addEducation({ collegeName, degree, year }); + onUserUpdated({ education }); + setOpen(false); + } catch (error) { + toast.error(getErrorMessage(error, "Could not add the college")); + } }; -const CollegeDesc = ({props,User,user})=>{ - const addCollege = async (e) => { - e.preventDefault(); - const collegeName = e.target[0].value; - const degree = e.target[1].value; - const year = e.target[2].value; - const username = localStorage.getItem("username"); - try { - const response = await axios.post(`${home}/in/addCollege/${username}`, { - collegeName, - degree, - year - }); - if (response.status === 200) { - console.log(response.data); - window.location.reload(); - } - } catch (error) { - console.log(error); - } - } - const [open, setOpen] = useState(false); - - return( - <> -
- {props !== undefined && typeof props !== 'undefined' && props.length > 0 ? ( - props.map((edu) => ( + return ( + <> +
+ {entries.length > 0 ? ( + entries.map((edu) => (
-

College = {edu.collegeName}

-

Degree = {edu.degree}

-

Year = {edu.year}

- - + )}
- - )) + )) ) : ( -

Empty

+

Empty

)} -
- { - User._id !== user.user._id?"": - } - - setOpen(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - +
+ {editable && } + + setOpen(false)} aria-labelledby="add-college-title"> + + Add College - -
+ + - -
+ +
-
- - ) + + + ); }; export default CollegeDesc; - \ No newline at end of file diff --git a/client/src/components/profile/userExp.jsx b/client/src/components/profile/userExp.jsx index 3c0afb6..bdde624 100644 --- a/client/src/components/profile/userExp.jsx +++ b/client/src/components/profile/userExp.jsx @@ -1,151 +1,111 @@ -// Component of Skills and Work Ex - -import React, { useContext, useEffect, useState } from "react"; -import axios from "axios"; -import "../../css/profile.css"; +// Skills and Work Experience sections of the profile card +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; import { Box, Button, Modal, Typography } from "@mui/material"; - import { Delete as DeleteIcon } from "@mui/icons-material"; -const style = { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: 400, - bgcolor: "background.paper", - border: "2px solid #000", - boxShadow: 24, - p: 4, -}; -const home = "http://localhost:3001"; -const Skills = ({ props,User,user }) => { - const addSkills = async (e) => { +import { addSkill, addWorkExperience } from "../../api/users"; +import { getErrorMessage } from "../../api/client"; +import { modalStyle } from "../profile"; +import "../../css/profile.css"; + +const DeleteButton = () => ( + +); + +const Skills = ({ entries = [], editable, onUserUpdated }) => { + const [open, setOpen] = useState(false); + + const handleAdd = async (e) => { e.preventDefault(); - const description = e.target[0].value; - const username = localStorage.getItem("username"); + const description = e.target.elements[0].value; try { - const response = await axios.post(`${home}/in/addSkills/${username}`, { - description, - }); - if (response.status === 200) { - window.location.reload(); - } + const skills = await addSkill({ description }); + onUserUpdated({ skills }); + setOpen(false); } catch (error) { - console.log(error); + toast.error(getErrorMessage(error, "Could not add the skill")); } }; - useEffect(() => {}, []); - const [open, setOpen] = useState(false); - return ( - <> -
- {props !== undefined && - typeof props !== "undefined" && - props.length > 0 ? ( - props.map((skill) => ( -
- -

Desc = {skill.description}

-
- )) - ) : ( -

Empty

- )} - { - User._id !== user.user._id?"": - } - - setOpen(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - - Add Skills - -
- - -
-
-
-
- +
+ {entries.length > 0 ? ( + entries.map((skill) => ( +
+ {editable && } +

Desc = {skill.description}

+
+ )) + ) : ( +

Empty

+ )} + {editable && } + + setOpen(false)} aria-labelledby="add-skill-title"> + + + Add Skills + +
+ + +
+
+
+
); }; -const WorkEx = ({ props ,User,user}) => { - const addWorkEx = async (e) => { +const WorkEx = ({ entries = [], editable, onUserUpdated }) => { + const [open, setOpen] = useState(false); + + const handleAdd = async (e) => { e.preventDefault(); - const companyName = e.target[0].value; - const year = e.target[1].value; - const username = localStorage.getItem("username"); + const [companyName, year] = Array.from(e.target.elements, (el) => el.value); try { - const response = await axios.post(`${home}/in/addWorkEx/${username}`, { - companyName, - year, - }); - if (response.status === 200) { - window.location.reload(); - } + const workexperience = await addWorkExperience({ companyName, year }); + onUserUpdated({ workexperience }); + setOpen(false); } catch (error) { - console.log(error); + toast.error(getErrorMessage(error, "Could not add the work experience")); } }; - const [open, setOpen] = useState(false); - useEffect(() => { - }, []); + return ( - <> -
- {props !== undefined && - typeof props !== "undefined" && - props.length > 0 ? ( - props.map((work) => ( -
- -

Company Name = {work.companyName}

-

Year = {work.year}

-
- )) - ) : ( -

Empty

- )} - { - User._id !== user.user._id?"": - } - - setOpen(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - - Add Work Experience - -
- - - -
-
-
-
- +
+ {entries.length > 0 ? ( + entries.map((work) => ( +
+ {editable && } +

Company Name = {work.companyName}

+

Year = {work.year}

+
+ )) + ) : ( +

Empty

+ )} + {editable && } + + setOpen(false)} aria-labelledby="add-work-title"> + + + Add Work Experience + +
+ + + +
+
+
+
); }; + export { Skills, WorkEx }; diff --git a/client/src/components/register.jsx b/client/src/components/register.jsx index 8dd0804..7fdd30e 100644 --- a/client/src/components/register.jsx +++ b/client/src/components/register.jsx @@ -1,100 +1,79 @@ -import React, {useRef } from 'react'; -import axios from "axios"; -import { Link, Navigate, useNavigate } from 'react-router-dom'; -import { removeCookie, setCookie } from "../hooks/cookie"; +import React, { useRef } from "react"; +import { toast } from "react-hot-toast"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; +import { sendWelcomeEmail } from "../api/auth"; +import { getErrorMessage } from "../api/client"; import "../css/home.css"; + +const HERO_IMAGE = + "https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"; + const RegistrationForm = () => { - // State to hold form data - - const navigate = useNavigate(); const username = useRef(); const email = useRef(); const password = useRef(); const passwordAgain = useRef(); - + const { register, pending } = useAuth(); + + const clearMismatch = () => passwordAgain.current.setCustomValidity(""); - const handleClick = async (e) => { + const handleSubmit = async (e) => { e.preventDefault(); if (passwordAgain.current.value !== password.current.value) { passwordAgain.current.setCustomValidity("Passwords don't match!"); - } else { - const user = { + passwordAgain.current.reportValidity(); + return; + } + try { + // Registering also logs the user in (the server sets the auth cookie). + const user = await register({ username: username.current.value, email: email.current.value, password: password.current.value, - }; - try { - const response = await axios.post("http://localhost:3001/user/register", user); - removeCookie("user"); - setCookie("user", {id: response.data.user._id ,username: response.data.user.username}); - localStorage.setItem("isLogged", true); - const email= await axios.post("http://localhost:3001/email/intro", {userEmail: user.email, userName: user.username}); - console.log(email); - navigate("/user/login"); - } catch (err) { - console.log(err); - } + }); + sendWelcomeEmail().catch(() => {}); // best effort; email may be unconfigured + toast.success(`Welcome, ${user.username}!`); + navigate(`/user/profile/${user._id}`, { replace: true }); + } catch (error) { + toast.error(getErrorMessage(error, "Could not register")); } }; - + return ( -
- -
-

Sign Up

- -
- - - - - -
-

Already have an account ?

- { - navigate("/user/login"); - }} style={{cursor:"pointer" , color:"purple", fontWeight:"bold"}}>Login - -
+
+ +
+

Sign Up

+
+ + + + + +
+

Already have an account ?

+ navigate("/user/login")} style={{ cursor: "pointer", color: "purple", fontWeight: "bold" }}> + Login + +
); }; -export default RegistrationForm; \ No newline at end of file +export default RegistrationForm; diff --git a/client/src/components/userdashboard.jsx b/client/src/components/userdashboard.jsx index bc591bb..1bfa857 100644 --- a/client/src/components/userdashboard.jsx +++ b/client/src/components/userdashboard.jsx @@ -1,159 +1,106 @@ -import React from "react"; -import { useState, useEffect } from "react"; -import { Profile} from "./profile"; +import React, { useEffect, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { Profile } from "./profile"; +import { useAuth } from "../context/AuthContext"; +import { getFriends, getUser } from "../api/users"; +import { getUserPosts } from "../api/posts"; +import { getErrorMessage } from "../api/client"; +import { avatarUrl, onAvatarError } from "../utils/avatar"; import "../css/userdashboard.css"; -import axios from "axios"; -import { useNavigate, useParams } from 'react-router-dom'; -import { useContext } from "react"; -import { AuthContext } from "../context/AuthContext"; -const home = "http://localhost:3001"; -// @desc User dashboard -// @route GET /in/:username/dashboard +const PostCard = ({ post, canApply }) => ( +
+
+

JOB Title: {post.jobTitle}

+ {canApply && ( + + + + )} +
+

Company Name : {post.companyName}

+

WorkPlace : {post.workPlace}

+

Job Location : {post.jobLocation}

+

Job Type : {post.jobType}

+

Salary : {post.salary}

+
+); + +const FriendCard = ({ friend, onOpen }) => ( +
+ +

+ {friend.username} +

+
+); +// Profile page for any user (/user/profile/:userId). Own profile is editable. const Dashboard = () => { - const [userFriends, setUserFriends] = useState([]); - const [User,setUser] = useState([]); - const {userId} = useParams(); + const { userId } = useParams(); const navigate = useNavigate(); - - const getUser = async () => { - try{ - const response = await axios.get(`/user/profile/${userId}`); - if(response.status === 200){ - console.log(response.data); - setUser(response.data.user); - } - } - catch(err){ - console.log(err); - } - } + const { user: me, updateUser } = useAuth(); + const [profileUser, setProfileUser] = useState(null); + const [friends, setFriends] = useState([]); + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const getUserFriends = async () => { - try { - const response = await axios.get(`/user/profile/${userId}`); - if (response.status === 200) { - // Assuming response.data.user.friends contains friend IDs - const friendIds = response.data.user.friends; + const isOwnProfile = me?._id === userId; - // Fetch details for each friend - const friendDetailsPromises = friendIds.map(async (friendId) => { - const friendResponse = await axios.get(`/user/${friendId}`); - return friendResponse.data.user; - }); - - // Wait for all friend details to be fetched - const friendDetails = await Promise.all(friendDetailsPromises); + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + Promise.all([getUser(userId), getFriends(userId), getUserPosts(userId)]) + .then(([fetchedUser, fetchedFriends, fetchedPosts]) => { + if (cancelled) return; + setProfileUser(fetchedUser); + setFriends(fetchedFriends); + setPosts(fetchedPosts); + }) + .catch((err) => !cancelled && setError(getErrorMessage(err, "Could not load this profile"))) + .finally(() => !cancelled && setLoading(false)); + return () => { + cancelled = true; + }; + }, [userId]); - setUserFriends(friendDetails); - } - } catch (error) { - console.error('Error fetching user:', error); - } + // Edits made on the profile card update this page and, for the own profile, the session user. + const handleUserUpdated = (patch) => { + setProfileUser((prev) => ({ ...prev, ...patch })); + if (isOwnProfile) updateUser(patch); }; - const [post,setPost] = useState([]); - const getPosts = async () => { - try { - const response = await axios.get(`/user/allposts/${userId}`); - if (response.status === 200) { - console.log(response.data); - setPost(response.data.allposts); - } - } catch (error) { - console.error('Error fetching posts:', error); - - } - } - const PostCard = (props)=>{ - const {user} = useContext(AuthContext); - return
-
-

JOB Title: {props.jobTitle}

- { - user.user._id !== userId ? :"" - } - -
-

Company Name : {props.companyName}

-

WorkPlace : {props.workPlace}

-

Job Location : {props.jobLocation}

-

Job Type : {props.jobType}

-

Salary : {props.salary}

-
- } - useEffect(() => { - getUserFriends(); - getUser(); - getPosts(); - },[userId]); + + if (loading) return

Loading profile…

; + if (error || !profileUser) return

{error || "Profile not found"}

; return ( -
+
- - +
-
- Your Activity +
+ {isOwnProfile ? "Your Activity" : `${profileUser.username}'s posts`}
- -
- { - post.length === 0 ?
No posts to show
: - post.map(PostCard) - } -
-
- + {posts.length === 0 ? ( +
No posts to show
+ ) : ( + posts.map((post) => ) + )}
-
-
-

Friends

- - {userFriends.map((friend) => ( -
-
-
- Profile -
-
-

{ - navigate(`/user/profile/${friend._id}`) - }}>{friend.username}

-

Education: {friend.Education}

-

Work Experience: {friend.workExperience}

-
+
+

Friends

+ {friends.length === 0 &&

No connections yet.

} + {friends.map((friend) => ( + navigate(`/user/profile/${friend._id}`)} /> ))} -
); }; export default Dashboard; - diff --git a/client/src/components/userimage.jsx b/client/src/components/userimage.jsx deleted file mode 100644 index c872edc..0000000 --- a/client/src/components/userimage.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Box } from "@mui/material"; - -const userimage = ({image, size="60px"}) => { - return ( - - user - - ); -}; - -export default userimage; \ No newline at end of file diff --git a/client/src/components/widgetwrapper.jsx b/client/src/components/widgetwrapper.jsx deleted file mode 100644 index a13c79b..0000000 --- a/client/src/components/widgetwrapper.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Box } from "@mui/material"; -import { styled } from "@mui/system"; - -const widgetwrapper = styled(Box)(({theme}) => ({ - display: "flex", - flexDirection: "column", -})); - -export default widgetwrapper; \ No newline at end of file diff --git a/client/src/context/AuthActions.js b/client/src/context/AuthActions.js deleted file mode 100644 index 7ee03ff..0000000 --- a/client/src/context/AuthActions.js +++ /dev/null @@ -1,23 +0,0 @@ -export const LoginStart = (userCredentials) => ({ - type: "LOGIN_START", - }); - - export const LoginSuccess = (user) => ({ - type: "LOGIN_SUCCESS", - payload: user, - }); - - export const LoginFailure = () => ({ - type: "LOGIN_FAILURE", - }); - - export const Follow = (userId) => ({ - type: "FOLLOW", - payload: userId, - }); - - export const Unfollow = (userId) => ({ - type: "UNFOLLOW", - payload: userId, - }); - \ No newline at end of file diff --git a/client/src/context/AuthContext.js b/client/src/context/AuthContext.js index af8c5db..7f8bfb9 100644 --- a/client/src/context/AuthContext.js +++ b/client/src/context/AuthContext.js @@ -1,32 +1,86 @@ -import { createContext, useEffect, useReducer } from "react"; -import AuthReducer from "./AuthReducer"; +import { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from "react"; +import AuthReducer, { initialAuthState } from "./AuthReducer"; +import { getErrorMessage, setUnauthorizedHandler } from "../api/client"; +import * as authApi from "../api/auth"; -const INITIAL_STATE = { - user:JSON.parse(localStorage.getItem("user")) || null, - isFetching: false, - error: false, -}; +const STORAGE_KEY = "user"; +// Reads the cached user. Older builds stored the whole { success, message, user } envelope. +const readStoredUser = () => { + try { + const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY)); + if (!parsed) return null; + return parsed.user && parsed.success !== undefined ? parsed.user : parsed; + } catch { + return null; + } +}; -export const AuthContext = createContext(INITIAL_STATE); +export const AuthContext = createContext(null); export const AuthContextProvider = ({ children }) => { - const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE); - - useEffect(()=>{ - localStorage.setItem("user", JSON.stringify(state.user)) - },[state.user]) - - return ( - - {children} - + const [state, dispatch] = useReducer(AuthReducer, initialAuthState, (initial) => { + const user = readStoredUser(); + return user ? { ...initial, user, status: "loading" } : initial; + }); + + // Keep the cache in sync so a reload does not flash the login page. + useEffect(() => { + if (state.user) localStorage.setItem(STORAGE_KEY, JSON.stringify(state.user)); + else localStorage.removeItem(STORAGE_KEY); + }, [state.user]); + + // Any 401 from the API means the cookie is gone or expired: drop the session. + useEffect(() => { + setUnauthorizedHandler(() => dispatch({ type: "LOGOUT" })); + return () => setUnauthorizedHandler(null); + }, []); + + // Re-validate a cached session against the server on first load. + useEffect(() => { + if (!readStoredUser()) return undefined; + let cancelled = false; + authApi + .fetchMe() + .then((user) => !cancelled && dispatch({ type: "LOGIN_SUCCESS", payload: user })) + .catch(() => !cancelled && dispatch({ type: "LOGOUT" })); + return () => { + cancelled = true; + }; + }, []); + + const runAuth = useCallback(async (request) => { + dispatch({ type: "LOGIN_START" }); + try { + const user = await request(); + dispatch({ type: "LOGIN_SUCCESS", payload: user }); + return user; + } catch (error) { + dispatch({ type: "LOGIN_FAILURE", payload: getErrorMessage(error, "Could not sign in") }); + throw error; + } + }, []); + + const login = useCallback((credentials) => runAuth(() => authApi.login(credentials)), [runAuth]); + const register = useCallback((details) => runAuth(() => authApi.register(details)), [runAuth]); + const logout = useCallback(async () => { + await authApi.logout().catch(() => {}); // local state is cleared even if the server is down + dispatch({ type: "LOGOUT" }); + }, []); + const updateUser = useCallback((patch) => dispatch({ type: "UPDATE_USER", payload: patch }), []); + // Pull the latest user from the server (new connection requests, friends, posts). + const refreshUser = useCallback(async () => { + const user = await authApi.fetchMe(); + dispatch({ type: "LOGIN_SUCCESS", payload: user }); + return user; + }, []); + + const value = useMemo( + () => ({ ...state, login, register, logout, updateUser, refreshUser, dispatch }), + [state, login, register, logout, updateUser, refreshUser] ); + + return {children}; }; + +export const useAuth = () => useContext(AuthContext); diff --git a/client/src/context/AuthReducer.js b/client/src/context/AuthReducer.js index 4f458a6..49e34a6 100644 --- a/client/src/context/AuthReducer.js +++ b/client/src/context/AuthReducer.js @@ -1,45 +1,21 @@ +// status: "loading" (cached session being re-validated) | "authenticated" | "anonymous" +export const initialAuthState = { user: null, status: "anonymous", pending: false, error: null }; + const AuthReducer = (state, action) => { - switch (action.type) { - case "LOGIN_START": - return { - user: null, - isFetching: true, - error: false, - }; - case "LOGIN_SUCCESS": - return { - user: action.payload, - isFetching: false, - error: false, - }; - case "LOGIN_FAILURE": - return { - user: null, - isFetching: false, - error: true, - }; - case "FOLLOW": - return { - ...state, - user: { - ...state.user, - followings: [...state.user.followings, action.payload], - }, - }; - case "UNFOLLOW": - return { - ...state, - user: { - ...state.user, - followings: state.user.followings.filter( - (following) => following !== action.payload - ), - }, - }; - default: - return state; - } - }; - - export default AuthReducer; - \ No newline at end of file + switch (action.type) { + case "LOGIN_START": + return { ...state, pending: true, error: null }; + case "LOGIN_SUCCESS": + return { user: action.payload, status: "authenticated", pending: false, error: null }; + case "LOGIN_FAILURE": + return { user: null, status: "anonymous", pending: false, error: action.payload }; + case "LOGOUT": + return { ...initialAuthState }; + case "UPDATE_USER": + return { ...state, user: { ...state.user, ...action.payload } }; + default: + return state; + } +}; + +export default AuthReducer; diff --git a/client/src/hooks/cookie.jsx b/client/src/hooks/cookie.jsx deleted file mode 100644 index 5fd25d9..0000000 --- a/client/src/hooks/cookie.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import Cookie from 'js-cookie'; - -const setCookie=(name, usrin)=> { - Cookie.set(name, usrin, { - expires: 10, - secure: true, - path: '/', - sameSite: 'strict', - - }); - } -const getCookie=async(name)=>{ - let obj=Cookie.get(name); - obj= decodeURIComponent(obj); - const obj1=await JSON.parse(obj); - return obj1; -} -const removeCookie=(name)=>{ - return Cookie.remove(name); -} - - export { - setCookie, - removeCookie, - getCookie - } \ No newline at end of file diff --git a/client/src/img/home.png b/client/src/img/home.png deleted file mode 100644 index 8511e46..0000000 Binary files a/client/src/img/home.png and /dev/null differ diff --git a/client/src/index.js b/client/src/index.js index 28c9328..9b9d902 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,11 +1,12 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import App from "./App"; import { AuthContextProvider } from "./context/AuthContext"; -ReactDOM.render( +createRoot(document.getElementById("root")).render( + - , - document.getElementById("root") + + ); diff --git a/client/src/pages/messenger/messenger.jsx b/client/src/pages/messenger/messenger.jsx index 922207c..83fc71e 100644 --- a/client/src/pages/messenger/messenger.jsx +++ b/client/src/pages/messenger/messenger.jsx @@ -1,159 +1,147 @@ import "./messenger.css"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { io } from "socket.io-client"; +import { toast } from "react-hot-toast"; import Conversation from "../../components/conversations/Conversation"; import Message from "../../components/messages/message"; import ChatOnline from "../../components/chatOnline/chatonline"; -import { useContext, useEffect, useRef, useState } from "react"; -import {AuthContext} from "../../context/AuthContext"; -import axios from "axios"; -import { io } from "socket.io-client"; +import { useAuth } from "../../context/AuthContext"; +import { getConversations, getMessages, sendMessage } from "../../api/chat"; +import { getErrorMessage } from "../../api/client"; +import logger from "../../utils/logger"; + +const SOCKET_URL = process.env.REACT_APP_SOCKET_URL || "ws://localhost:8900"; export default function Messenger() { + const { user } = useAuth(); const [conversations, setConversations] = useState([]); const [currentChat, setCurrentChat] = useState(null); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); const [arrivalMessage, setArrivalMessage] = useState(null); const [onlineUsers, setOnlineUsers] = useState([]); - const socket = useRef(); - const { user } = useContext(AuthContext); + const socket = useRef(null); const scrollRef = useRef(); - + + // One socket per mount; registered as this user; torn down on unmount. useEffect(() => { - socket.current = io("ws://localhost:8900"); - socket.current.on("getMessage", (data) => { - setArrivalMessage({ - sender: data.senderId, - text: data.text, - createdAt: Date.now(), - }); + const s = io(SOCKET_URL); + socket.current = s; + s.on("getMessage", (data) => { + setArrivalMessage({ sender: data.senderId, text: data.text, conversationId: data.conversationId, createdAt: Date.now() }); }); - }, []); + s.on("getUsers", (users) => { + setOnlineUsers(user.friends?.filter((f) => users.some((u) => u.userId === f)) ?? []); + }); + s.emit("addUser", user._id); + return () => { + s.off("getMessage"); + s.off("getUsers"); + s.disconnect(); + }; + }, [user._id, user.friends]); + // Append live messages that belong to the open conversation. useEffect(() => { - arrivalMessage && - currentChat?.members.includes(arrivalMessage.sender) && - setMessages((prev) => [...prev, arrivalMessage]); + if (!arrivalMessage || !currentChat) return; + const forThisChat = arrivalMessage.conversationId + ? arrivalMessage.conversationId === currentChat._id + : currentChat.members.includes(arrivalMessage.sender); + if (forThisChat) setMessages((prev) => [...prev, arrivalMessage]); }, [arrivalMessage, currentChat]); useEffect(() => { - socket.current.emit("addUser", user.user._id); - socket.current.on("getUsers", (users) => { - - setOnlineUsers( - user.user.friends?.filter((f) => (users.some((u) => u.userId === f))) - ); - - }); - }, [user.user]); - - useEffect(() => { - const getConversations = async () => { - try { - const res = await axios.get("/conversations/" + user.user._id); - setConversations(res.data); - } catch (err) { - console.log(err); - } + let cancelled = false; + getConversations(user._id) + .then((data) => !cancelled && setConversations(data)) + .catch((err) => logger.error("Could not load conversations:", err)); + return () => { + cancelled = true; }; - getConversations(); - }, [user.user._id]); + }, [user._id]); useEffect(() => { - const getMessages = async () => { - try { - const res = await axios.get("/messages/" + currentChat?._id); - setMessages(res.data); - } catch (err) { - console.log(err); - } + if (!currentChat?._id) return undefined; + let cancelled = false; + getMessages(currentChat._id) + .then((data) => !cancelled && setMessages(data)) + .catch((err) => logger.error("Could not load messages:", err)); + return () => { + cancelled = true; }; - getMessages(); }, [currentChat]); - - const handleSubmit = async (e) => { - e.preventDefault(); - const message = { - sender: user.user._id, - text: newMessage, - conversationId: currentChat._id, - }; - const receiverId = currentChat.members.find( - (member) => member !== user.user._id - ); - - socket.current.emit("sendMessage", { - senderId: user.user._id, - receiverId, - text: newMessage, - }); + useEffect(() => { + scrollRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + // Opening a chat from the online list adds it to the sidebar if it is new. + const openChat = useCallback((conversation) => { + setCurrentChat(conversation); + setConversations((prev) => (prev.some((c) => c._id === conversation._id) ? prev : [conversation, ...prev])); + }, []); + + const handleSubmit = async (e) => { + e.preventDefault(); + const text = newMessage.trim(); + if (!text || !currentChat) return; try { - const res = await axios.post("/messages", message); - setMessages([...messages, res.data]); + // Persist first, then notify, so a delivered message always exists on the server. + const saved = await sendMessage({ conversationId: currentChat._id, text }); + setMessages((prev) => [...prev, saved]); setNewMessage(""); - } catch (err) { - console.log(err); + const receiverId = currentChat.members.find((member) => member !== user._id); + socket.current?.emit("sendMessage", { senderId: user._id, receiverId, text, conversationId: currentChat._id }); + } catch (error) { + toast.error(getErrorMessage(error, "Could not send the message")); } }; - useEffect(() => { - scrollRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); - return ( - <> -
-
-
- - {conversations.map((c) => ( -
setCurrentChat(c)}> - -
- ))} -
+
+
+
+ + {conversations.map((c) => ( +
setCurrentChat(c)}> + +
+ ))}
-
-
- {currentChat ? ( - <> -
- {messages.map((m) => ( -
- -
- ))} -
-
- - -
- - ) : ( - - Open a conversation to start a chat. - - )} -
+
+
+
+ {currentChat ? ( + <> +
+ {messages.map((m, index) => ( +
+ +
+ ))} +
+
+ + +
+ + ) : ( + Open a conversation to start a chat. + )}
-
-
- -
+
+
+
+
- +
); -} \ No newline at end of file +} diff --git a/client/src/reportWebVitals.js b/client/src/reportWebVitals.js deleted file mode 100644 index 5253d3a..0000000 --- a/client/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/client/src/routes/AppLayout.jsx b/client/src/routes/AppLayout.jsx new file mode 100644 index 0000000..420474d --- /dev/null +++ b/client/src/routes/AppLayout.jsx @@ -0,0 +1,12 @@ +import { Outlet } from "react-router-dom"; +import Navbar from "../components/navbar"; + +// Shared chrome for every logged-in screen; the navbar is declared once here. +const AppLayout = () => ( + <> + + + +); + +export default AppLayout; diff --git a/client/src/routes/NotFound.jsx b/client/src/routes/NotFound.jsx new file mode 100644 index 0000000..f4c9787 --- /dev/null +++ b/client/src/routes/NotFound.jsx @@ -0,0 +1,11 @@ +import { Link } from "react-router-dom"; + +const NotFound = () => ( +
+

Page not found

+

The page you were looking for does not exist.

+ Go to home +
+); + +export default NotFound; diff --git a/client/src/routes/ProtectedRoute.jsx b/client/src/routes/ProtectedRoute.jsx new file mode 100644 index 0000000..866d3fa --- /dev/null +++ b/client/src/routes/ProtectedRoute.jsx @@ -0,0 +1,22 @@ +import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; + +// Renders child routes only for a logged-in user; otherwise redirects to login and remembers +// where the user was going so login can send them back. +export const ProtectedRoute = () => { + const { status } = useAuth(); + const location = useLocation(); + + if (status === "loading") return

Loading…

; + if (status !== "authenticated") return ; + return ; +}; + +// Login/register are pointless for a logged-in user: send them to their profile instead. +export const AnonymousRoute = () => { + const { status, user } = useAuth(); + + if (status === "loading") return null; + if (status === "authenticated") return ; + return ; +}; diff --git a/client/src/utils/avatar.js b/client/src/utils/avatar.js new file mode 100644 index 0000000..1b2c9a1 --- /dev/null +++ b/client/src/utils/avatar.js @@ -0,0 +1,9 @@ +export const DEFAULT_AVATAR = "/images/person/noAvatar.png"; // served from public/ + +export const avatarUrl = (photo) => photo || DEFAULT_AVATAR; + +// onError handler for : fall back to the bundled avatar once, without looping. +export const onAvatarError = (event) => { + if (event.currentTarget.src.endsWith(DEFAULT_AVATAR)) return; + event.currentTarget.src = DEFAULT_AVATAR; +}; diff --git a/client/src/utils/logger.js b/client/src/utils/logger.js new file mode 100644 index 0000000..affeb6d --- /dev/null +++ b/client/src/utils/logger.js @@ -0,0 +1,9 @@ +// Console output that is silent in production builds. +const enabled = process.env.NODE_ENV !== "production"; + +const logger = { + warn: (...args) => enabled && console.warn(...args), + error: (...args) => enabled && console.error(...args), +}; + +export default logger; diff --git a/client/src/widgets/UserWidget.jsx b/client/src/widgets/UserWidget.jsx index 345ba30..0c61817 100644 --- a/client/src/widgets/UserWidget.jsx +++ b/client/src/widgets/UserWidget.jsx @@ -1,71 +1,31 @@ - -import { useTheme } from "@mui/material"; -import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import Card from 'react-bootstrap/Card'; -import ListGroup from 'react-bootstrap/ListGroup'; - -import axios from "axios"; +import { useAuth } from "../context/AuthContext"; +import { avatarUrl, onAvatarError } from "../utils/avatar"; import "../css/widget.css"; -const home = "http://localhost:3001"; +// Sidebar card for the logged-in user. Reads from the auth context; no extra request needed. const UserWidget = () => { - const [user, setUser] = useState([]); - const { palette } = useTheme(); + const { user } = useAuth(); const navigate = useNavigate(); - - // const dark = palette.neutral.dark; - // const medium = palette.neutral.medium; - // const main = palette.neutral.main; - - const getUser = async () => { - try { - let response = await axios.get("/user/profile"); - if (response.status === 200) { - console.log(response.data.user); - setUser(response.data.user); - } - } catch (error) { - console.error("Error fetching user:", error); - } - }; - - useEffect(() => { - getUser(); - }, []); + if (!user) return null; return ( -
+
- https://buffer.com/cdn-cgi/image/w=1000,fit=contain,q=90,f=auto/library/content/images/size/w1200/2023/10/free-images.jpg +
- { - navigate(`/user/profile/${user._id}`); - }} style={{cursor: "pointer"}}>
{user.username}
-

Software Engineer with 5 years of experience in full-stack development.

+ navigate(`/user/profile/${user._id}`)} style={{ cursor: "pointer" }}> +
{user.username}
+
+ {user.name &&

{user.name}

} +

{user.about || "No bio yet. Add one from your profile."}

    -
  • Location: San Francisco
  • -
  • Education: MIT
  • -
  • Interests: Hiking, Reading, Traveling
  • +
  • Connections: {user.friends?.length ?? 0}
  • +
  • Skills: {user.skills?.length ?? 0}
  • +
  • Posts: {user.posts?.length ?? 0}
-
- - - {/* -

{ - navigate(`/user/profile/${user._id}`); - }} - > - {user.username} -

*/}
); }; diff --git a/client/src/widgets/allposts.jsx b/client/src/widgets/allposts.jsx index fdff6a4..a3df279 100644 --- a/client/src/widgets/allposts.jsx +++ b/client/src/widgets/allposts.jsx @@ -1,43 +1,46 @@ -import axios from "axios"; import { useEffect, useState } from "react"; +import { getMyPosts } from "../api/posts"; +import { getErrorMessage } from "../api/client"; -const postCard = (props)=>{ - return
-

JobTitle : {props.jobTitle}

-

CompanyName : {props.companyName}

-

WorkPlace : {props.workPlace}

-

JobLocation : {props.jobLocation}

-

JobType : {props.jobType}

-

Salary : {props.salary}

+const PostCard = ({ post }) => ( +
+

JobTitle : {post.jobTitle}

+

CompanyName : {post.companyName}

+

WorkPlace : {post.workPlace}

+

JobLocation : {post.jobLocation}

+

JobType : {post.jobType}

+

Salary : {post.salary}

+
+); + +// The logged-in user's own job posts. +const Allposts = () => { + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + getMyPosts() + .then((data) => !cancelled && setPosts(data)) + .catch((err) => !cancelled && setError(getErrorMessage(err, "Could not load your posts"))) + .finally(() => !cancelled && setLoading(false)); + return () => { + cancelled = true; + }; + }, []); + + return ( +
+

Your posts

+ {loading &&

Loading…

} + {error &&

{error}

} + {!loading && !error && posts.length === 0 &&

You have not posted any jobs yet.

} + {posts.map((post) => ( + + ))}
-} -const Allposts = ()=>{ - const [posts,setposts] = useState([]); - - const getPosts = async () => { - try { - let response = await axios.get("/user/allposts"); - if (response.status === 200) { - console.log(response.data); - const postsData = await response.data.allposts; // Assuming 'allposts' is a Promise + ); +}; - setposts(postsData); - } - } catch (err) { - console.log(err); - } - }; - useEffect(()=>{ - getPosts(); - },[]); - return ( -
-

Notifications

- { - posts.map(postCard) - } -
- - ) -} -export default Allposts; \ No newline at end of file +export default Allposts; diff --git a/client/src/widgets/button.jsx b/client/src/widgets/button.jsx deleted file mode 100644 index 4629924..0000000 --- a/client/src/widgets/button.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import ButtonBase from '@mui/material/ButtonBase'; -import Typography from '@mui/material/Typography'; - -const images = [ - { - url: 'https://picsum.photos/200', - title: 'Breakfast', - width: '13rem', - }, -]; - -const ImageButton = styled(ButtonBase)(({ theme }) => ({ - position: 'relative', - height: 200, - [theme.breakpoints.down('sm')]: { - width: '100% !important', // Overrides inline-style - height: 100, - }, - '&:hover, &.Mui-focusVisible': { - zIndex: 1, - '& .MuiImageBackdrop-root': { - opacity: 0.15, - }, - '& .MuiImageMarked-root': { - opacity: 0, - }, - '& .MuiTypography-root': { - border: '4px solid currentColor', - }, - }, -})); - -const ImageSrc = styled('span')({ - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - backgroundSize: 'cover', - backgroundPosition: 'center 40%', -}); - -const Image = styled('span')(({ theme }) => ({ - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: theme.palette.common.white, -})); - -const ImageBackdrop = styled('span')(({ theme }) => ({ - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - backgroundColor: theme.palette.common.black, - opacity: 0.4, - transition: theme.transitions.create('opacity'), -})); - -const ImageMarked = styled('span')(({ theme }) => ({ - height: 3, - width: 18, - backgroundColor: theme.palette.common.white, - position: 'absolute', - bottom: -2, - left: 'calc(50% - 9px)', - transition: theme.transitions.create('opacity'), -})); - -export default function ButtonBaseDemo() { - return ( - - {images.map((image) => ( - - - - - `calc(${theme.spacing(1)} + 6px)`, - }} - > - {image.title} - - - - - ))} - - ); -} \ No newline at end of file diff --git a/client/src/widgets/homepage.jsx b/client/src/widgets/homepage.jsx index f50f342..8e6e00a 100644 --- a/client/src/widgets/homepage.jsx +++ b/client/src/widgets/homepage.jsx @@ -1,17 +1,24 @@ - +import { Link } from "react-router-dom"; import "../css/home.css"; -const Homepage = ()=>{ - return ( -
- -
-

Welcome to WorkPlace

-
- - -
-
+ +const HERO_IMAGE = + "https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"; + +const Homepage = () => ( +
+ +
+

Welcome to WorkPlace

+
+ + + + + +
- ) -} -export default Homepage; \ No newline at end of file +
+
+); + +export default Homepage; diff --git a/gui.py b/gui.py deleted file mode 100644 index e9ff607..0000000 --- a/gui.py +++ /dev/null @@ -1,35 +0,0 @@ -# from PIL import Image, ImageTk -import cv2 -import numpy as np -import torch - -# Load the model -model = torch.load('mode.pt', map_location=torch.device('cpu')) # Load model to CPU -model.eval() - -def process_image(image_path): - """Process the image using the model without tensors.""" - # Load image using OpenCV - img = cv2.imread(image_path) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Convert BGR to RGB - - # Resize the image (if needed) to match the model's expected input size - input_size = (640, 640) # Example: Change based on your model's input size - img_resized = cv2.resize(img, input_size) - - # Normalize the image (if required by the model) - img_normalized = img_resized / 255.0 # Scale to [0, 1] - img_normalized = np.expand_dims(img_normalized, axis=0) # Add batch dimension - - # Process with the model - with torch.no_grad(): - output = model(torch.from_numpy(img_normalized).permute(0, 3, 1, 2).float()) # Adjust dimensions for model - output = output.squeeze(0).permute(1, 2, 0).numpy() # Convert back to NumPy - output = (output * 255).astype(np.uint8) # Scale to [0, 255] - - return output - -resultimg = process_image("lion.jpg") - -cv2.imshow(resultimg) - diff --git a/mode.pt b/mode.pt deleted file mode 100644 index 2a5e02b..0000000 Binary files a/mode.pt and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 49b7b6e..9ac16f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1436 +1,331 @@ { - - "name": "Workplace", - + "name": "workplace", "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - - "dependencies": { - - "@mui/icons-material": "^5.14.16", - - "ejs": "^3.1.10", - - "link": "^2.1.0", - - "nodemailer": "^6.9.14", - - "react-spinners": "^0.13.8" - - } - - }, - - "node_modules/@babel/runtime": { - - "version": "7.23.2", - - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - - "dependencies": { - - "regenerator-runtime": "^0.14.0" - - }, - - "engines": { - - "node": ">=6.9.0" - - } - - }, - - "node_modules/@emotion/cache": { - - "version": "11.11.0", - - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - - "peer": true, - - "dependencies": { - - "@emotion/memoize": "^0.8.1", - - "@emotion/sheet": "^1.2.2", - - "@emotion/utils": "^1.2.1", - - "@emotion/weak-memoize": "^0.3.1", - - "stylis": "4.2.0" - - } - - }, - - "node_modules/@emotion/memoize": { - - "version": "0.8.1", - - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - - "peer": true - - }, - - "node_modules/@emotion/sheet": { - - "version": "1.2.2", - - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", - - "peer": true - - }, - - "node_modules/@emotion/utils": { - - "version": "1.2.1", - - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", - - "peer": true - - }, - - "node_modules/@emotion/weak-memoize": { - - "version": "0.3.1", - - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", - - "peer": true - - }, - - "node_modules/@floating-ui/core": { - - "version": "1.5.0", - - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", - - "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", - - "peer": true, - - "dependencies": { - - "@floating-ui/utils": "^0.1.3" - - } - - }, - - "node_modules/@floating-ui/dom": { - - "version": "1.5.3", - - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", - - "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", - - "peer": true, - - "dependencies": { - - "@floating-ui/core": "^1.4.2", - - "@floating-ui/utils": "^0.1.3" - - } - - }, - - "node_modules/@floating-ui/react-dom": { - - "version": "2.0.3", - - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.3.tgz", - - "integrity": "sha512-wOoKUw2P24/OXbNr3bbCqWgoltsyY7lFBDPVtjj/V4WDIJ5hja2C/r+CoWmS+Y75Ahndds3wa7eJRhnJxTCJaQ==", - - "peer": true, - - "dependencies": { - - "@floating-ui/dom": "^1.5.1" - - }, - - "peerDependencies": { - - "react": ">=16.8.0", - - "react-dom": ">=16.8.0" - - } - - }, - - "node_modules/@floating-ui/utils": { - - "version": "0.1.6", - - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", - - "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", - - "peer": true - - }, - - "node_modules/@mui/base": { - - "version": "5.0.0-beta.23", - - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.23.tgz", - - "integrity": "sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.23.2", - - "@floating-ui/react-dom": "^2.0.2", - - "@mui/types": "^7.2.8", - - "@mui/utils": "^5.14.17", - - "@popperjs/core": "^2.11.8", - - "clsx": "^2.0.0", - - "prop-types": "^15.8.1" - + "name": "workplace", + "license": "MIT", + "devDependencies": { + "concurrently": "^9.1.2" }, - "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@types/react": "^17.0.0 || ^18.0.0", - - "react": "^17.0.0 || ^18.0.0", - - "react-dom": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@types/react": { - - "optional": true - - } - + "node": ">=20" } - }, - - "node_modules/@mui/core-downloads-tracker": { - - "version": "5.14.17", - - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.17.tgz", - - "integrity": "sha512-eE0uxrpJAEL2ZXkeGLKg8HQDafsiXY+6eNpP4lcv3yIjFfGbU6Hj9/P7Adt8jpU+6JIhmxvILGj2r27pX+zdrQ==", - - "peer": true, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - } - - }, - - "node_modules/@mui/icons-material": { - - "version": "5.14.16", - - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.16.tgz", - - "integrity": "sha512-wmOgslMEGvbHZjFLru8uH5E+pif/ciXAvKNw16q6joK6EWVWU5rDYWFknDaZhCvz8ZE/K8ZnJQ+lMG6GgHzXbg==", - - "dependencies": { - - "@babel/runtime": "^7.23.2" - - }, - + "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==", + "dev": true, + "license": "MIT", "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@mui/material": "^5.0.0", - - "@types/react": "^17.0.0 || ^18.0.0", - - "react": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@types/react": { - - "optional": true - - } - - } - - }, - - "node_modules/@mui/material": { - - "version": "5.14.17", - - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.17.tgz", - - "integrity": "sha512-+y0VeOLWfEA4Z98We/UH6KCo8+f2HLZDK45FY+sJf8kSojLy3VntadKtC/u0itqnXXb1Pr4wKB2tSIBW02zY4Q==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.23.2", - - "@mui/base": "5.0.0-beta.23", - - "@mui/core-downloads-tracker": "^5.14.17", - - "@mui/system": "^5.14.17", - - "@mui/types": "^7.2.8", - - "@mui/utils": "^5.14.17", - - "@types/react-transition-group": "^4.4.8", - - "clsx": "^2.0.0", - - "csstype": "^3.1.2", - - "prop-types": "^15.8.1", - - "react-is": "^18.2.0", - - "react-transition-group": "^4.4.5" - - }, - - "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@emotion/react": "^11.5.0", - - "@emotion/styled": "^11.3.0", - - "@types/react": "^17.0.0 || ^18.0.0", - - "react": "^17.0.0 || ^18.0.0", - - "react-dom": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@emotion/react": { - - "optional": true - - }, - - "@emotion/styled": { - - "optional": true - - }, - - "@types/react": { - - "optional": true - - } - - } - - }, - - "node_modules/@mui/private-theming": { - - "version": "5.14.17", - - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.17.tgz", - - "integrity": "sha512-u4zxsCm9xmQrlhVPug+Ccrtsjv7o2+rehvrgHoh0siSguvVgVQq5O3Hh10+tp/KWQo2JR4/nCEwquSXgITS1+g==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.23.2", - - "@mui/utils": "^5.14.17", - - "prop-types": "^15.8.1" - - }, - - "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@types/react": "^17.0.0 || ^18.0.0", - - "react": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@types/react": { - - "optional": true - - } - - } - - }, - - "node_modules/@mui/styled-engine": { - - "version": "5.14.17", - - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.17.tgz", - - "integrity": "sha512-AqpVjBEA7wnBvKPW168bNlqB6EN7HxTjLOY7oi275AzD/b1C7V0wqELy6NWoJb2yya5sRf7ENf4iNi3+T5cOgw==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.23.2", - - "@emotion/cache": "^11.11.0", - - "csstype": "^3.1.2", - - "prop-types": "^15.8.1" - - }, - - "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@emotion/react": "^11.4.1", - - "@emotion/styled": "^11.3.0", - - "react": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@emotion/react": { - - "optional": true - - }, - - "@emotion/styled": { - - "optional": true - - } - - } - - }, - - "node_modules/@mui/system": { - - "version": "5.14.17", - - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.17.tgz", - - "integrity": "sha512-Ccz3XlbCqka6DnbHfpL3o3TfOeWQPR+ewvNAgm8gnS9M0yVMmzzmY6z0w/C1eebb+7ZP7IoLUj9vojg/GBaTPg==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.23.2", - - "@mui/private-theming": "^5.14.17", - - "@mui/styled-engine": "^5.14.17", - - "@mui/types": "^7.2.8", - - "@mui/utils": "^5.14.17", - - "clsx": "^2.0.0", - - "csstype": "^3.1.2", - - "prop-types": "^15.8.1" - - }, - - "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@emotion/react": "^11.5.0", - - "@emotion/styled": "^11.3.0", - - "@types/react": "^17.0.0 || ^18.0.0", - - "react": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@emotion/react": { - - "optional": true - - }, - - "@emotion/styled": { - - "optional": true - - }, - - "@types/react": { - - "optional": true - - } - - } - - }, - - "node_modules/@mui/types": { - - "version": "7.2.8", - - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.8.tgz", - - "integrity": "sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==", - - "peer": true, - - "peerDependencies": { - - "@types/react": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@types/react": { - - "optional": true - - } - - } - - }, - - "node_modules/@mui/utils": { - - "version": "5.14.17", - - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.17.tgz", - - "integrity": "sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.23.2", - - "@types/prop-types": "^15.7.9", - - "prop-types": "^15.8.1", - - "react-is": "^18.2.0" - - }, - - "engines": { - - "node": ">=12.0.0" - - }, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/mui" - - }, - - "peerDependencies": { - - "@types/react": "^17.0.0 || ^18.0.0", - - "react": "^17.0.0 || ^18.0.0" - - }, - - "peerDependenciesMeta": { - - "@types/react": { - - "optional": true - - } - - } - - }, - - "node_modules/@popperjs/core": { - - "version": "2.11.8", - - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - - "peer": true, - - "funding": { - - "type": "opencollective", - - "url": "https://opencollective.com/popperjs" - - } - - }, - - "node_modules/@types/prop-types": { - - "version": "15.7.10", - - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", - - "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", - - "peer": true - - }, - - "node_modules/@types/react": { - - "version": "18.2.37", - - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", - - "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", - - "peer": true, - - "dependencies": { - - "@types/prop-types": "*", - - "@types/scheduler": "*", - - "csstype": "^3.0.2" - - } - - }, - - "node_modules/@types/react-transition-group": { - - "version": "4.4.9", - - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.9.tgz", - - "integrity": "sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==", - - "peer": true, - - "dependencies": { - - "@types/react": "*" - + "node": ">=8" } - - }, - - "node_modules/@types/scheduler": { - - "version": "0.16.6", - - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", - - "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==", - - "peer": true - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - + "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - - } - - }, - - "node_modules/async": { - - "version": "3.2.5", - - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" - - }, - - "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==" - - }, - - "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==", - - "dependencies": { - - "balanced-match": "^1.0.0", - - "concat-map": "0.0.1" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - - "node_modules/clsx": { - - "version": "2.0.0", - - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - - "peer": true, - + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - - "node": ">=6" - + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - + "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - - }, - - "node_modules/concat-map": { - - "version": "0.0.1", - - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - - }, - - "node_modules/csstype": { - - "version": "3.1.2", - - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - - "peer": true - - }, - - "node_modules/dom-helpers": { - - "version": "5.2.1", - - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - - "peer": true, - - "dependencies": { - - "@babel/runtime": "^7.8.7", - - "csstype": "^3.0.2" - - } - - }, - - "node_modules/ejs": { - - "version": "3.1.10", - - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - - "dependencies": { - - "jake": "^10.8.5" - + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, - "bin": { - - "ejs": "bin/cli.js" - + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, - "engines": { - - "node": ">=0.10.0" - - } - - }, - - "node_modules/filelist": { - - "version": "1.0.4", - - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - - "dependencies": { - - "minimatch": "^5.0.1" - - } - - }, - - "node_modules/filelist/node_modules/brace-expansion": { - - "version": "2.0.1", - - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - - "dependencies": { - - "balanced-match": "^1.0.0" - + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } - }, - - "node_modules/filelist/node_modules/minimatch": { - - "version": "5.1.6", - - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - - "dependencies": { - - "brace-expansion": "^2.0.1" - - }, - + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", "engines": { - - "node": ">=10" - + "node": "6.* || 8.* || >= 10.*" } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" - } - }, - - "node_modules/jake": { - - "version": "10.9.1", - - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", - - "dependencies": { - - "async": "^3.2.3", - - "chalk": "^4.0.2", - - "filelist": "^1.0.4", - - "minimatch": "^3.1.2" - - }, - - "bin": { - - "jake": "bin/cli.js" - - }, - + "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==", + "dev": true, + "license": "MIT", "engines": { - - "node": ">=10" - + "node": ">=8" } - - }, - - "node_modules/js-tokens": { - - "version": "4.0.0", - - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - - "peer": true - }, - - "node_modules/link": { - - "version": "2.1.0", - - "resolved": "https://registry.npmjs.org/link/-/link-2.1.0.tgz", - - "integrity": "sha512-6jgX7ejPBOQaKsFY/9aFEg0HW0JyFuMGDoN+KQX1W94t+8Fi5xwlSlGouRMDqDlrE46drqu4PYduAG7tUwdF7Q==", - - "bin": { - - "link": "dist/cli.js" - - }, - - "funding": { - - "url": "https://github.com/privatenumber/link?sponsor=1" - + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } - }, - - "node_modules/loose-envify": { - - "version": "1.4.0", - - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - - "peer": true, - + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - - "js-tokens": "^3.0.0 || ^4.0.0" - + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, - - "bin": { - - "loose-envify": "cli.js" - + "funding": { + "url": "https://github.com/sponsors/ljharb" } - }, - - "node_modules/minimatch": { - - "version": "3.1.2", - - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - + "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==", + "dev": true, + "license": "MIT", "dependencies": { - - "brace-expansion": "^1.1.7" - + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "engines": { - - "node": "*" - + "node": ">=8" } - }, - - "node_modules/nodemailer": { - - "version": "6.9.14", - - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", - + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - - "node": ">=6.0.0" - + "node": ">=8" } - }, - - "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==", - - "peer": true, - + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - - "node": ">=0.10.0" - + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } - }, - - "node_modules/prop-types": { - - "version": "15.8.1", - - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - - "peer": true, - - "dependencies": { - - "loose-envify": "^1.4.0", - - "object-assign": "^4.1.1", - - "react-is": "^16.13.1" - + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" } - }, - - "node_modules/prop-types/node_modules/react-is": { - - "version": "16.13.1", - - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - - "peer": true - + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" }, - - "node_modules/react": { - - "version": "18.2.0", - - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - - "peer": true, - + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", "dependencies": { - - "loose-envify": "^1.1.0" - + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "engines": { - - "node": ">=0.10.0" - - } - - }, - - "node_modules/react-dom": { - - "version": "18.2.0", - - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - - "peer": true, - - "dependencies": { - - "loose-envify": "^1.1.0", - - "scheduler": "^0.23.0" - + "node": ">=10" }, - - "peerDependencies": { - - "react": "^18.2.0" - + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } - - }, - - "node_modules/react-is": { - - "version": "18.2.0", - - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - - "peer": true - }, - - "node_modules/react-spinners": { - - "version": "0.13.8", - - "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", - - "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", - - "peerDependencies": { - - "react": "^16.0.0 || ^17.0.0 || ^18.0.0", - - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" - + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" } - }, - - "node_modules/react-transition-group": { - - "version": "4.4.5", - - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - - "peer": true, - + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", "dependencies": { - - "@babel/runtime": "^7.5.5", - - "dom-helpers": "^5.0.1", - - "loose-envify": "^1.4.0", - - "prop-types": "^15.6.2" - + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, - - "peerDependencies": { - - "react": ">=16.6.0", - - "react-dom": ">=16.6.0" - - } - - }, - - "node_modules/regenerator-runtime": { - - "version": "0.14.0", - - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - - }, - - "node_modules/scheduler": { - - "version": "0.23.0", - - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - - "peer": true, - - "dependencies": { - - "loose-envify": "^1.1.0" - + "engines": { + "node": ">=12" } - }, - - "node_modules/stylis": { - - "version": "4.2.0", - - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - - "peer": true - - }, - - "node_modules/supports-color": { - - "version": "7.2.0", - - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - - "dependencies": { - - "has-flag": "^4.0.0" - - }, - + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", "engines": { - - "node": ">=8" - + "node": ">=12" } - } - } - } - diff --git a/package.json b/package.json index 5345852..c557ba0 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,21 @@ { - - "dependencies": { - - "@mui/icons-material": "^5.14.16", - - "ejs": "^3.1.10", - - "link": "^2.1.0", - - "nodemailer": "^6.9.14", - - "react-spinners": "^0.13.8" - + "name": "workplace", + "private": true, + "description": "Dev orchestrator for the Workplace client, API server and socket server. Each app keeps its own package.json.", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "scripts": { + "install:all": "npm install && npm --prefix client install && npm --prefix server install && npm --prefix socket install", + "dev": "concurrently -k -n client,server,socket -c blue,green,magenta \"npm --prefix client start\" \"npm --prefix server run dev\" \"npm --prefix socket run dev\"", + "build": "npm --prefix client run build", + "lint": "npm run lint:client && npm run lint:server && npm run lint:socket", + "lint:client": "npm --prefix client run lint", + "lint:server": "find server -name '*.js' -not -path '*/node_modules/*' -exec node --check {} ';'", + "lint:socket": "find socket -name '*.js' -not -path '*/node_modules/*' -exec node --check {} ';'" + }, + "devDependencies": { + "concurrently": "^9.1.2" } - } - diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..b71c6fb --- /dev/null +++ b/server/.env.example @@ -0,0 +1,24 @@ +# Copy to server/.env (dotenv loads from the server/ working directory, not the repo root). + +# MongoDB connection string (Atlas or local) +MONGO_URL=mongodb://localhost:27017/workplace + +# Secret used to sign and verify JWTs. Generate one with: +# node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" +ACCESS_TOKEN= + +# Cloudinary (profile photos) - https://cloudinary.com/console +CLOUDINARY_CLOUDNAME= +CLOUDINARY_APIKEY= +CLOUDINARY_APISECRET= + +# Gmail account used to send transactional email. +# GMAIL_PASS must be a Google App Password, not the account password: +# https://support.google.com/accounts/answer/185833 +GMAIL_USER= +GMAIL_PASS= + +# Optional +PORT=3001 # API port (the client proxy expects 3001) +CLIENT_URL=http://localhost:3000 # Origin allowed by CORS / cookies +# NODE_ENV=production # Marks the auth cookie Secure (https only) diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..df5a338 --- /dev/null +++ b/server/app.js @@ -0,0 +1,40 @@ +import express from "express"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { env } from "./config/env.js"; +import authRoutes from "./routes/auth.js"; +import userRoutes from "./routes/users.js"; +import profileRoutes from "./routes/profile.js"; +import postRoutes from "./routes/posts.js"; +import { conversationRoutes, messageRoutes } from "./routes/chat.js"; +import emailRoutes from "./routes/email.js"; +import { errorHandler, notFound } from "./middlewares/errorHandler.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const app = express(); + +// credentials:true lets the browser send the httpOnly auth cookie on cross-origin calls; +// that requires a concrete origin, not "*". +app.use(cors({ origin: env.clientUrl, credentials: true })); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); +app.use(express.static(join(__dirname, "public"))); + +// Routes. Paths are the ones the current client calls; postRoutes is mounted before +// userRoutes so /user/allposts is matched before /user/:userId. +app.use("/user", authRoutes); +app.use("/", postRoutes); +app.use("/user", userRoutes); +app.use("/in", profileRoutes); +app.use("/conversations", conversationRoutes); +app.use("/messages", messageRoutes); +app.use("/email", emailRoutes); + +app.use(notFound); +app.use(errorHandler); + +export default app; diff --git a/server/config/conn.js b/server/config/conn.js index 7f00cb0..c10a944 100644 --- a/server/config/conn.js +++ b/server/config/conn.js @@ -1,19 +1,12 @@ -// Desc: connect to mongoDB -import dotenv from 'dotenv'; +import mongoose from "mongoose"; -dotenv.config(); -import { connect as _connect } from "mongoose"; - -const connectDB= async ()=>{ - try{ - const connect= await _connect(process.env.MONGO_URL) - .then(() => { - console.log("db connection success: "); - }) - } - catch(err){ - console.log(err); - } +// Connects to MongoDB. Rejects on failure so the caller can decide to exit; logs later +// connection drops instead of failing silently. +const connectDB = async (uri) => { + mongoose.connection.on("error", (err) => console.error("[mongo] connection error:", err.message)); + mongoose.connection.on("disconnected", () => console.warn("[mongo] disconnected")); + await mongoose.connect(uri); + console.log("[mongo] connected"); }; export default connectDB; diff --git a/server/config/env.js b/server/config/env.js new file mode 100644 index 0000000..b1444fc --- /dev/null +++ b/server/config/env.js @@ -0,0 +1,35 @@ +// Loads server/.env (regardless of the current working directory) and exposes typed config. +// Import this module FIRST in index.js: ESM evaluates imports before the importing module's +// body, so any module that reads process.env at import time must come after this one. +import dotenv from "dotenv"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: join(__dirname, "..", ".env") }); + +const REQUIRED = ["MONGO_URL", "ACCESS_TOKEN"]; +const OPTIONAL_FEATURES = { + "photo upload": ["CLOUDINARY_CLOUDNAME", "CLOUDINARY_APIKEY", "CLOUDINARY_APISECRET"], + "email": ["GMAIL_USER", "GMAIL_PASS"], +}; + +// Throws when a required variable is missing; warns when an optional feature is unconfigured. +export const assertEnv = () => { + const missing = REQUIRED.filter((key) => !process.env[key]); + if (missing.length) { + throw new Error(`Missing required environment variables: ${missing.join(", ")} (see server/.env.example)`); + } + for (const [feature, keys] of Object.entries(OPTIONAL_FEATURES)) { + const absent = keys.filter((key) => !process.env[key]); + if (absent.length) console.warn(`[env] ${feature} disabled: missing ${absent.join(", ")}`); + } +}; + +export const env = { + port: Number(process.env.PORT) || 3001, + clientUrl: process.env.CLIENT_URL || "http://localhost:3000", + mongoUrl: process.env.MONGO_URL, + jwtSecret: process.env.ACCESS_TOKEN, + isProduction: process.env.NODE_ENV === "production", +}; diff --git a/server/controllers/authController.js b/server/controllers/authController.js new file mode 100644 index 0000000..3310487 --- /dev/null +++ b/server/controllers/authController.js @@ -0,0 +1,59 @@ +import bcrypt from "bcrypt"; +import UserDB from "../models/userModel.js"; +import HttpError from "../utils/httpError.js"; +import { signToken } from "../utils/token.js"; +import { authCookieOptions, clearAuthCookieOptions } from "../utils/cookieOptions.js"; + +const SALT_ROUNDS = 10; +const DEFAULT_PHOTO = "/images/person/noAvatar.png"; // served from client/public + +// @route POST /user/register +export const registerUser = async (req, res) => { + const { username, email, password } = req.body; + if (!username || !email || !password) { + throw new HttpError(400, "username, email and password are required"); + } + + const existing = await UserDB.findOne({ $or: [{ email }, { username }] }); + if (existing) { + const field = existing.email === String(email).toLowerCase() ? "email" : "username"; + throw new HttpError(409, `An account with that ${field} already exists`); + } + + const member = await UserDB.create({ + username, + email, + password: await bcrypt.hash(password, SALT_ROUNDS), + photo: DEFAULT_PHOTO, + }); + + res + .cookie("authorization", signToken(member), authCookieOptions) + .status(201) + .json({ success: true, message: "Registration successful", user: member }); +}; + +// @route POST /user/login +export const loginUser = async (req, res) => { + const { email, password } = req.body; + if (!email || !password) throw new HttpError(400, "email and password are required"); + + const user = await UserDB.findOne({ email }).select("+password"); + if (!user) throw new HttpError(401, "No account with that email; please register first"); + + const matches = await bcrypt.compare(password, user.password); + if (!matches) throw new HttpError(401, "Invalid password"); + + res + .cookie("authorization", signToken(user), authCookieOptions) + .status(200) + .json({ success: true, message: "Logged in successfully", user }); +}; + +// @route GET /user/logout +export const logoutUser = (req, res) => { + res + .clearCookie("authorization", clearAuthCookieOptions) + .status(200) + .json({ success: true, message: "Logged out successfully" }); +}; diff --git a/server/controllers/chatController.js b/server/controllers/chatController.js new file mode 100644 index 0000000..d743745 --- /dev/null +++ b/server/controllers/chatController.js @@ -0,0 +1,60 @@ +import Conversation from "../models/Conversation.js"; +import Message from "../models/Message.js"; +import HttpError from "../utils/httpError.js"; + +const isMember = (conversation, userId) => conversation.members.some((m) => String(m) === String(userId)); + +const memberConversationOr403 = async (conversationId, userId) => { + const conversation = await Conversation.findById(conversationId); + if (!conversation || !isMember(conversation, userId)) { + throw new HttpError(403, "Not a member of this conversation"); + } + return conversation; +}; + +// @route POST /conversations — start (or return the existing) conversation with receiverId +export const createConversation = async (req, res) => { + const { receiverId } = req.body; + if (!receiverId) throw new HttpError(400, "receiverId is required"); + const members = [String(req.user.id), String(receiverId)]; + + const existing = await Conversation.findOne({ members: { $all: members } }); + if (existing) return res.status(200).json(existing); + + const created = await Conversation.create({ members }); + res.status(201).json(created); +}; + +// @route GET /conversations/:userId — the caller's conversations (:userId must be the caller) +export const listConversations = async (req, res) => { + if (String(req.params.userId) !== String(req.user.id)) throw new HttpError(403, "Forbidden"); + const conversations = await Conversation.find({ members: { $in: [String(req.user.id)] } }).sort({ updatedAt: -1 }); + res.status(200).json(conversations); +}; + +// @route GET /conversations/find/:firstUserId/:secondUserId — one of the two must be the caller +export const findConversation = async (req, res) => { + const { firstUserId, secondUserId } = req.params; + const me = String(req.user.id); + if (firstUserId !== me && secondUserId !== me) throw new HttpError(403, "Forbidden"); + const conversation = await Conversation.findOne({ members: { $all: [firstUserId, secondUserId] } }); + res.status(200).json(conversation); +}; + +// @route POST /messages — send a message as the caller +export const sendMessage = async (req, res) => { + const { conversationId } = req.body; + const text = typeof req.body.text === "string" ? req.body.text.trim() : ""; + if (!conversationId || !text) throw new HttpError(400, "conversationId and text are required"); + + await memberConversationOr403(conversationId, req.user.id); + const message = await Message.create({ conversationId, sender: String(req.user.id), text }); + res.status(201).json(message); +}; + +// @route GET /messages/:conversationId — messages of a conversation the caller belongs to +export const listMessages = async (req, res) => { + await memberConversationOr403(req.params.conversationId, req.user.id); + const messages = await Message.find({ conversationId: req.params.conversationId }).sort({ createdAt: 1 }); + res.status(200).json(messages); +}; diff --git a/server/controllers/cloudinaryUpload.js b/server/controllers/cloudinaryUpload.js deleted file mode 100644 index 81b14a2..0000000 --- a/server/controllers/cloudinaryUpload.js +++ /dev/null @@ -1,44 +0,0 @@ -import dotenv from "dotenv"; -dotenv.config(); -import UserDB from "../models/userModel.js" - -import multer from "multer"; -import {v2 as cloudinary} from 'cloudinary'; - -cloudinary.config({ - cloud_name: process.env.CLOUDINARY_CLOUDNAME, - api_key: process.env.CLOUDINARY_APIKEY, - api_secret: process.env.CLOUDINARY_APISECRET - }); - -const uploadPhoto = async (req,res) =>{ - const id=req.body.user; - if(req.file===undefined||req.file=== null) - return res.status(300).json({MSG:"No Upload"}); - - const user = await UserDB.findOne({ _id:id }); - if (!user) - return res.status(404).json({ msg: "User not found" }); - else{ - const upload= await cloudUp(req.file.path); - user.photo = upload.secure_url; - await user - .save() - .then(() => res.json({url:upload.secure_url})) - .catch((err) => res.status(400).json("Error: " + err)); - } -} - - -const cloudUp = async(filepath)=>{ - try { - const result = await cloudinary.uploader.upload(filepath); - // console.log(result); - return result; - } catch (error) { - console.error(error); - } - - } -export default uploadPhoto; - diff --git a/server/controllers/createPost.js b/server/controllers/createPost.js deleted file mode 100644 index d5b74d2..0000000 --- a/server/controllers/createPost.js +++ /dev/null @@ -1,37 +0,0 @@ -import BlogDB from '../models/postModel.js'; -import UserDB from "../models/userModel.js"; - -// @route POST /user/jobpostform -// @desc Create new post -const newPost = async function(req,res){ - const {jobTitle,companyName,workPlace,jobLocation,jobType,salary} = req.body; - try{ - let currentUser_id = req.user.id; - const currentUser = await UserDB.findOne({"_id": currentUser_id}); - const post = await BlogDB.create({ - user_id:currentUser_id, - jobTitle: jobTitle, - companyName:companyName, - workPlace:workPlace, - jobLocation:jobLocation, - jobType:jobType, - salary:salary - }); - console.log(post); - await post.save(); - currentUser.posts.push(post); - await currentUser.save(); - console.log(post); - return res.status(200).json({ - message:"Post created SuccessFully" - }) - } - catch(err){ - console.log(err); - res.status(500).json({ - message:"Internal Server Error" - }) - } -}; - -export default newPost; \ No newline at end of file diff --git a/server/controllers/emails/forgetPass.js b/server/controllers/emails/forgetPass.js deleted file mode 100644 index e69de29..0000000 diff --git a/server/controllers/emails/intro.js b/server/controllers/emails/intro.js index 7275936..4bb8c24 100644 --- a/server/controllers/emails/intro.js +++ b/server/controllers/emails/intro.js @@ -1,54 +1,40 @@ // Introductory email to newly registered users import nodemailer from "nodemailer"; -import dotenv from "dotenv"; -import ejs from 'ejs' -dotenv.config(); -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import ejs from "ejs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import HttpError from "../../utils/httpError.js"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TEMPLATE = join(__dirname, "..", "..", "views", "Templates", "intro.ejs"); -// @route = /email/intro -const sendIntroEmail = async (req, res) => { - const { userEmail, userName } = req.body; - try { - const transporter = nodemailer.createTransport({ +// One pooled transporter for the process instead of a new connection per request. +let transporter; +const getTransporter = () => { + if (!transporter) { + transporter = nodemailer.createTransport({ service: "gmail", - host: "smtp.gmail.com", - port: 587, - auth: { - user: process.env.GMAIL_USER, - pass: process.env.GMAIL_PASS, - }, + pool: true, + auth: { user: process.env.GMAIL_USER, pass: process.env.GMAIL_PASS }, }); - ejs.renderFile(`${__dirname}/../../views/Templates/intro.ejs`, - { name: userName }, - function (err, data) { - if (err) { - console.log(err); - } else { - var mainOptions = { - from: process.env.GMAIL_USER, - to: userEmail, - subject: "Welcome to our website", - html: data, - }; - transporter.sendMail(mainOptions, function (err, info) { - if (err) { - console.log(err); - } else { - console.log("Message sent: " + info); - } - }); - } - } - ); - res.status(200).json({ message: "Email sent" }); - } catch (error) { - console.log(error); - res.status(500).json({ message: "Internal server error" }); } + return transporter; +}; + +// @route POST /email/intro — welcome email to the logged-in user +const sendIntroEmail = async (req, res) => { + if (!process.env.GMAIL_USER || !process.env.GMAIL_PASS) { + throw new HttpError(503, "Email is not configured on this server"); + } + const { email, username } = req.user; + const html = await ejs.renderFile(TEMPLATE, { name: username }); + await getTransporter().sendMail({ + from: process.env.GMAIL_USER, + to: email, + subject: "Welcome to Workplace", + html, + }); + res.status(200).json({ success: true, message: "Email sent" }); }; export { sendIntroEmail }; diff --git a/server/controllers/loginControllers.js b/server/controllers/loginControllers.js deleted file mode 100644 index eb9e956..0000000 --- a/server/controllers/loginControllers.js +++ /dev/null @@ -1,148 +0,0 @@ -import checkF from "../middlewares/generateJWT.js"; -import BlogDB from "../models/userModel.js"; -import { genSaltSync, hashSync } from "bcrypt"; -import UserDB from "../models/userModel.js"; -import bcrypt from "bcrypt"; -// @desc Get register form -// @route GET /user/register -// @access Public -export const getRegisterForm = (req, res) => { - res.json({ msg: "register form" }); -}; - -// @desc Register user -// @route POST /user/register -// @access Public -export const registerUser = async (req, res) => { - try { - const { username, email, password } = req.body; - - if (!username || !email || !password) - return res.status(400).json({ error: 'Please provide all required fields.' }); - - // Checking DB for unique Admin - const existingUser = await UserDB.findOne({ email: email }); - console.log(existingUser); - if (existingUser) { - return res.status(400).json({ - success: false, - message: "user already exist", - }); - } - - // hashing and salting password - let salt = genSaltSync(10); - let hashPasscode = hashSync(password, salt); - - // Storing data of user - const member = await UserDB.create({ - username, - email, - password: hashPasscode, - photo: "https://upload.wikimedia.org/wikipedia/commons/4/46/Jennifer_Lawrence_at_the_83rd_Academy_Awards.jpg" - }); - - // Token generation and storage - - console.log(checkF(member)); - - return res - .cookie("authorization", checkF(member), { - httpOnly: true, - secure: false, - }) - .status(200) - .json({ - success: true, - message: "Registration successful!", - user: member, - }); - } catch (err) { - console.log(err); - res.status(500).json({ msg: "Server error REGISTER" }); // render 500 - } -}; - -// @desc Get login form -// @route GET /user/login -// @access Public -export const getLoginForm = (req, res) => { - res.json({ msg: "login form" }); -}; - -// @desc Login user -// @route POST /user/login -// @access Public -//@desc = a post request to verify logged in user -export const loginUser = async (req, res) => { - try { - const { email, password } = req.body; - if (!email || !password) { - return res.status(400).json({ - success: false, - message: "Please provide all required fields", - }); - } - - //Checking DB for unique UserDB and passwd - const user = await UserDB.findOne({ email: email }); - if (!user) - return res.status(401).json({ - success: false, - message: "user is not registered please register first", - }); - - const check = await bcrypt.compare(password, user.password); - if (!check) - return res.status(401).json({ - success: false, - message: "Invalid Password", - }); - // Generation of JWT - if (user && check) { - const token = checkF(user); - return res - .cookie("authorization", token, { - httpOnly: true, - secure: false, - }) - .status(200) - .json({ - success: true, - message: "logged in Successfully", - user: user, - }); - } else { - res.status(401).json({ - success: false, - message: "validation error", - }); // redirect to register - throw new Error("Validation Error"); - } - // token generated - } catch (err) { - console.log(err); // render to 500 - res.status(500).json({ msg: "Server error" }); // render 500 - } -}; - -// @desc Logout user -// @route GET /user/logout -// @access Public -export const logoutUser = (req, res) => { - try{ - - console.log(`logout success from server`); - req.session=null; - // logout user and delete cookie and redirect to login - res.clearCookie("authorization"); - res - .status(200) - .json({ - success: true, - message: "logged out successfully", - }) - }catch{ - res.status(500).json({ msg: "Server error" }); // render 500 - } -}; diff --git a/server/controllers/postController.js b/server/controllers/postController.js new file mode 100644 index 0000000..aa748db --- /dev/null +++ b/server/controllers/postController.js @@ -0,0 +1,71 @@ +import JobPost from "../models/postModel.js"; +import UserDB from "../models/userModel.js"; +import HttpError from "../utils/httpError.js"; +import escapeRegex from "../utils/escapeRegex.js"; + +const MAX_LIST = 100; +const MAX_SEARCH_USERS = 20; +const MAX_SEARCH_POSTS = 50; + +// @route POST /user/jobpostform — create a job post owned by the logged-in user +export const createPost = async (req, res) => { + const { jobTitle, companyName, workPlace, jobLocation, jobType, salary } = req.body; + if (!jobTitle || !companyName) throw new HttpError(400, "jobTitle and companyName are required"); + + const owner = await UserDB.findById(req.user.id); + if (!owner) throw new HttpError(404, "User not found"); + + const post = await JobPost.create({ + user_id: owner._id, + jobTitle, + companyName, + workPlace, + jobLocation, + jobType, + salary: salary === "" || salary === undefined ? undefined : Number(salary), + }); + await UserDB.updateOne({ _id: owner._id }, { $addToSet: { posts: post._id } }); + + res.status(201).json({ success: true, message: "Post created successfully", post }); +}; + +// @route GET /home — newest posts first +export const listAllPosts = async (req, res) => { + const posts = await JobPost.find({}).sort({ createdAt: -1 }).limit(MAX_LIST); + res.status(200).json({ success: true, message: "Posts retrieved", posts }); +}; + +// @route GET /user/allposts — the logged-in user's posts +export const myPosts = async (req, res) => { + const allposts = await JobPost.find({ user_id: req.user.id }).sort({ createdAt: -1 }); + res.status(200).json({ success: true, message: "Posts retrieved", allposts }); +}; + +// @route GET /user/allposts/:userId — any user's posts +export const userPosts = async (req, res) => { + const allposts = await JobPost.find({ user_id: req.params.userId }).sort({ createdAt: -1 }); + res.status(200).json({ success: true, message: "Posts retrieved", allposts }); +}; + +// @route GET /search/:value?jobTitle=&companyName=&jobLocation=&workPlace=&jobType= +// Users matching :value by username, and posts matching the optional filters (or :value +// against jobTitle when no filter is given). Returned separately so callers can tell them apart. +export const search = async (req, res) => { + const value = escapeRegex(req.params.value); + const { jobTitle, companyName, jobLocation, workPlace, jobType } = req.query; + + const postFilter = {}; + if (jobTitle) postFilter.jobTitle = { $regex: escapeRegex(jobTitle), $options: "i" }; + if (companyName) postFilter.companyName = { $regex: escapeRegex(companyName), $options: "i" }; + if (jobLocation) postFilter.jobLocation = { $regex: escapeRegex(jobLocation), $options: "i" }; + if (workPlace) postFilter.workPlace = workPlace; + if (jobType) postFilter.jobType = jobType; + if (Object.keys(postFilter).length === 0) postFilter.jobTitle = { $regex: value, $options: "i" }; + + const [users, posts] = await Promise.all([ + UserDB.find({ username: { $regex: value, $options: "i" } }).limit(MAX_SEARCH_USERS), + JobPost.find(postFilter).limit(MAX_SEARCH_POSTS), + ]); + + res.status(200).json({ success: true, message: "Search results", user: users, posts }); +}; diff --git a/server/controllers/profileController.js b/server/controllers/profileController.js new file mode 100644 index 0000000..9661647 --- /dev/null +++ b/server/controllers/profileController.js @@ -0,0 +1,59 @@ +import UserDB from "../models/userModel.js"; +import HttpError from "../utils/httpError.js"; + +const MAX_FIELD_LENGTH = 500; +const clean = (value) => (typeof value === "string" ? value.trim().slice(0, MAX_FIELD_LENGTH) : ""); + +const findMeOr404 = async (req) => { + const user = await UserDB.findById(req.user.id); + if (!user) throw new HttpError(404, "User not found"); + return user; +}; + +// @route GET /in/:username — public dashboard of a user +export const getDashboard = async (req, res) => { + const user = await UserDB.findOne({ username: req.params.username }); + if (!user) throw new HttpError(404, "User not found"); + res.status(200).json({ success: true, message: "User retrieved", user }); +}; + +// @route POST /in/update — name / about of the logged-in user +export const updateInfo = async (req, res) => { + const name = clean(req.body.name); + const about = clean(req.body.about); + const user = await findMeOr404(req); + if (name) user.name = name; + if (about) user.about = about; + await user.save(); + res.status(200).json({ success: true, message: "success", name: user.name, about: user.about }); +}; + +// @route POST /in/addCollege — append an education entry +export const addEducation = async (req, res) => { + const collegeName = clean(req.body.collegeName); + if (!collegeName) throw new HttpError(400, "collegeName is required"); + const user = await findMeOr404(req); + user.education.push({ collegeName, degree: clean(req.body.degree), year: clean(req.body.year) }); + await user.save(); + res.status(200).json({ success: true, message: "success", education: user.education }); +}; + +// @route POST /in/addWorkEx — append a work-experience entry +export const addWorkEx = async (req, res) => { + const companyName = clean(req.body.companyName); + if (!companyName) throw new HttpError(400, "companyName is required"); + const user = await findMeOr404(req); + user.workexperience.push({ companyName, year: clean(req.body.year) }); + await user.save(); + res.status(200).json({ success: true, message: "success", workexperience: user.workexperience }); +}; + +// @route POST /in/addSkills — append a skill +export const addSkills = async (req, res) => { + const description = clean(req.body.description); + if (!description) throw new HttpError(400, "description is required"); + const user = await findMeOr404(req); + user.skills.push({ description }); + await user.save(); + res.status(200).json({ success: true, message: "success", skills: user.skills }); +}; diff --git a/server/controllers/searchBarApi.js b/server/controllers/searchBarApi.js deleted file mode 100644 index bb15219..0000000 --- a/server/controllers/searchBarApi.js +++ /dev/null @@ -1,65 +0,0 @@ -import UserDB from "../models/userModel.js"; -import BlogDB from "../models/postModel.js"; - -const searchApi = async (req, res) => { - const value = req.params.value.toLowerCase(); - const { jobTitle, companyName, workPlace, jobLocation, jobType } = req.query; - let qObj = {}; - if (jobTitle) qObj.jobTitle = { $regex: jobTitle, $options: "i" }; - if (companyName) qObj.companyName = { $regex: companyName, $options: "i" }; - if (jobLocation) qObj.jobLocation = { $regex: jobLocation, $options: "i" }; - if (workPlace) qObj.workPlace = workPlace; - if (jobType) qObj.jobType = jobType; - - try { - let users = await UserDB.find({ - username: { - $regex: ".*" + value + ".*", - $options: "i", - }, - }); - let result = await BlogDB.find(qObj); - users = users.concat(result); - - if (users) - res.status(200).json({ - message: "Found users", - user: users, - }); - else { - res.status(200).json({ - message: "No users found", - }); - } - } catch (err) { - console.log(err); - res.status(500).json({ - message: "Search Api Failure", - }); - } -}; - -const displayAllPost= async(req,res)=>{ - const posts = await BlogDB.find({}); - console.log(posts); - try{ - if(posts){ - res.status(200).json({ - message:"posts Retrieved", - posts:posts - }) - }else{ - res.status(400).json({ - message:"Unable to retreive posts" - }) - } - }catch(err){ - console.log(err); - res.status(500).json({ - message:"internal Server error" - }) - } -} - -export { searchApi, - displayAllPost }; diff --git a/server/controllers/updateInfoUser.js b/server/controllers/updateInfoUser.js deleted file mode 100644 index 540ed80..0000000 --- a/server/controllers/updateInfoUser.js +++ /dev/null @@ -1,90 +0,0 @@ -import UserDB from "../models/userModel.js"; - -// @desc : Get dashboard of user -// @route : GET /in/update/:username/:typeId -// @type : post request -const updateInfo = async (req, res) => { - const { username } = req.params; - - const { name, about } = req.body; - try { - const user = await UserDB.findOne({ username: username }); - if (!user) - return res.status(404).json({ msg: "User not found" }); - if (name !== undefined && name !== null && name !== "") - user.name = name; - if (about !== undefined && about !== null && about !== "") - user.about = about; - - await user.save(); - } catch (error) { - console.log(error); - return res.status(500).json({ msg: "Server error" }); - } - - // console.log(req.body); - return res.status(200).json({ message: "success",name,about }); -}; - -const addEducation = async (req, res) => { - const { username } = req.params; - username.toString(); - console.log(username); - // console.log(req.body); - const { collegeName, degree, year } = req.body; - try { - const user= await UserDB.findOne({ "username": username}); - if (!user) - return res.status(404).json({ msg: "User not found" }); - user.education.push({ collegeName, degree, year }); - await user.save(); - } - catch (error) { - console.log(error); - return res.status(500).json({ msg: "Server error" }); - } - return res.status(200).json({ message: "success" }); -} - -const addWorkEx = async (req,res) =>{ - const { username } = req.params; - console.log(req.body); - const { companyName, year } = req.body; - try { - const user= await UserDB.findOne({ username: username}); - if (!user) - return res.status(404).json({ msg: "User not found" }); - user.workexperience.push({ companyName, year }); - await user.save(); - } - catch (error) { - console.log(error); - return res.status(500).json({ msg: "Server error" }); - } - return res.status(200).json({ message: "success" }); - -} - -const addSkills = async (req,res) =>{ - const { username } = req.params; - console.log(req.body); - const { description } = req.body; - try { - const user= await UserDB.findOne({ username: username}); - if (!user) - return res.status(404).json({ msg: "User not found" }); - user.skills.push({ description }); - await user.save(); - } - catch (error) { - console.log(error); - return res.status(500).json({ msg: "Server error",description }); - } - return res.status(200).json({ message: "success" }); - -} - -export { updateInfo, -addEducation, -addWorkEx, -addSkills }; diff --git a/server/controllers/uploadController.js b/server/controllers/uploadController.js new file mode 100644 index 0000000..8e4be83 --- /dev/null +++ b/server/controllers/uploadController.js @@ -0,0 +1,26 @@ +import { unlink } from "fs/promises"; +import { v2 as cloudinary } from "cloudinary"; +import UserDB from "../models/userModel.js"; +import HttpError from "../utils/httpError.js"; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUDNAME, + api_key: process.env.CLOUDINARY_APIKEY, + api_secret: process.env.CLOUDINARY_APISECRET, +}); + +// @route POST /in/add — replace the logged-in user's profile photo (multipart field "photo") +export const uploadPhoto = async (req, res) => { + if (!req.file) throw new HttpError(400, "No photo uploaded"); + try { + const user = await UserDB.findById(req.user.id); + if (!user) throw new HttpError(404, "User not found"); + + const result = await cloudinary.uploader.upload(req.file.path, { folder: "workplace/profile" }); + user.photo = result.secure_url; + await user.save(); + res.status(200).json({ success: true, url: result.secure_url }); + } finally { + await unlink(req.file.path).catch(() => {}); // multer's temp file; best effort + } +}; diff --git a/server/controllers/userController.js b/server/controllers/userController.js index 18a8e26..4eb4826 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -1,189 +1,89 @@ import UserDB from "../models/userModel.js"; +import HttpError from "../utils/httpError.js"; -// @desc : Get dashboard of user -// @route : GET /in/:username -const getDashboard = async (req, res) => { - const username=req.params.username.toString(); - let user; - try { - user = await UserDB.findOne({username:username}); - if(!user) - return res.status(404).json({msg:"User not found"}); - - - } catch (error) { - console.log(error); - return res.status(500).json({msg:"Server error"}); - } - console.log(user); - return res.status(200).json(user); -}; - -// @desc : --------For Testing Only-------- -// @route : POST /in/populate -const populateDashboard = async (req, res) => { - const { username,name, email, password } = req.body; - - const form= await UserDB.create({ - username,name,email,password - }); +const asArray = (value) => (Array.isArray(value) ? value : []); +const idsEqual = (a, b) => String(a) === String(b); - res.status(200).json({ msg: "populateDashboard" }); +const findUserOr404 = async (id) => { + const user = await UserDB.findById(id); + if (!user) throw new HttpError(404, "User not found"); + return user; }; -export { - getDashboard, - populateDashboard - +// @route GET /user/profile — the logged-in user +export const getMyProfile = async (req, res) => { + const user = await findUserOr404(req.user.id); + res.status(200).json({ success: true, message: "User profile", user }); }; -// import UserDB from "../models/userModel.js"; -import BlogDB from "../models/postModel.js"; -// Create profile -const createProfile = async function(req,res){ - const {username,About,Education,workExperience,Skills} = req.body; - - try{ - let user_id = req.user.id; - - const new_data = { - "$set": { - "About": About, - "Education": Education, - "workExperience": workExperience, - "Skills": Skills - } - }; - const updatedUser = await UserDB.findOneAndUpdate({"_id" : user_id},new_data); - console.log(updatedUser); - if(updatedUser){ - return res.status(200).json({ - success:true, - message:"Profile created Successfully", - user:updatedUser - }) - }else{ - return console.log("ERROR"); - } - } - catch(err){ - console.log(err); - res.status(400).json({ - success:false, - message:"error while creating Profile" - }) - } - + +// @route GET /user/profile/:userId and GET /user/:userId — any user's public profile +export const getUserById = async (req, res) => { + const user = await findUserOr404(req.params.userId); + res.status(200).json({ success: true, message: "User retrieved", user }); }; -// Profile page -const Profile = async function(req,res){ - try{ - let user_id = req.user.id; - let matchedUser = await UserDB.findOne({"_id": user_id}); - if(matchedUser){ - return res.status(200).json({ - success: true, - message:"User Profile", - user: matchedUser - }) - } - } - catch(err){ - console.log(err); - } +// @route POST /user/createProfile — set about / education / work experience / skills +// Accepts the capitalised keys the current client sends as well as the schema's names. +export const createProfile = async (req, res) => { + const body = req.body || {}; + const update = {}; + const about = body.about ?? body.About; + if (typeof about === "string") update.about = about.trim().slice(0, 500); + if (body.education ?? body.Education) update.education = asArray(body.education ?? body.Education); + if (body.workexperience ?? body.workExperience) { + update.workexperience = asArray(body.workexperience ?? body.workExperience); + } + if (body.skills ?? body.Skills) update.skills = asArray(body.skills ?? body.Skills); + const user = await UserDB.findByIdAndUpdate(req.user.id, { $set: update }, { new: true, runValidators: true }); + if (!user) throw new HttpError(404, "User not found"); + res.status(200).json({ success: true, message: "Profile saved", user }); }; -const allposts = async function(req,res){ - - try{ - let currentUser_id = req.user.id; - const allPosts = await BlogDB.find({"user_id": currentUser_id}); - if(allPosts){ - return res.status(200).json({ - message: "All Posts Recovered", - allposts: allPosts - }); - }else{ - return res.status(200).json({ - message:"User dont have post" - }); - } - } - catch(err){ - console.log(err); - } -}; +// @route GET /user/profile/:userId/connect — send a friend request to :userId +export const sendFriendRequest = async (req, res) => { + const me = req.user.id; + const targetId = req.params.userId; + if (idsEqual(me, targetId)) throw new HttpError(400, "You cannot connect with yourself"); -const anyUserPosts = async function(req,res){ - const userId = req.params.userId.toString(); - try{ - const allPosts = await BlogDB.find({"user_id": userId}); - if(allPosts){ - return res.status(200).json({ - message: "All Posts Recovered", - allposts: allPosts - }); - }else{ - return res.status(200).json({ - message:"User dont have post" - }); - } - } - catch(err){ - res.status(500).json({message:"Internal Server Error"}); - console.log(err); - } -} -const connectFriends = async function(req,res){ - try { - const friends_userid = req.params.userid.toString(); - const user_id = req.user.id.toString(); - const existingUser = await UserDB.findById({"_id": user_id}).populate('friends'); - const friend = await UserDB.findById({ "_id": friends_userid }).populate('friends'); - // const existingRequest = await User.findOne({ - // "_id": user_id, - // "existingUser.friends._id": friends_userid, - // }); - - // if (existingRequest) { - // return res.json({ success: false, message: 'Connection request already sent.' }); - // } - if(user_id === friends_userid){ - return res.json({ success: false, message: 'Same User' }) - } - const FriendRequest = { - from:user_id, - to:friends_userid, - status:'pending', - username: existingUser.username - } - friend.friendRequests.push(FriendRequest); - // Save changes to the database - await Promise.all([existingUser.save(), friend.save()]); - res.status(200).json({ - message: "Friends connected", - user: existingUser - }); - } catch (err) { - console.log(err); - } - + const [sender, target] = await Promise.all([findUserOr404(me), findUserOr404(targetId)]); + if (target.friends.some((id) => idsEqual(id, me))) throw new HttpError(409, "Already connected"); + if (target.friendRequests.some((r) => idsEqual(r.from, me) && r.status === "pending")) { + throw new HttpError(409, "Connection request already sent"); + } + target.friendRequests.push({ from: me, to: targetId, status: "pending", username: sender.username }); + await target.save(); + res.status(200).json({ success: true, message: "Connection request sent", user: sender }); }; -const friends = async function(req,res){ - const friend_id = req.params.friend_id.toString(); - try{ - const friend = await UserDB.findOne({"_id": friend_id}); - res.status(200).json({ - message: "Friends retrieved success", - user: friend - }) - } - catch(err){ - console.log(err); - } +// @route PUT /user/connect/:userId/:friendsId/:status/:requestId +// :userId is the requester, :friendsId is the recipient (must be the caller), +// :status is "Accept" or "Reject", :requestId is the friendRequests sub-document id. +export const respondFriendRequest = async (req, res) => { + const { userId: requesterId, friendsId: recipientId, status, requestId } = req.params; + if (!idsEqual(recipientId, req.user.id)) throw new HttpError(403, "Only the recipient can respond to a request"); + + const recipient = await findUserOr404(recipientId); + const request = recipient.friendRequests.id(requestId); + if (!request || !idsEqual(request.from, requesterId)) throw new HttpError(404, "Friend request not found"); + + const accepted = status.toLowerCase() === "accept"; + if (accepted) { + await Promise.all([ + UserDB.updateOne({ _id: recipientId }, { $addToSet: { friends: requesterId } }), + UserDB.updateOne({ _id: requesterId }, { $addToSet: { friends: recipientId } }), + ]); + } + await UserDB.updateOne({ _id: recipientId }, { $pull: { friendRequests: { _id: requestId } } }); + + const user = await UserDB.findById(recipientId); + res.status(200).json({ success: true, message: accepted ? "Request accepted" : "Request rejected", user }); }; -export {createProfile,Profile,allposts,connectFriends,friends,anyUserPosts}; \ No newline at end of file +// @route GET /user/friends/:userId — id, username and photo of each friend (bare array) +export const listFriends = async (req, res) => { + const user = await UserDB.findById(req.params.userId).populate("friends", "username photo"); + if (!user) throw new HttpError(404, "User not found"); + res.status(200).json(user.friends.map(({ _id, username, photo }) => ({ _id, username, photo }))); +}; diff --git a/server/index.js b/server/index.js index 3a70380..efe1ef9 100644 --- a/server/index.js +++ b/server/index.js @@ -1,60 +1,24 @@ -import dotenv from "dotenv"; - -dotenv.config(); -import express, { static as stat, json as _json } from "express"; -import bodyParser from "body-parser"; -import cookieParser from "cookie-parser"; -import cors from "cors"; -import { createServer } from "http"; -import { Server } from "socket.io"; - -const PORT = 3001; -const app = express(); -app.use(cors()); -const httpServer = createServer(app); -const io = new Server(httpServer, { - cors: { - origin: "*", - }, -}); -import loginRouter from "./routes/loginRouter.js"; -import userRouter from "./routes/userRoutes.js"; -import emailRouter from "./routes/emailRoutes.js"; -import mainRouter from "./routes/mainRoutes.js"; -import upgradeRouter from "./routes/userRouter.js"; -import conversationRoute from "./routes/conversations.js" -import messageRoute from "./routes/messages.js"; -// DB +import { assertEnv, env } from "./config/env.js"; // must stay the first import (loads .env) import connectDB from "./config/conn.js"; -connectDB(); - -// middleware & statics -app.use(cors()); -app.use(express.static("public")); -app.use(express.json()); -// Body-parser middleware -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); -app.use(cookieParser()); -// template engine -app.set("view engine", "ejs"); -app.set("views", "./views"); +import app from "./app.js"; -// routes -app.use("/", mainRouter); -app.use("/email", emailRouter); -app.use("/in", upgradeRouter); -app.use("/user", loginRouter); -app.use("/user", userRouter); - -app.use("/conversations", conversationRoute); -app.use("/messages", messageRoute); - -//404 route -app.use((req, res) => { - res.status(404).send("Sorry can't find that!"); +process.on("unhandledRejection", (reason) => { + console.error("[fatal] unhandled promise rejection:", reason); + process.exit(1); }); +process.on("uncaughtException", (err) => { + console.error("[fatal] uncaught exception:", err); + process.exit(1); +}); + +try { + assertEnv(); + await connectDB(env.mongoUrl); +} catch (err) { + console.error(`[fatal] ${err.message}`); + process.exit(1); +} -app.listen(PORT, () => { - console.log(`Server is now running on http://localhost:${PORT}`); +app.listen(env.port, () => { + console.log(`Server is now running on http://localhost:${env.port}`); }); diff --git a/server/middlewares/errorHandler.js b/server/middlewares/errorHandler.js new file mode 100644 index 0000000..d2bba62 --- /dev/null +++ b/server/middlewares/errorHandler.js @@ -0,0 +1,36 @@ +// JSON 404 for unknown routes. +export const notFound = (req, res) => { + res.status(404).json({ success: false, message: `Route not found: ${req.method} ${req.originalUrl}` }); +}; + +// Single place that turns thrown errors into JSON responses. Mongoose and multer errors are +// mapped to 4xx; anything else is a 500 with the details kept server-side. +// eslint-disable-next-line no-unused-vars +export const errorHandler = (err, req, res, next) => { + if (res.headersSent) return next(err); + + let status = err.status || err.statusCode || 500; + let message = err.message || "Internal server error"; + + if (err.name === "ValidationError") { + status = 400; + message = Object.values(err.errors).map((e) => e.message).join(", "); + } else if (err.name === "CastError") { + status = 400; + message = `Invalid ${err.path}: ${err.value}`; + } else if (err.code === 11000) { + status = 409; + message = `${Object.keys(err.keyValue || {}).join(", ") || "value"} already in use`; + } else if (err.name === "MulterError") { + status = 400; + } + + // Only unexpected errors (no explicit status) are masked and logged with their stack; + // deliberate 5xx such as "email not configured" keep their message. + const unexpected = !err.status && !err.statusCode && status >= 500; + if (unexpected) { + console.error(err); + message = "Internal server error"; + } + res.status(status).json({ success: false, message }); +}; diff --git a/server/middlewares/generateJWT.js b/server/middlewares/generateJWT.js deleted file mode 100644 index d8f9b7e..0000000 --- a/server/middlewares/generateJWT.js +++ /dev/null @@ -1,22 +0,0 @@ -import dotenv from 'dotenv'; -dotenv.config(); -import jsonwebtoken from 'jsonwebtoken'; - -// Generates JWT -const checkF = function (user) { - try { - const token = jsonwebtoken.sign({ - user: { username: user.username, email: user.email, id: user._id } - }, - process.env.ACCESS_TOKEN, - { - expiresIn: "24h" - }); - return token; - } catch (error) { - console.error(error); // Log the error for debugging - throw new Error('Validation Error'); - } -} - -export default checkF; diff --git a/server/middlewares/validateJWT.js b/server/middlewares/validateJWT.js index 485084c..1037982 100644 --- a/server/middlewares/validateJWT.js +++ b/server/middlewares/validateJWT.js @@ -1,32 +1,24 @@ -import jsonwebtoken from 'jsonwebtoken'; -import dotenv from 'dotenv'; - -dotenv.config(); - -const secretKey = process.env.ACCESS_TOKEN; +import jsonwebtoken from "jsonwebtoken"; +// Reads the JWT from the httpOnly `authorization` cookie (set at login) or from an +// `Authorization: Bearer ` header, verifies it, and exposes the payload as req.user. const authenticateToken = (req, res, next) => { + const bearer = req.headers.authorization; + const token = + req.cookies?.authorization || + (bearer && bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : undefined); - if (req.isAuthenticated) { return next() } - const token = req.cookies.authorization; - - if (!token) { - console.log(`Token not found`); - res.status(400).send("token Not found"); - } + if (!token) { + return res.status(401).json({ success: false, message: "Authentication required" }); + } - try { - // Verify the token and decode its payload - const decodedToken = jsonwebtoken.verify(token, secretKey); - - req.user = decodedToken.user; - console.log(`Token verified via JWT`); - // Proceed to the next middleware or route handler - next(); - } catch (error) { - return res.status(403) - .json({ message: 'Invalid token' }); // redirect to / - } -} + try { + const decoded = jsonwebtoken.verify(token, process.env.ACCESS_TOKEN); + req.user = decoded.user; + return next(); + } catch (error) { + return res.status(401).json({ success: false, message: "Invalid or expired token" }); + } +}; export default authenticateToken; diff --git a/server/models/Conversation.js b/server/models/Conversation.js index 68d758b..a5322a3 100644 --- a/server/models/Conversation.js +++ b/server/models/Conversation.js @@ -1,10 +1,11 @@ import { Schema, model } from "mongoose"; -const ConversationSchema = new Schema({ - members:{ - type: Array - } -},{ - timestamps:true -}); -export default model("Conversation",ConversationSchema); \ No newline at end of file +// members are stored as id strings (historical data); `ref` still lets them be populated. +const ConversationSchema = new Schema( + { + members: { type: [{ type: String, ref: "UserDB" }], required: true, index: true }, + }, + { timestamps: true } +); + +export default model("Conversation", ConversationSchema); diff --git a/server/models/Message.js b/server/models/Message.js index 61ef089..2ce025b 100644 --- a/server/models/Message.js +++ b/server/models/Message.js @@ -1,16 +1,12 @@ import { Schema, model } from "mongoose"; -const MessageSchema = new Schema({ - conversationId:{ - type:String - }, - sender:{ - type:String - }, - text:{ - type:String - } -},{ - timestamps:true -}); -export default model("Message",MessageSchema); \ No newline at end of file +const MessageSchema = new Schema( + { + conversationId: { type: String, required: true, index: true }, + sender: { type: String, ref: "UserDB", required: true }, + text: { type: String, required: true, trim: true, maxlength: 2000 }, + }, + { timestamps: true } +); + +export default model("Message", MessageSchema); diff --git a/server/models/postModel.js b/server/models/postModel.js index 73feeae..f2ed77b 100644 --- a/server/models/postModel.js +++ b/server/models/postModel.js @@ -1,27 +1,20 @@ import { Schema, model } from "mongoose"; -const postSchema = Schema({ - user_id:{ - type: Schema.Types.ObjectId, - ref:"User" - }, - jobTitle:{ - type: String - }, - companyName:{ - type:String - }, - workPlace:{ - type:String - }, - jobLocation:{ - type: String - }, - jobType:{ - type: String - }, - salary:{ - type: Number - } -}); -export default model("BlogDB",postSchema); \ No newline at end of file +const postSchema = new Schema( + { + user_id: { type: Schema.Types.ObjectId, ref: "UserDB", required: true, index: true }, + jobTitle: { type: String, required: [true, "jobTitle is required"], trim: true }, + companyName: { type: String, required: [true, "companyName is required"], trim: true }, + workPlace: { type: String, default: "", trim: true }, + jobLocation: { type: String, default: "", trim: true }, + jobType: { type: String, default: "", trim: true }, + salary: { type: Number, min: 0 }, + }, + { + timestamps: true, + // The model used to be called BlogDB; keep its collection so existing data stays readable. + collection: "blogdbs", + } +); + +export default model("JobPost", postSchema); diff --git a/server/models/userModel.js b/server/models/userModel.js index c5ec3ce..1426c8f 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -1,6 +1,6 @@ import { Schema, model } from "mongoose"; -const userSchema = Schema({ +const userSchema = new Schema({ username:{ type: String, required:[true,"Enter your username"], @@ -12,8 +12,10 @@ const userSchema = Schema({ }, email:{ type: String, - default: '', - required:[true,"ENTER YOUR EMAIL"] + required:[true,"ENTER YOUR EMAIL"], + unique:true, + lowercase:true, + trim:true }, photo: { @@ -22,8 +24,8 @@ const userSchema = Schema({ }, password:{ type:String, - default: '' - // required:[true,"Enter your Password"] + default: '', + select: false // never loaded unless explicitly asked for with .select('+password') }, googleId:{ type:String, @@ -66,23 +68,23 @@ const userSchema = Schema({ }], friends:[{ type: Schema.Types.ObjectId, - default:[], ref:"UserDB" }], friendRequests:[{ from:{ type: Schema.Types.ObjectId, - default:[], - ref:"UserDB" + ref:"UserDB", + required:true }, to:{ type: Schema.Types.ObjectId, - default:[], - ref:"UserDB" + ref:"UserDB", + required:true }, status:{ type:String, - enum:['pending','accepted','rejected'] + enum:['pending','accepted','rejected'], + default:'pending' }, username:{ type:String, @@ -90,11 +92,16 @@ const userSchema = Schema({ }], posts:[{ type: Schema.Types.ObjectId, - default:[], - ref:"BlogDB" + ref:"JobPost" }], },{ - timestamps:true + timestamps:true, + toJSON: { + transform(doc, ret) { + delete ret.password; // belt-and-braces for documents loaded with +password + return ret; + } + } }); export default model("UserDB",userSchema); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index e0e1a44..e72e0fb 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -7,33 +7,32 @@ "": { "name": "server", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "bcrypt": "^5.1.1", - "body-parser": "^1.20.2", "cloudinary": "^2.0.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "ejs": "^3.1.10", "express": "^4.18.2", - "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.14", - "nodemon": "^3.0.1", - "passport": "^0.7.0", - "path": "^0.12.7", - "socket.io": "^4.7.2", - "url": "^0.11.3", - "uuid": "^9.0.1" + "nodemailer": "^6.9.14" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=20" } }, "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", @@ -49,78 +48,41 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6", - "uuid": "^9.0.1" - } - }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz", - "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.5.2.tgz", + "integrity": "sha512-UBvCBdPHAmiiDNEpD2ORBGzna4qYr06fLELfGDPW3/KZ9uLK+cGT9npAc/x/6/8SLzdbILMuc3HWFQ0FvJMhCw==", + "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "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==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.8.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@types/webidl-conversions": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.2.tgz", - "integrity": "sha512-uNv6b/uGRLlCVmelat2rA8bcVd3k/42mV2EmjhPh6JLkd35T5bgwR/t6xy7a9MWhd9sixIeBUzhBenvk3NO+DQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" }, "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", "dependencies": { - "@types/node": "*", "@types/webidl-conversions": "*" } }, "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==" + "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" @@ -133,6 +95,7 @@ "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" }, @@ -141,11 +104,12 @@ } }, "node_modules/agent-base/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -157,36 +121,26 @@ } }, "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "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" @@ -198,17 +152,21 @@ "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" }, "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==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "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" @@ -221,6 +179,7 @@ "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", @@ -233,31 +192,27 @@ "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==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "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==" - }, - "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==", - "engines": { - "node": "^4.5.0 || >= 5.9" - } + "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" @@ -267,30 +222,36 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "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.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "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.11.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -298,29 +259,32 @@ } }, "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==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "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.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/bson": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz", - "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", "engines": { "node": ">=16.20.1" } @@ -328,12 +292,14 @@ "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==" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/busboy": { "version": "1.6.0", @@ -350,72 +316,46 @@ "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": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "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-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "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", @@ -428,6 +368,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -436,51 +379,28 @@ "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/cloudinary": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.0.0.tgz", - "integrity": "sha512-NUbufckg2GiuJjB0DVGFBRNLt4jl+2nOA1KeZv1ub8FCduNFvkcdGF1t9JKeoVVM8Po9wYvjICcecMsjQVq1Jw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.11.0.tgz", + "integrity": "sha512-o2pAkDDqrzPFE/mn9GCfFjBIGQmPs++Ts6rtkFl4vY528DNHiMovKWjH16HBjeGa9iwI7eAad24aWN2emT0aww==", + "license": "MIT", "dependencies": { - "cloudinary-core": "^2.13.0", - "lodash": "^4.17.21", - "q": "^1.5.1" + "lodash": "^4.17.23" }, "engines": { "node": ">=9" } }, - "node_modules/cloudinary-core": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/cloudinary-core/-/cloudinary-core-2.13.0.tgz", - "integrity": "sha512-Nt0Q5I2FtenmJghtC4YZ3MZZbGg1wLm84SsxcuVwZ83OyJqG9CNIGp86CiI6iDv3QobaqBUpOT7vg+HqY5HxEA==", - "peerDependencies": { - "lodash": ">=4.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "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" } @@ -488,7 +408,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -497,6 +418,7 @@ "engines": [ "node >= 0.8" ], + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -507,12 +429,14 @@ "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==" + "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" }, @@ -524,93 +448,82 @@ "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.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "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/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "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==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "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==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "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/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "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" } @@ -619,34 +532,52 @@ "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.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "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" } @@ -654,12 +585,14 @@ "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==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -673,80 +606,23 @@ "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==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "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==", + "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/engine.io": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", - "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "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" } @@ -755,6 +631,19 @@ "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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { "node": ">= 0.4" } @@ -762,125 +651,79 @@ "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==" + "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.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.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", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "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.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "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" }, @@ -889,16 +732,17 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -909,6 +753,7 @@ "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" } @@ -917,6 +762,7 @@ "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" } @@ -925,6 +771,7 @@ "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" }, @@ -936,6 +783,7 @@ "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" }, @@ -946,13 +794,16 @@ "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==" + "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" @@ -965,6 +816,7 @@ "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" } @@ -973,6 +825,8 @@ "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", @@ -989,15 +843,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "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", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "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" @@ -1006,10 +866,25 @@ "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": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1029,6 +904,8 @@ "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" }, @@ -1036,40 +913,33 @@ "node": ">= 6" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "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==", - "engines": { - "node": ">=4" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { - "es-define-property": "^1.0.0" + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "*" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "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" }, @@ -1077,10 +947,21 @@ "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.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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" }, @@ -1091,12 +972,14 @@ "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==" + "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==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1104,30 +987,31 @@ "node": ">= 0.4" } }, - "node_modules/http": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", - "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" - }, "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "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" @@ -1137,11 +1021,12 @@ } }, "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1153,14 +1038,16 @@ } }, "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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" }, @@ -1171,12 +1058,16 @@ "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==" + "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" @@ -1185,12 +1076,14 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "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" } @@ -1199,6 +1092,8 @@ "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" }, @@ -1210,6 +1105,8 @@ "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" } @@ -1218,6 +1115,7 @@ "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" } @@ -1226,6 +1124,8 @@ "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" }, @@ -1237,6 +1137,8 @@ "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" } @@ -1244,17 +1146,18 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" }, "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", + "async": "^3.2.6", "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" @@ -1264,11 +1167,12 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -1287,90 +1191,92 @@ "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==" + "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "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==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "node_modules/kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "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==" + "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==" + "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==" + "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==" + "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==" + "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==" + "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==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } + "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" }, @@ -1385,14 +1291,25 @@ "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" } @@ -1400,17 +1317,23 @@ "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "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" } @@ -1419,6 +1342,7 @@ "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" }, @@ -1430,6 +1354,7 @@ "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" } @@ -1438,6 +1363,7 @@ "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" }, @@ -1446,20 +1372,22 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1468,6 +1396,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", "engines": { "node": ">=8" } @@ -1476,6 +1405,7 @@ "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" @@ -1488,6 +1418,7 @@ "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" }, @@ -1499,6 +1430,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -1507,24 +1439,25 @@ } }, "node_modules/mongodb": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.2.0.tgz", - "integrity": "sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.0", - "bson": "^6.2.0", - "mongodb-connection-string-url": "^2.6.0" + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -1552,26 +1485,28 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongoose": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.0.tgz", - "integrity": "sha512-PzwkLgm1Jhj0NQdgGfnFsu0QP9V1sBFgbavEgh/IPAUzKAagzvEhuaBuAQOQGjczVWnpIU9tBqyd02cOTgsPlA==", - "dependencies": { - "bson": "^6.2.0", - "kareem": "2.5.1", - "mongodb": "6.2.0", + "version": "8.24.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.4.tgz", + "integrity": "sha512-DoV8hYy5ssoh/j99rhYzeS2BLLcLJZ+EGQYfe/Ln+yMorBy0wgKdlwJBj59zJ1N0MnEKQypbeqUYFB0FVThmYg==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", - "sift": "16.0.1" + "sift": "17.1.3" }, "engines": { "node": ">=16.20.1" @@ -1584,12 +1519,14 @@ "node_modules/mongoose/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==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -1598,6 +1535,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", "dependencies": { "debug": "4.x" }, @@ -1606,11 +1544,12 @@ } }, "node_modules/mquery/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1622,19 +1561,23 @@ } }, "node_modules/mquery/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.0.0", @@ -1652,6 +1595,7 @@ "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" } @@ -1659,12 +1603,14 @@ "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==" + "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" }, @@ -1683,39 +1629,45 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/node-fetch/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==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/node-fetch/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/nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } }, "node_modules/nodemon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", - "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", - "debug": "^3.2.7", + "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", @@ -1734,23 +1686,75 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "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==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", "dependencies": { "abbrev": "1" }, @@ -1758,13 +1762,15 @@ "nopt": "bin/nopt.js" }, "engines": { - "node": "*" + "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" } @@ -1773,6 +1779,8 @@ "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", @@ -1784,14 +1792,19 @@ "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.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "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" } @@ -1800,6 +1813,7 @@ "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" }, @@ -1811,6 +1825,7 @@ "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" } @@ -1819,66 +1834,38 @@ "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/passport": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", - "dependencies": { - "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-strategy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, "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.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" }, - "node_modules/pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1886,23 +1873,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" }, "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" @@ -1914,31 +1895,27 @@ "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==" + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -1951,19 +1928,21 @@ "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==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -1973,6 +1952,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1986,12 +1966,15 @@ "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" }, "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" }, @@ -2003,6 +1986,8 @@ "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" }, @@ -2030,20 +2015,20 @@ "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==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2052,23 +2037,24 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -2077,17 +2063,19 @@ "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==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -2096,38 +2084,26 @@ "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==" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } + "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==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2136,118 +2112,98 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" - }, - "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==" - }, - "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==", + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", "dependencies": { - "semver": "^7.5.3" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, "engines": { - "node": ">=10" - } - }, - "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" + "node": ">= 0.4" }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", - "dependencies": { - "ws": "~8.11.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=10.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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": { - "ms": "2.1.2" + "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": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "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": { - "ms": "2.1.2" + "semver": "^7.5.3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=10" } }, - "node_modules/socket.io/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2264,6 +2220,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -2271,12 +2228,14 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" }, "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", @@ -2290,6 +2249,7 @@ "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" }, @@ -2301,6 +2261,8 @@ "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" }, @@ -2309,9 +2271,11 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -2328,6 +2292,7 @@ "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" }, @@ -2339,6 +2304,8 @@ "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" }, @@ -2350,36 +2317,38 @@ "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.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dependencies": { - "nopt": "~1.0.10" - }, + "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": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "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" @@ -2391,96 +2360,45 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "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/url": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", - "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.11.2" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - }, - "node_modules/url/node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dependencies": { - "inherits": "2.0.3" - } - }, "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==" - }, - "node_modules/util/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + "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/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "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" } @@ -2489,26 +2407,29 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "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" } @@ -2516,32 +2437,14 @@ "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==" - }, - "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "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 - } - } + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } @@ -2549,7 +2452,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/server/package.json b/server/package.json index 23afed5..c5e5dac 100644 --- a/server/package.json +++ b/server/package.json @@ -1,34 +1,32 @@ { "name": "server", "version": "1.0.0", - "description": "", + "description": "Workplace REST API (Express + Mongoose)", "main": "index.js", "type": "module", "scripts": { - "start": "nodemon index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node index.js", + "dev": "nodemon index.js" }, "author": "", - "license": "ISC", + "license": "MIT", "dependencies": { "bcrypt": "^5.1.1", - "body-parser": "^1.20.2", "cloudinary": "^2.0.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "ejs": "^3.1.10", "express": "^4.18.2", - "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.14", - "nodemon": "^3.0.1", - "passport": "^0.7.0", - "path": "^0.12.7", - "socket.io": "^4.7.2", - "url": "^0.11.3", - "uuid": "^9.0.1" + "nodemailer": "^6.9.14" + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "nodemon": "^3.0.1" } } diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..427989d --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,12 @@ +// Mounted at /user — registration, login, logout +import { Router } from "express"; +import asyncHandler from "../utils/asyncHandler.js"; +import { loginUser, logoutUser, registerUser } from "../controllers/authController.js"; + +const router = Router(); + +router.post("/register", asyncHandler(registerUser)); +router.post("/login", asyncHandler(loginUser)); +router.get("/logout", logoutUser); + +export default router; diff --git a/server/routes/chat.js b/server/routes/chat.js new file mode 100644 index 0000000..eb11ee1 --- /dev/null +++ b/server/routes/chat.js @@ -0,0 +1,23 @@ +// conversationRoutes mounted at /conversations, messageRoutes at /messages. +// Both require a logged-in user; controllers enforce conversation membership. +import { Router } from "express"; +import authenticateToken from "../middlewares/validateJWT.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { + createConversation, + findConversation, + listConversations, + listMessages, + sendMessage, +} from "../controllers/chatController.js"; + +export const conversationRoutes = Router(); +conversationRoutes.use(authenticateToken); +conversationRoutes.post("/", asyncHandler(createConversation)); +conversationRoutes.get("/find/:firstUserId/:secondUserId", asyncHandler(findConversation)); +conversationRoutes.get("/:userId", asyncHandler(listConversations)); + +export const messageRoutes = Router(); +messageRoutes.use(authenticateToken); +messageRoutes.post("/", asyncHandler(sendMessage)); +messageRoutes.get("/:conversationId", asyncHandler(listMessages)); diff --git a/server/routes/conversations.js b/server/routes/conversations.js deleted file mode 100644 index bb02b24..0000000 --- a/server/routes/conversations.js +++ /dev/null @@ -1,45 +0,0 @@ -import Router from "express"; -const ConversationRoute = Router(); -import Conversation from "../models/Conversation.js"; - -//new Conv -ConversationRoute.post("/",async (req,res)=>{ - const newConversation = new Conversation({ - members:[req.body.senderId,req.body.receiverId] - }) - - try{ - const savedConversation = await newConversation.save(); - res.status(200).json(savedConversation); - }catch(err){ - res.status(500).json(err); - } -}) -//get conv of user -ConversationRoute.get("/:userId",async (req,res)=>{ - - try{ - - const conversation = await Conversation.find({ - members:{ $in :[req.params.userId]} - }); - res.status(200).json(conversation); - }catch(err){ - res.status(500).json(err); - } -}) - -// get conv includes two userId - -ConversationRoute.get("/find/:firstUserId/:secondUserId", async (req, res) => { - try { - const conversation = await Conversation.findOne({ - members: { $all: [req.params.firstUserId, req.params.secondUserId] }, - }); - res.status(200).json(conversation) - } catch (err) { - res.status(500).json(err); - } - }); - -export default ConversationRoute; \ No newline at end of file diff --git a/server/routes/email.js b/server/routes/email.js new file mode 100644 index 0000000..e72b028 --- /dev/null +++ b/server/routes/email.js @@ -0,0 +1,12 @@ +// Mounted at /email +import { Router } from "express"; +import authenticateToken from "../middlewares/validateJWT.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { sendIntroEmail } from "../controllers/emails/intro.js"; + +const router = Router(); + +// Welcome email to the logged-in user. The recipient comes from the token, never the body. +router.post("/intro", authenticateToken, asyncHandler(sendIntroEmail)); + +export default router; diff --git a/server/routes/emailRoutes.js b/server/routes/emailRoutes.js deleted file mode 100644 index 077c751..0000000 --- a/server/routes/emailRoutes.js +++ /dev/null @@ -1,10 +0,0 @@ -import Router from "express"; -import { sendIntroEmail } from "../controllers/emails/intro.js"; -const emailRouter = Router(); - -// route begins with /email -// @route = /email/intro - -emailRouter.route("/intro").post(sendIntroEmail); - -export default emailRouter; \ No newline at end of file diff --git a/server/routes/loginRouter.js b/server/routes/loginRouter.js deleted file mode 100644 index 6750baa..0000000 --- a/server/routes/loginRouter.js +++ /dev/null @@ -1,15 +0,0 @@ -import Router from "express"; -import {loginUser, logoutUser ,registerUser} from '../controllers/loginControllers.js'; -import authenticateToken from "../middlewares/validateJWT.js"; -const loginRouter = Router(); - -// route begins with 'user' -loginRouter.route('/register').post(registerUser); -loginRouter.route('/login').post(loginUser); - -loginRouter.route('/logout') - .get(logoutUser); - - -export default loginRouter; - diff --git a/server/routes/mainRoutes.js b/server/routes/mainRoutes.js deleted file mode 100644 index a68ff93..0000000 --- a/server/routes/mainRoutes.js +++ /dev/null @@ -1,65 +0,0 @@ -import Router from "express"; -import UserDB from "../models/userModel.js"; -import BlogDB from "../models/postModel.js"; -import authenticateToken from "../middlewares/validateJWT.js"; -const mainRouter = Router(); -//@desc search Bar get request to search user -//@route /search/:username -mainRouter.get("/search/:username",async function(req,res){ - const username = req.params.username.toLowerCase(); - try{ - - const users = await UserDB.find({"username": {$regex:username,$options:"i"}}); - if(users) - res.status(200).json({ - message: "Found users", - user:users - }) - else{ - res.status(200).json({ - message:"No users found" - }) - } - } - catch(err){ - console.log(err); - } -}); -import { displayAllPost, searchApi } from "../controllers/searchBarApi.js"; -// const mainRouter = Router(); - -mainRouter.get("/search/:value",searchApi); - -//@desc to get another user profile through userid -mainRouter.get("/user/profile/:userid",authenticateToken,async function(req,res){ - const userid = req.params.userid.toString(); - try{ - const user = await UserDB.findOne({"_id":userid}); - console.log(user); - if(user){ - res.status(200).json({ - message:"user info retrieved success", - user: user - }) - } - else{ - res.status(404).json({ - message: "User not found", - }); - } - } - catch(err){ - - res.status(500).json({ - message:"Internal Server error", - }) - } -}) - -//@desc to get all user posts -//@route /home - -mainRouter.get('/home', displayAllPost ) - - -export default mainRouter; \ No newline at end of file diff --git a/server/routes/messages.js b/server/routes/messages.js deleted file mode 100644 index ff57a16..0000000 --- a/server/routes/messages.js +++ /dev/null @@ -1,28 +0,0 @@ -import Router from "express"; -const messageRoute = Router(); -import Message from "../models/Message.js"; - -//add -messageRoute.post("/",async (req,res)=>{ - const newMessage = new Message(req.body); - try{ - const savedMessage = await newMessage.save(); - res.status(200).json(savedMessage); - }catch(err){ - res.status(500).json(err); - } -}) - -//get -messageRoute.get("/:conversationId",async (req,res)=>{ - try{ - const messages = await Message.find({ - conversationId: req.params.conversationId - }) - console.log(messages); - res.status(200).json(messages); - }catch(err){ - res.status(500).json(err); - } -}) -export default messageRoute; \ No newline at end of file diff --git a/server/routes/posts.js b/server/routes/posts.js new file mode 100644 index 0000000..1dcc083 --- /dev/null +++ b/server/routes/posts.js @@ -0,0 +1,15 @@ +// Mounted at / — job posts and search. Paths are the legacy ones the client calls. +import { Router } from "express"; +import authenticateToken from "../middlewares/validateJWT.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { createPost, listAllPosts, myPosts, search, userPosts } from "../controllers/postController.js"; + +const router = Router(); + +router.get("/home", asyncHandler(listAllPosts)); +router.get("/search/:value", asyncHandler(search)); +router.post("/user/jobpostform", authenticateToken, asyncHandler(createPost)); +router.get("/user/allposts", authenticateToken, asyncHandler(myPosts)); +router.get("/user/allposts/:userId", authenticateToken, asyncHandler(userPosts)); + +export default router; diff --git a/server/routes/profile.js b/server/routes/profile.js new file mode 100644 index 0000000..7a5606f --- /dev/null +++ b/server/routes/profile.js @@ -0,0 +1,33 @@ +// Mounted at /in — profile editing for the logged-in user, plus the public dashboard +import { Router } from "express"; +import multer from "multer"; +import authenticateToken from "../middlewares/validateJWT.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { uploadPhoto } from "../controllers/uploadController.js"; +import { addEducation, addSkills, addWorkEx, getDashboard, updateInfo } from "../controllers/profileController.js"; + +const router = Router(); + +const MAX_PHOTO_BYTES = 5 * 1024 * 1024; +const ALLOWED_PHOTO_TYPES = ["image/jpeg", "image/jpg", "image/png"]; + +const photoUpload = multer({ + storage: multer.diskStorage({}), + limits: { fileSize: MAX_PHOTO_BYTES }, + fileFilter: (req, file, cb) => { + if (ALLOWED_PHOTO_TYPES.includes(file.mimetype)) return cb(null, true); + cb(new multer.MulterError("LIMIT_UNEXPECTED_FILE", "Only JPEG and PNG images are allowed")); + }, +}).single("photo"); + +// Writes target req.user, never a user named in the URL. The optional :username segment is +// kept only so the current client keeps working; it is ignored. +router.post("/add", authenticateToken, photoUpload, asyncHandler(uploadPhoto)); +router.post("/update/:username?", authenticateToken, asyncHandler(updateInfo)); +router.post("/addCollege/:username?", authenticateToken, asyncHandler(addEducation)); +router.post("/addWorkEx/:username?", authenticateToken, asyncHandler(addWorkEx)); +router.post("/addSkills/:username?", authenticateToken, asyncHandler(addSkills)); + +router.get("/:username", asyncHandler(getDashboard)); + +export default router; diff --git a/server/routes/userRouter.js b/server/routes/userRouter.js deleted file mode 100644 index 2cc9974..0000000 --- a/server/routes/userRouter.js +++ /dev/null @@ -1,40 +0,0 @@ -// @route = /in -import { Router } from "express"; -const upgradeRouter = Router(); -import uploadPhoto from "../controllers/cloudinaryUpload.js"; - -import { - getDashboard, - populateDashboard, -} from "../controllers/userController.js"; -import { addEducation, addSkills, addWorkEx, updateInfo } from "../controllers/updateInfoUser.js"; -import multer from "multer"; - -const fileFilter = (req, file, cb) => { - const allowedFileTypes = ["image/jpeg", "image/jpg", "image/png"]; - if (allowedFileTypes.includes(file.mimetype)) { - cb(null, true); - } else { - cb(null, false); - } -}; - -let uploader = multer({ storage:multer.diskStorage({}) -, fileFilter -}); - -upgradeRouter.route("/add").post(uploader.single("photo"), uploadPhoto); - -// @route = /in - -upgradeRouter.route("/update/:username").post(updateInfo); -upgradeRouter.route("/addCollege/:username").post(addEducation); -upgradeRouter.route("/addWorkEx/:username").post(addWorkEx); -upgradeRouter.route("/addSkills/:username").post(addSkills); - -upgradeRouter.route("/populate").post(populateDashboard); -upgradeRouter.route("/:username").get(getDashboard); - -// upgradeRouter.get("/profile/:id", userController.getProfileById); - -export default upgradeRouter; diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js deleted file mode 100644 index 07c0709..0000000 --- a/server/routes/userRoutes.js +++ /dev/null @@ -1,173 +0,0 @@ -import Router from "express"; -import createPost from "../controllers/createPost.js"; -import UserDB from "../models/userModel.js"; -import BlogDB from "../models/postModel.js"; -import authenticateToken from "../middlewares/validateJWT.js"; -import { createProfile,Profile,allposts,connectFriends,friends,anyUserPosts } from "../controllers/userController.js"; -const userRouter = Router(); - -//route begins with /user - -//@desc post request to create Profile -//@route /user/createProfile -userRouter.post('/createProfile',authenticateToken,createProfile); -//@desc get req to get user Profile -//@route /user/profile -userRouter.get("/profile",authenticateToken,Profile); -//@desc post req to createPost -//@route /user/jobpostform -userRouter.post('/jobpostform',authenticateToken,createPost); - -//@desc get req to get allposts of any user -//@route /user/allposts -userRouter.get('/allposts/:userId',authenticateToken,anyUserPosts); -// //@desc get req to get allposts of a currently logged in user -// //@route /user/allposts -// userRouter.get('/allposts',authenticateToken,allposts); - -//@desc get req to send request -//@route /user/profile/:userid/connect -userRouter.get("/profile/:userid/connect",authenticateToken,connectFriends); -//@desc get req to connect with friends -userRouter.put("/connect/:userId/:friendsId/:status/:id", authenticateToken,async function(req,res){ - const userId = req.params.userId.toString(); - const friendsId = req.params.friendsId.toString(); - const status = req.params.status; - const friendRequestsId = req.params.id.toString(); - console.log(userId,friendsId,status,friendRequestsId); - try{ - const User = await UserDB.findById({"_id":userId}).populate() - const Friend = await UserDB.findById({"_id":friendsId}).populate() - console.log(Friend); - if(status === "Accept"){ - // if(!Friend.friends.includes(userId)){ - await User.updateOne({ - $push:{ - friends: friendsId - } - }) - await Friend.updateOne({ - $push:{ - friends: userId - } - }) - const FriendRequest = { - // "_id":friendRequestsId, - from: userId, - to: friendsId, - status:'pending', - username: User.username - } - await Friend.updateOne({ - $pull:{ - friendRequests: FriendRequest - } - }, - { - "new":true - } - ) - res.status(200).json({ - message: "Request Accepted", - user: User - }) - // }else{ - // res.status(100).json({ - // message: "Already Friend" - // }) - // } - } - else{ - // Save changes to the database - const FriendRequest = { - // "_id":friendRequestsId, - from: userId, - to: friendsId, - status:'pending', - username: User.username - } - await Friend.updateOne({ - $pull:{ - friendRequests: FriendRequest - } - }, - { - "new":true - } - ) - await Friend.updateOne({ - $pull:{ - friendRequests: friendRequestsId - } - }) - res.status(200).json({ - message:"Request Rejected", - user: User - }) - } - // existingUser.friends.push(friend); - // friend.friends.push(existingUser); - - // // Save changes to the database - // await Promise.all([existingUser.save(), friend.save()]); - - // console.log(existingUser); - // res.status(200).json({ - // message: "Friends connected", - // user: existingUser - // }); - } catch (err) { - console.log(err); - } - -}); - -userRouter.get("/mail",async function(req,res){ - console.log("mail route"); - res.json({ - message:"mail route" - }) -}); - - -//@desc get all friends -//@route /user/:friendID -userRouter.get("/:friend_id",async function(req,res){ - const friend_id = req.params.friend_id.toString(); - try{ - const friend = await UserDB.findOne({"_id": friend_id}); - res.status(200).json({ - message: "Friend retrieved success", - user: friend - }) - } - catch(err){ - console.log(err); - } -}) - -//@desc get req for details of a friend -//@route /users/:friendID -userRouter.get("/:friend_id",friends); - -userRouter.get("/friends/:userId", async (req, res) => { - try { - const user = await UserDB.findById(req.params.userId); - const friends = await Promise.all( - user.friends.map((friendId) => { - return UserDB.findById(friendId); - }) - ); - let friendList = []; - friends.map((friend) => { - const { _id, username } = friend; - friendList.push({ _id, username }); - }); - res.status(200).json(friendList); - } catch (err) { - res.status(500).json(err); - } - }); - -export default userRouter; - diff --git a/server/routes/users.js b/server/routes/users.js new file mode 100644 index 0000000..826e92d --- /dev/null +++ b/server/routes/users.js @@ -0,0 +1,25 @@ +// Mounted at /user — profiles and connections. Every route requires a logged-in user. +import { Router } from "express"; +import authenticateToken from "../middlewares/validateJWT.js"; +import asyncHandler from "../utils/asyncHandler.js"; +import { + createProfile, + getMyProfile, + getUserById, + listFriends, + respondFriendRequest, + sendFriendRequest, +} from "../controllers/userController.js"; + +const router = Router(); +router.use(authenticateToken); + +router.post("/createProfile", asyncHandler(createProfile)); +router.get("/profile", asyncHandler(getMyProfile)); +router.get("/profile/:userId/connect", asyncHandler(sendFriendRequest)); +router.get("/profile/:userId", asyncHandler(getUserById)); +router.put("/connect/:userId/:friendsId/:status/:requestId", asyncHandler(respondFriendRequest)); +router.get("/friends/:userId", asyncHandler(listFriends)); +router.get("/:userId", asyncHandler(getUserById)); + +export default router; diff --git a/server/scripts/security-smoke.sh b/server/scripts/security-smoke.sh new file mode 100755 index 0000000..58f731b --- /dev/null +++ b/server/scripts/security-smoke.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Security smoke test for the API. Needs a running server pointed at a throwaway database +# (it registers users alice, bob and carol). Not part of CI. +# +# BASE_URL=http://localhost:3001 server/scripts/security-smoke.sh +# +# Checks: password hash never returned, cookie flags, 401 without a token, Bearer header +# accepted, profile writes target the logged-in user only, regex-safe search, chat routes +# limited to conversation members, welcome email limited to the logged-in user. +set -u +B=${BASE_URL:-http://localhost:3001}; J=$(mktemp -d); rm -rf $J; mkdir -p $J +pass=0; fail=0 +check(){ if [ "$2" = "$3" ]; then echo "PASS $1"; pass=$((pass+1)); else echo "FAIL $1 (got: $2 | want: $3)"; fail=$((fail+1)); fi; } +code(){ curl -s -o /dev/null -w '%{http_code}' "$@"; } +reg(){ curl -s -D $J/$1.h -c $J/$1.jar -X POST $B/user/register -H 'Content-Type: application/json' -d "{\"username\":\"$1\",\"email\":\"$1@x.test\",\"password\":\"pw-$1\"}"; } + +A=$(reg alice); Bb=$(reg bob); C=$(reg carol) +check "register 200 + success" "$(echo "$A" | grep -c '"success":true')" 1 +check "register body has no password" "$(echo "$A" | grep -c password)" 0 +check "cookie HttpOnly+SameSite+Max-Age" "$(grep -i 'set-cookie' $J/alice.h | grep -ci 'httponly.*samesite=lax\|samesite=lax.*httponly')$(grep -i set-cookie $J/alice.h | grep -ci max-age)" 11 +AID=$(echo "$A" | sed -E 's/.*"_id":"([a-f0-9]+)".*/\1/'); BID=$(echo "$Bb" | sed -E 's/.*"_id":"([a-f0-9]+)".*/\1/') + +L=$(curl -s -c $J/alice.jar -X POST $B/user/login -H 'Content-Type: application/json' -d '{"email":"alice@x.test","password":"pw-alice"}') +check "login first attempt 200" "$(echo "$L" | grep -c '"success":true')" 1 +check "login body has no password" "$(echo "$L" | grep -c password)" 0 +check "login wrong password 401" "$(code -X POST $B/user/login -H 'Content-Type: application/json' -d '{"email":"alice@x.test","password":"nope"}')" 401 + +check "GET /user/profile no cookie -> 401" "$(code $B/user/profile)" 401 +check "GET /user/profile bad token -> 401" "$(code -H 'Authorization: Bearer garbage' $B/user/profile)" 401 +P=$(curl -s -b $J/alice.jar $B/user/profile) +check "GET /user/profile with cookie 200, no hash" "$(echo "$P" | grep -c '"success":true')$(echo "$P" | grep -c password)" 10 +TOK=$(awk '/authorization/ {print $7}' $J/alice.jar) +check "Bearer header accepted" "$(code -H "Authorization: Bearer $TOK" $B/user/profile)" 200 + +check "POST /in/populate gone -> 404" "$(code -X POST $B/in/populate -H 'Content-Type: application/json' -d '{"username":"evil","email":"e@x","password":"p"}')" 404 +check "POST /in/update no cookie -> 401" "$(code -X POST $B/in/update/alice -H 'Content-Type: application/json' -d '{"about":"x"}')" 401 +check "POST /in/addCollege no cookie -> 401" "$(code -X POST $B/in/addCollege/alice -H 'Content-Type: application/json' -d '{"collegeName":"x"}')" 401 +curl -s -o /dev/null -b $J/alice.jar -X POST $B/in/update/bob -H 'Content-Type: application/json' -d '{"about":"HACKED-VIA-URL"}' +check "IDOR: alice editing /in/update/bob changes alice" "$(curl -s $B/in/alice | grep -c HACKED-VIA-URL)" 1 +check "IDOR: bob untouched" "$(curl -s $B/in/bob | grep -c HACKED-VIA-URL)" 0 +check "GET /in/:username has no hash" "$(curl -s $B/in/bob | grep -c password)" 0 +check "addCollege empty body -> 400" "$(code -b $J/alice.jar -X POST $B/in/addCollege/x -H 'Content-Type: application/json' -d '{}')" 400 +check "photo upload wrong type -> 400" "$(printf 'notanimage' > /tmp/x.txt; code -b $J/alice.jar -X POST $B/in/add -F 'photo=@/tmp/x.txt;type=text/plain')" 400 + +check "search hostile regex -> 200 fast" "$(code --max-time 3 "$B/search/(a%2B)%2B%24")" 200 +check "search result has no hash" "$(curl -s "$B/search/ali" | grep -c password)" 0 + +check "email/intro no cookie -> 401" "$(code -X POST $B/email/intro -H 'Content-Type: application/json' -d '{"userEmail":"victim@x.test","userName":"v"}')" 401 + +check "chat: no cookie -> 401" "$(code $B/conversations/$AID)" 401 +CV=$(curl -s -b $J/bob.jar -X POST $B/conversations -H 'Content-Type: application/json' -d "{\"receiverId\":\"$AID\"}") +CID=$(echo "$CV" | sed -E 's/.*"_id":"([a-f0-9]+)".*/\1/') +check "chat: bob creates conversation 201" "$(echo "$CV" | grep -c '"members"')" 1 +check "chat: bob reading alice's list -> 403" "$(code -b $J/bob.jar $B/conversations/$AID)" 403 +check "chat: alice reading own list -> 200" "$(code -b $J/alice.jar $B/conversations/$AID)" 200 +M=$(curl -s -b $J/alice.jar -X POST $B/messages -H 'Content-Type: application/json' -d "{\"conversationId\":\"$CID\",\"sender\":\"$BID\",\"text\":\"hi\"}") +check "chat: sender forced to token user (not spoofed)" "$(echo "$M" | grep -c "\"sender\":\"$AID\"")" 1 +check "chat: carol reading thread -> 403" "$(code -b $J/carol.jar $B/messages/$CID)" 403 +check "chat: carol posting to thread -> 403" "$(code -b $J/carol.jar -X POST $B/messages -H 'Content-Type: application/json' -d "{\"conversationId\":\"$CID\",\"text\":\"spam\"}")" 403 +check "chat: bob reading thread -> 200" "$(code -b $J/bob.jar $B/messages/$CID)" 200 + + +# ---- correctness checks (added with the backend restructure) ---- +check "register duplicate username -> 409" "$(code -X POST $B/user/register -H 'Content-Type: application/json' -d '{"username":"alice","email":"other@x.test","password":"pw"}')" 409 +check "unknown route -> JSON 404" "$(curl -s $B/nope/route | grep -c '"success":false')" 1 +check "invalid ObjectId -> 400 (not a hang)" "$(code --max-time 3 -b $J/alice.jar $B/user/profile/not-an-id)" 400 +check "GET /in/:username unknown -> 404" "$(code $B/in/nobody)" 404 + +# profile save actually persists (was a silent no-op with capitalised keys) +curl -s -o /dev/null -b $J/alice.jar -X POST $B/user/createProfile -H 'Content-Type: application/json' -d '{"About":"I build things","Skills":[{"description":"node"}]}' +check "createProfile persists about + skills" "$(curl -s -b $J/alice.jar $B/user/profile | grep -c '"about":"I build things"')$(curl -s -b $J/alice.jar $B/user/profile | grep -c '"description":"node"')" 11 + +# friend request lifecycle: bob -> alice, alice accepts, request removed, both friends, no duplicates +check "self-connect -> 400" "$(code -b $J/bob.jar $B/user/profile/$BID/connect)" 400 +check "bob sends request to alice -> 200" "$(code -b $J/bob.jar $B/user/profile/$AID/connect)" 200 +check "duplicate request -> 409" "$(code -b $J/bob.jar $B/user/profile/$AID/connect)" 409 +RID=$(curl -s -b $J/alice.jar $B/user/profile | python3 -c 'import sys,json; r=json.load(sys.stdin)["user"]["friendRequests"]; print(r[0]["_id"] if r else "")') +check "request visible to alice with status pending" "$(curl -s -b $J/alice.jar $B/user/profile | grep -c '"status":"pending"')" 1 +check "carol cannot answer alice's request -> 403" "$(code -b $J/carol.jar -X PUT $B/user/connect/$BID/$AID/Accept/$RID)" 403 +check "alice accepts -> 200" "$(code -b $J/alice.jar -X PUT $B/user/connect/$BID/$AID/Accept/$RID)" 200 +AP=$(curl -s -b $J/alice.jar $B/user/profile) +check "request removed after accept" "$(echo "$AP" | grep -c '"friendRequests":\[\]')" 1 +check "alice has bob as friend" "$(echo "$AP" | grep -c "\"friends\":\[\"$BID\"\]")" 1 +check "bob has alice as friend" "$(curl -s -b $J/bob.jar $B/user/profile | grep -c "\"friends\":\[\"$AID\"\]")" 1 +check "already connected -> 409" "$(code -b $J/bob.jar $B/user/profile/$AID/connect)" 409 +check "friends list is a bare array with username" "$(curl -s -b $J/alice.jar $B/user/friends/$AID | grep -c '^\[{"_id":"[a-f0-9]*","username":"bob"')" 1 + +# posts +check "create post missing fields -> 400" "$(code -b $J/alice.jar -X POST $B/user/jobpostform -H 'Content-Type: application/json' -d '{"salary":10}')" 400 +PP=$(curl -s -b $J/alice.jar -X POST $B/user/jobpostform -H 'Content-Type: application/json' -d '{"jobTitle":"Node Engineer","companyName":"Acme","salary":"120"}') +check "create post -> post returned with createdAt" "$(echo "$PP" | grep -c '"createdAt"')" 1 +check "GET /home lists it under posts" "$(curl -s $B/home | grep -c '"jobTitle":"Node Engineer"')" 1 +check "GET /user/allposts (self) works" "$(curl -s -b $J/alice.jar $B/user/allposts | grep -c '"allposts":\[{')" 1 +check "GET /user/allposts/:id works" "$(curl -s -b $J/bob.jar $B/user/allposts/$AID | grep -c '"jobTitle":"Node Engineer"')" 1 +check "post appended to user.posts" "$(curl -s -b $J/alice.jar $B/user/profile | grep -c '"posts":\["')" 1 + +# search: users and posts come back in separate keys +SR=$(curl -s "$B/search/node") +check "search: user key holds users only" "$(echo "$SR" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(all("username" in u for u in d["user"]), len(d["posts"])>0)')" "True True" + +curl -s -D $J/logout.h -b $J/alice.jar $B/user/logout > /dev/null +check "logout clears cookie" "$(grep -i set-cookie $J/logout.h | grep -ci 'authorization=;')" 1 +echo; echo "passed=$pass failed=$fail" diff --git a/server/utils/asyncHandler.js b/server/utils/asyncHandler.js new file mode 100644 index 0000000..640a642 --- /dev/null +++ b/server/utils/asyncHandler.js @@ -0,0 +1,5 @@ +// Wraps an async route handler so a rejected promise reaches the global error handler +// instead of leaving the request hanging. +const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); + +export default asyncHandler; diff --git a/server/utils/cookieOptions.js b/server/utils/cookieOptions.js new file mode 100644 index 0000000..e4ae1c0 --- /dev/null +++ b/server/utils/cookieOptions.js @@ -0,0 +1,17 @@ +const ONE_DAY_MS = 24 * 60 * 60 * 1000; // matches the JWT expiresIn of 24h + +// Options for the httpOnly auth cookie. `secure` is only enforced in production so the +// app still works over plain http in local development. +export const authCookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: ONE_DAY_MS, +}; + +// clearCookie must be given the same attributes (minus maxAge) to match the cookie. +export const clearAuthCookieOptions = { + httpOnly: authCookieOptions.httpOnly, + secure: authCookieOptions.secure, + sameSite: authCookieOptions.sameSite, +}; diff --git a/server/utils/escapeRegex.js b/server/utils/escapeRegex.js new file mode 100644 index 0000000..6d251da --- /dev/null +++ b/server/utils/escapeRegex.js @@ -0,0 +1,5 @@ +// Escapes user input before it is interpolated into a MongoDB $regex, so characters like +// `(`, `+`, `*` are matched literally instead of changing (or blowing up) the pattern. +const escapeRegex = (value = "") => String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +export default escapeRegex; diff --git a/server/utils/httpError.js b/server/utils/httpError.js new file mode 100644 index 0000000..b171dc7 --- /dev/null +++ b/server/utils/httpError.js @@ -0,0 +1,7 @@ +// An Error carrying an HTTP status, understood by middlewares/errorHandler.js. +export default class HttpError extends Error { + constructor(status, message) { + super(message); + this.status = status; + } +} diff --git a/server/utils/token.js b/server/utils/token.js new file mode 100644 index 0000000..ba82659 --- /dev/null +++ b/server/utils/token.js @@ -0,0 +1,11 @@ +import jsonwebtoken from "jsonwebtoken"; + +export const TOKEN_TTL = "24h"; // keep in sync with utils/cookieOptions.js maxAge + +// Signs the minimal identity payload that validateJWT puts back on req.user. +export const signToken = (user) => + jsonwebtoken.sign( + { user: { username: user.username, email: user.email, id: user._id } }, + process.env.ACCESS_TOKEN, + { expiresIn: TOKEN_TTL } + ); diff --git a/socket/.env.example b/socket/.env.example new file mode 100644 index 0000000..9738805 --- /dev/null +++ b/socket/.env.example @@ -0,0 +1,7 @@ +# Copy to socket/.env + +# Port the socket.io server listens on (the client connects to ws://localhost:PORT) +PORT=8900 + +# Origin allowed to connect (the CRA dev server) +CLIENT_URL=http://localhost:3000 diff --git a/socket/index.js b/socket/index.js index c7b3b25..2177167 100644 --- a/socket/index.js +++ b/socket/index.js @@ -1,8 +1,14 @@ -const io = require("socket.io")(8900,{ +require("dotenv").config(); + +const PORT = Number(process.env.PORT) || 8900; +const CLIENT_URL = process.env.CLIENT_URL || "http://localhost:3000"; + +const io = require("socket.io")(PORT, { cors:{ - origin:"http://localhost:3000" + origin: CLIENT_URL }, }); +console.log(`Socket server listening on ${PORT}, allowing origin ${CLIENT_URL}`); let users = []; const addUser = (userId,socketId)=>{ @@ -43,4 +49,4 @@ io.on("connection",(socket)=>{ removeUser(socket.id); io.emit("getUsers",users); }) -}) \ No newline at end of file +}) diff --git a/socket/package-lock.json b/socket/package-lock.json index cead19c..5d104e9 100644 --- a/socket/package-lock.json +++ b/socket/package-lock.json @@ -7,9 +7,16 @@ "": { "name": "socket", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { + "dotenv": "^16.3.1", "socket.io": "^4.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=20" } }, "node_modules/@socket.io/component-emitter": { @@ -50,6 +57,30 @@ "node": ">= 0.6" } }, + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -58,6 +89,70 @@ "node": "^4.5.0 || >= 5.9" } }, + "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/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "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/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/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -94,6 +189,18 @@ } } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/engine.io": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", @@ -122,6 +229,110 @@ "node": ">=10.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/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/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/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/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/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-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/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -141,6 +352,22 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -154,6 +381,45 @@ "node": ">= 0.6" } }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -162,6 +428,65 @@ "node": ">=0.10.0" } }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/socket.io": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", @@ -199,6 +524,49 @@ "node": ">=10.0.0" } }, + "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/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/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/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/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/socket/package.json b/socket/package.json index fa7607b..0d83038 100644 --- a/socket/package.json +++ b/socket/package.json @@ -1,15 +1,22 @@ { "name": "socket", "version": "1.0.0", - "description": "", + "description": "Workplace real-time chat server (socket.io)", "main": "index.js", "scripts": { - "start": "nodemon index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node index.js", + "dev": "nodemon index.js" }, "author": "", - "license": "ISC", + "license": "MIT", "dependencies": { + "dotenv": "^16.3.1", "socket.io": "^4.7.4" + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "nodemon": "^3.0.1" } }