├── .gitignore
├── server
├── .gitignore
├── models
│ ├── dbModel.js
│ └── eventModel.js
├── package.json
├── controllers
│ ├── sessionController.js
│ ├── geoCodeController.js
│ ├── userController.js
│ ├── tmEventController.js
│ ├── attendeeController.js
│ └── eventController.js
├── server.js
├── routes
│ └── api.js
└── package-lock.json
├── .DS_Store
├── docs
├── schema.png
└── eventDataFormat.png
├── client
├── src
│ ├── components
│ │ ├── logo.gif
│ │ ├── UserContext.js
│ │ ├── RsvpDisplay.jsx
│ │ ├── Login.jsx
│ │ ├── App.jsx
│ │ ├── MarkerUpdator.jsx
│ │ ├── MarkerCreator.jsx
│ │ └── Map.jsx
│ ├── main.jsx
│ └── stylesheets
│ │ ├── index.css
│ │ └── App.css
├── vite.config.js
├── .gitignore
├── package.json
└── index.html
├── .eslintrc.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | coverage/
4 | .env
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PoppinIteration/Poppin-Events/HEAD/.DS_Store
--------------------------------------------------------------------------------
/docs/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PoppinIteration/Poppin-Events/HEAD/docs/schema.png
--------------------------------------------------------------------------------
/docs/eventDataFormat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PoppinIteration/Poppin-Events/HEAD/docs/eventDataFormat.png
--------------------------------------------------------------------------------
/client/src/components/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PoppinIteration/Poppin-Events/HEAD/client/src/components/logo.gif
--------------------------------------------------------------------------------
/client/src/components/UserContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const UserContext = React.createContext(null);
4 |
5 | export { UserContext };
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | server: {
7 | port: 3000,
8 | proxy: {
9 | '/api': 'http://localhost:5000/',
10 | },
11 | },
12 | plugins: [react()],
13 | });
14 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './stylesheets/index.css';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import App from './components/App';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root'),
12 | );
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | },
6 | extends: [
7 | 'plugin:react/recommended',
8 | 'airbnb',
9 | ],
10 | overrides: [
11 | ],
12 | parserOptions: {
13 | ecmaVersion: 'latest',
14 | sourceType: 'module',
15 | },
16 | plugins: [
17 | 'react',
18 | ],
19 | rules: {
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | pnpm-debug.log*
10 | lerna-debug.log*
11 |
12 | node_modules
13 | dist
14 | dist-ssr
15 | *.local
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/client/src/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "concurrently": "^7.6.0"
4 | },
5 | "scripts": {
6 | "start": "concurrently \"cd ./server && npm run server\" \"cd ./client && npm run dev\""
7 | },
8 | "devDependencies": {
9 | "eslint": "^8.35.0",
10 | "eslint-config-airbnb": "^19.0.4",
11 | "eslint-plugin-import": "^2.27.5",
12 | "eslint-plugin-jsx-a11y": "^6.7.1",
13 | "eslint-plugin-react": "^7.32.2",
14 | "eslint-plugin-react-hooks": "^4.6.0",
15 | "nodemon": "^2.0.21"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/models/dbModel.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 | require('dotenv').config();
3 |
4 | // put your PG_URI here
5 | const PG_URI = process.env.PG_URI;
6 | // 'postgres://leekwldv:85zQjHG1-BdoK3wOIfWOGl63DL85OW9c@mahmud.db.elephantsql.com/leekwldv'
7 | // create new pool
8 | const pool = new Pool({
9 | connectionString: PG_URI,
10 | });
11 |
12 | module.exports = {
13 | query: (text, params, callback) => {
14 | console.log('executing query', text, 'with params: ', params);
15 | return pool.query(text, params, callback);
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js",
9 | "server": "nodemon server.js"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "cookie-parser": "^1.4.6",
16 | "express": "^4.18.2",
17 | "express-session": "^1.17.3",
18 | "google-auth-library": "^8.7.0",
19 | "jwt-decode": "^3.1.2",
20 | "pg": "^8.9.0"
21 | },
22 | "devDependencies": {
23 | "dotenv": "^16.0.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview"
9 | },
10 | "dependencies": {
11 | "@react-google-maps/api": "^2.18.1",
12 | "@react-oauth/google": "^0.8.0",
13 | "axios": "^1.3.4",
14 | "jwt-decode": "^3.1.2",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-dotenv": "^0.1.3",
18 | "react-google-autocomplete": "^2.7.3",
19 | "react-icons": "^4.8.0",
20 | "react-router-dom": "^6.8.2",
21 | "react-scripts": "^5.0.1"
22 | },
23 | "devDependencies": {
24 | "@vitejs/plugin-react": "^3.1.0",
25 | "vite": "^4.1.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/controllers/sessionController.js:
--------------------------------------------------------------------------------
1 | const sessionController = {};
2 |
3 | sessionController.validateSession = (req, res, next) => {
4 | console.log('validating session...');
5 | console.log('req.session is currently: ', req.session);
6 | if (req.session.loggedIn) {
7 | res.locals = {
8 | name: req.session.name,
9 | email: req.session.email,
10 | picture: req.session.picture,
11 | loggedIn: req.session.loggedIn,
12 | id: req.session.userID,
13 | };
14 | } else {
15 | res.locals.loggedIn = false;
16 | }
17 | next();
18 | };
19 | sessionController.deleteSession = (req, res, next) => {
20 | console.log('deleting session...');
21 | req.session.destroy(e => console.log('LOLLLLLLLLLLLLLLLLLLLLLL', e));
22 | next();
23 | };
24 |
25 | module.exports = sessionController;
26 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 | Poppin
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/server/models/eventModel.js:
--------------------------------------------------------------------------------
1 | class EventModel {
2 | constructor(
3 | name,
4 | address,
5 | date,
6 | description,
7 | id,
8 | lat,
9 | lng,
10 | locName,
11 | end_date,
12 | image_url,
13 | ticketmaster_evt_id,
14 | rsvp_url,
15 | evt_origin_type_id,
16 | organizer_id,
17 | organizer_name,
18 | organizer_email,
19 | organizer_picture) {
20 | this.name = name;
21 | this.address = address;
22 | this.date = date;
23 | this.description = description;
24 | this.id = id;
25 | this.location = {
26 | 'lat': lat,
27 | 'lng': lng
28 | };
29 | this.locName = locName;
30 | this.end_date = end_date;
31 | this.image_url = image_url;
32 | this.ticketmaster_evt_id = ticketmaster_evt_id;
33 | this.rsvp_url = rsvp_url;
34 | this.evt_origin_type_id = evt_origin_type_id;
35 | this.organizer = {
36 | 'id': organizer_id,
37 | 'name': organizer_name,
38 | 'email': organizer_email,
39 | 'picture': organizer_picture
40 | };
41 | }
42 | }
43 |
44 | module.exports = EventModel;
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | // require in dependencies
2 | const path = require('path');
3 | const express = require('express');
4 | const session = require('express-session');
5 | const apiRouter = require('./routes/api');
6 |
7 | // require in routes
8 |
9 | const PORT = 5000;
10 | const app = express();
11 |
12 | // request parsing (if needed)
13 | app.use(express.json());
14 | app.use(express.urlencoded({ extended: true }));
15 |
16 | // start session
17 | app.use(session({
18 | secret: 'af168f987f1gh78fhg91f',
19 | name: 'ssid',
20 | saveUninitialized: false,
21 | }));
22 | // handle requests for static files
23 | app.use(express.static(path.resolve(__dirname, '../dist')));
24 | // route handlers
25 |
26 | app.use('/api', apiRouter);
27 |
28 | // catch-all 404 route handler
29 | app.use((_, res) => res.status(404).send('Page Not Found'));
30 |
31 | // error handler
32 | app.use((err, req, res, next) => {
33 | const defaultErr = {
34 | log: 'Express error handler caught unknown middleware error',
35 | status: 500,
36 | message: { err: 'An error occurred' },
37 | };
38 | const errorObj = { ...defaultErr, ...err };
39 | console.log(errorObj.log);
40 | return res.status(errorObj.status).json(errorObj.message).redirect('/');
41 | });
42 |
43 | // start server
44 | app.listen(PORT, () => console.log(`start listening on port : ${PORT}`));
45 |
46 | module.exports = app;
47 |
--------------------------------------------------------------------------------
/client/src/components/RsvpDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from "react";
2 | import axios from "axios";
3 |
4 | import "../stylesheets/App.css";
5 | import { UserContext } from "./UserContext";
6 |
7 | // import { GoogleMap, useJsApiLoader, MarkerF } from "@react-google-maps/api";
8 | // /* https://www.npmjs.com/package/@react-google-maps/api */
9 | // import MarkerCreator from "./MarkerCreator";
10 | // import MarkerUpdator from "./MarkerUpdator";
11 |
12 | function RsvpDisplay() {
13 | const { user } = useContext(UserContext);
14 | const [rsvp, setRSVP] = useState([]);
15 |
16 | useEffect(() => {
17 | const { id } = user;
18 | axios.get(`/api/rsvp?userID=${id}`).then((res) => {
19 | setRSVP(res.data);
20 | // console.log("rsvp is currently: ", rsvp);
21 | });
22 |
23 | console.log(rsvp);
24 |
25 | // const generateRSVPEvents = async () => {
26 | // const response = await axios.get(`/api/rsvp?userID=${id}`);
27 | // console.log(response);
28 | // // setRSVP(response);
29 | // };
30 | // generateRSVPEvents();
31 | }, []);
32 | console.log("rsvp is currently: ", rsvp);
33 | const events = [];
34 | rsvp.forEach((event) => {
35 | events.push(
36 |
37 | {event.event.name} {event.rsvp}
38 |
39 | );
40 | });
41 |
42 | return (
43 |
44 |
Your RSVP'd Events
45 |
46 |
47 | );
48 | }
49 |
50 | export default RsvpDisplay;
51 |
--------------------------------------------------------------------------------
/server/controllers/geoCodeController.js:
--------------------------------------------------------------------------------
1 | const geocodeController = {};
2 |
3 | geocodeController.reverseGeocode = (req, res, next) => {
4 | console.log("inGEOCODE");
5 | // variables to use
6 | const { lat, lng } = req.params;
7 | // console.log('lat: ', lat);
8 | // console.log('lng: ', lng);
9 | const { VITE_GOOGLE_MAPS_API_KEY } = process.env;
10 | // console.log('VITE_GOOGLE_MAPS_API_KEY', VITE_GOOGLE_MAPS_API_KEY);
11 |
12 | // fetch from Google Geocode API
13 | fetch(
14 | `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${VITE_GOOGLE_MAPS_API_KEY}`
15 | )
16 | .then((response) => response.json())
17 | .then((locationData) => {
18 | // console.log('locationData: ', locationData);
19 |
20 | for (
21 | let i = 0;
22 | i < locationData.results[0].address_components.length;
23 | i += 1
24 | ) {
25 | locationData.results[0].address_components[i].types.forEach((type) => {
26 | if (type.includes("locality")) {
27 | res.locals.city =
28 | locationData.results[0].address_components[i].long_name;
29 | }
30 | if (type.includes("administrative_area_level_1")) {
31 | res.locals.state =
32 | locationData.results[0].address_components[i].long_name;
33 | }
34 | });
35 | }
36 | if (res.locals.city && res.locals.state) {
37 | return next();
38 | }
39 | return next({
40 | log: "Error in geocodeController.reverseGeocode middleware",
41 | status: 500,
42 | message: {
43 | err: "Error in geocodeController.reverseGeocode middleware",
44 | },
45 | });
46 | })
47 | .catch((error) => {
48 | next({
49 | log: "Error in geocodeController.reverseGeocode: city not found from Google Geocode API fetch",
50 | status: 404,
51 | message: { err: error },
52 | });
53 | });
54 | };
55 |
56 | module.exports = geocodeController;
57 |
--------------------------------------------------------------------------------
/server/controllers/userController.js:
--------------------------------------------------------------------------------
1 | // import the user model
2 | const jwt_decode = require('jwt-decode'); // ended up only using jwt_decode on front end
3 | const db = require('../models/dbModel');
4 |
5 | const userController = {};
6 |
7 | // get user info from database
8 | userController.login = async (req, res, next) => {
9 | try {
10 | // console.log('req body in userController.login', req.body);
11 | // grab name
12 | const { name, email, picture } = req.body;
13 | const query = `SELECT * FROM users WHERE email = '${email}'`;
14 | const user = await db.query(query);
15 | // if the user does not exist in the database, create them
16 | const userVals = [name, email, picture];
17 | if (user.rows.length) res.locals.id = user.rows[0].id;
18 | if (user.rows.length === 0) {
19 | const createUsr = 'INSERT INTO users (name, email, picture) VALUES ($1, $2, $3) RETURNING id';
20 | const newUser = await db.query(createUsr, userVals);
21 | res.locals.id = newUser.rows[0];
22 | } else if (user.rows[0].picture !== picture) {
23 | const updatePic = 'UPDATE users SET picture = $2 WHERE email = $1;';
24 | await db.query(updatePic, userVals.slice(1));
25 | }
26 | req.session.loggedIn = true;
27 | req.session.email = email;
28 | req.session.name = name;
29 | req.session.picture = picture;
30 | req.session.userID = res.locals.id;
31 | return next();
32 | } catch (e) {
33 | return next({
34 | log: 'Error in userController.login',
35 | status: 500,
36 | message: { error: e.message },
37 | });
38 | }
39 | };
40 |
41 | // get user info from database
42 | userController.getUser = async (req, res, next) => {
43 | const { email } = req.params;
44 | const query = `SELECT * FROM users WHERE email = ${email}`;
45 | const value = [id];
46 | const user = await db.query(query, value);
47 | // use array destructuring to get the first element of the array
48 | [res.locals.user] = user.rows;
49 | return next();
50 | };
51 |
52 | // create a new user in the database
53 | // userController.createUser = async (req, res, next) => {
54 |
55 | // }
56 |
57 | module.exports = userController;
58 |
--------------------------------------------------------------------------------
/client/src/components/Login.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/function-component-definition */
2 | import React, { useState, useContext } from 'react';
3 | import { useNavigate } from 'react-router-dom';
4 | import { FcGoogle } from 'react-icons/fc';
5 | import { GoogleOAuthProvider } from '@react-oauth/google';
6 | import { GoogleLogin } from '@react-oauth/google';
7 | import jwt_decode from "jwt-decode";
8 | import env from 'react-dotenv';
9 | import { UserContext } from './UserContext';
10 | import axios from 'axios';
11 |
12 | const Login = (props) => {
13 | const navigate = useNavigate();
14 | const responseGoogle = async (response) => {
15 | // the google oauth (identity services) api responds with a JWT with all user info
16 | const userObject = jwt_decode(response.credential);
17 | // destructure that info for our purposes
18 | const { name, email, picture } = userObject;
19 | try {
20 | const res = await axios.post('/api/users', {
21 | name, email, picture,
22 | });
23 | // reroute to map
24 | if (res.status === 200){
25 | props.setUser({ name, email, picture, id: res.data});
26 | navigate('/map');
27 | }
28 | } catch (e) {
29 | console.log('error in post: ', e.message);
30 | }
31 | };
32 |
33 | return (
34 |
35 | {/* https://www.npmjs.com/package/react-google-login */}
36 | {/* https://vitejs.dev/guide/env-and-mode.html */}
37 |
38 | (
40 |
46 | Sign in with google
47 |
48 | )}
49 | onSuccess={responseGoogle}
50 | onFailure={responseGoogle}
51 | cookiePolicy="single_host_origin"
52 | size="medium"
53 | />
54 |
55 |
56 | )
57 | }
58 |
59 | export default Login
--------------------------------------------------------------------------------
/server/controllers/tmEventController.js:
--------------------------------------------------------------------------------
1 | const tmEventController = {};
2 |
3 | // get all events in a specific city
4 | tmEventController.getEvents = (req, res, next) => {
5 | console.log("inTMEVENTCONTROLLER");
6 | const { city, state } = res.locals;
7 | const { TICKETMASTER_API_KEY } = process.env;
8 | console.log('city', city)
9 | console.log('state', state)
10 |
11 | fetch(
12 | `https://app.ticketmaster.com/discovery/v2/events.json?city=${city}&state=${state}&size=200&apikey=${TICKETMASTER_API_KEY}`,
13 | )
14 | .then((response) => response.json())
15 | .then((data) => {
16 | const myEvents = data._embedded.events;
17 | const extracted = [];
18 |
19 | // only pull the first n events
20 | for (let i = 0; i < 200; i += 1) {
21 | // each event details
22 | const details = {
23 | name: myEvents[i].name,
24 | address: `${myEvents[i]._embedded.venues[0].address.line1}, ${myEvents[i]._embedded.venues[0].city.name}, ${myEvents[i]._embedded.venues[0].state.stateCode} ${myEvents[i]._embedded.venues[0].postalCode}`,
25 | date: myEvents[i].dates.start.dateTime,
26 | description: "",
27 | id: null,
28 | location: {
29 | lat: myEvents[i]._embedded.venues[0].location.latitude,
30 | lng: myEvents[i]._embedded.venues[0].location.longitude,
31 | },
32 | locName: myEvents[i]._embedded.venues[0].name,
33 | end_date: null,
34 | image_url: myEvents[i].images[0].url,
35 | ticketmaster_evt_id: myEvents[i].id,
36 | rsvp_url: myEvents[i].url,
37 | evt_origin_type_id: 2,
38 | organizer: {
39 | id: 1,
40 | name: "Ticketmaster",
41 | email: "customer_support@ticketmaster.com",
42 | picture:
43 | "https://play-lh.googleusercontent.com/KmWVboPY-BCCfiflJ-AYCPGBv86QLMsXsSpvQksC0DVR8ENV0lh-lwHnXrekpHwbQA=w240-h480-rw",
44 | },
45 | };
46 | extracted.push(details);
47 | }
48 | res.locals.events = extracted;
49 | return next();
50 | })
51 | .catch((error) =>
52 | next({
53 | log: "tmEventController.getEvents error: Error getting events from ticketmaster.",
54 | status: 404,
55 | message: { err: error },
56 | })
57 | );
58 | };
59 |
60 | module.exports = tmEventController;
61 |
--------------------------------------------------------------------------------
/server/routes/api.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const userController = require('../controllers/userController');
3 | const eventController = require('../controllers/eventController');
4 | const sessionController = require('../controllers/sessionController');
5 | const tmEventController = require('../controllers/tmEventController');
6 | const geocodeController = require('../controllers/geoCodeController');
7 | const attendeeController = require('../controllers/attendeeController');
8 |
9 | const router = express.Router();
10 |
11 | // Responds with user info (location + events) when passed in the correct google id
12 | // router.get(
13 | // '/user/:id',
14 | // userController.getUser,
15 | // (req, res) => res.status(200).json(res.locals.user),
16 | // );
17 |
18 | // Log in or a new user in the database
19 | router.post(
20 | '/users',
21 | userController.login,
22 | (req, res) => res.status(200).json(res.locals.id),
23 | );
24 |
25 | // Responds with all events in the database (Name, Location, Date, Description, Created By)
26 | router.get(
27 | '/events',
28 | eventController.getEvents,
29 | (req, res) => res.status(200).json(res.locals.events),
30 | );
31 |
32 | // Create an event in the database
33 | router.post(
34 | '/events',
35 | eventController.createEvent,
36 | (req, res) => res.status(200).json(res.locals.id),
37 | );
38 |
39 | // Update an event in the database
40 | router.put(
41 | '/events',
42 | eventController.updateEvent,
43 | (req, res) => res.sendStatus(200),
44 | );
45 |
46 | // Delete an event in the database
47 | router.delete(
48 | '/events',
49 | eventController.deleteEvent,
50 | (req, res) => res.sendStatus(200),
51 | );
52 |
53 | // GET all the events in a specific area from Ticketmaster API
54 | router.get(
55 | '/ticketmaster/:lat/:lng',
56 | geocodeController.reverseGeocode,
57 | tmEventController.getEvents,
58 | (req, res) => res.status(200).json(res.locals.events),
59 | );
60 |
61 | // GET all rsvp events for a user
62 | router.get(
63 | '/rsvp', // GET request to /api/rsvp?userID=
64 | attendeeController.getEventsByUser,
65 | (req, res) => res.status(200).json(res.locals.rsvpEvents),
66 | );
67 |
68 | // RSVP to an event - create an event if necessary, then add the user to attendees table and return the new row on the response object
69 | router.post(
70 | '/rsvp/:rsvp_level',
71 | eventController.findEvent,
72 | eventController.createEvent,
73 | attendeeController.addAttendee,
74 | (req, res) => {
75 | return res.status(200).json(res.locals.newAttendee);
76 | },
77 | );
78 |
79 | // Checks for active sessions
80 | router.get(
81 | '/sessions',
82 | sessionController.validateSession,
83 | (req, res) => res.status(200).send(res.locals),
84 | );
85 | router.delete(
86 | '/sessions',
87 | sessionController.deleteSession,
88 | (req, res) => res.sendStatus(200),
89 | );
90 |
91 | module.exports = router;
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Poppin-Events
2 | An app that allows users to view events in their area, create new events, and edit/delete their own events from the map
3 |
4 | # To future archaeologists:
5 | We used Vite! Enjoy (here is the starter video for structure: https://www.youtube.com/watch?v=PPjpHaLkV7A)
6 |
7 | Note: You'll have to npm i for the root directory, the client, and the server separately
8 |
9 | Run 'npm start' in root directory to concurrently run server(5000) and client(3000)
10 |
11 | Npm install your frontend dependencies in client folder, backend dependencies in server folder
12 |
13 | If your server is running on 3001, it won't work, so shut it down and killall node before starting again
14 |
15 | Some important considerations - things you'll have to create and set up for yourself:
16 |
17 | 1 - GoogleAPI key for OAuth and Maps (make a .env file in the client directory with key value pairs that correspond with constants that are imported like import.meta.env.VITE_GOOGLE_OATH_CLIENT_ID or import.meta.env.VITE_GOOGLE_OATH_CLIENT_ID)
18 |
19 | - You will need to sign up via Google Cloud Platform to create your own application to get your own Client ID and Secret for Oauth, and same thing for Google Maps
20 |
21 | 2 - SQL server URI (make a .env file in the server directory with PG_URI='')
22 |
23 | SQL server Schema:
24 | 
25 |
26 | Must swap in your own via .envs:
27 |
28 | The following will be accessed using "process.env.":
29 | server/.env => add in a PG_URI
30 | - Links to a SQL database
31 | server/.env => add in TICKETMASTER_API_KEY
32 | - Links to the Ticketmaster api
33 | server/.env => add in VITE_GOOGLE_MAPS_API_KEY
34 | - Links to the Google geocoding api for reverse geocoding
35 |
36 | The following will be imported using "import.meta.env.":
37 | client/.env => swap out VITE_GOOGLE_MAPS_API_KEY
38 | - Links to Google Maps api
39 | client/.env => swap out VITE_GOOGLE_OATH_CLIENT_ID
40 | - Links to Google oAuth 2.0 Login
41 |
42 | Event data has now been standardized to be sent and received from both frontend and backend in the following format:
43 | 
44 |
45 | Unimplemented features with frameworks:
46 |
47 | 1 - display pictures next to the organizer's name in the event display: Currently, we get, store, and update user picture urls, but they are unused
48 |
49 | Possible Refactors:
50 |
51 | 1 - Possibly display event boxes on map if you want, instead of to the side
52 |
53 | 2 - Refactor use of .id for both events and users to be userID / eventID on the front-end
54 |
55 | Bugs to squash:
56 |
57 | 1 - Newly created events don't have their time populate correctly on edit without a full page reload - likely, the date isn't being saved in state in a way the datetime-local input wants to receive
58 |
--------------------------------------------------------------------------------
/client/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | // fetch requester
2 | import axios from 'axios';
3 | import React, { useState, useEffect } from 'react';
4 | // import routes and usenavigate
5 | import { Routes, Route, useNavigate } from 'react-router-dom';
6 |
7 | //import map and login components
8 | import Map from './Map';
9 | import Login from './Login';
10 | import { UserContext } from './UserContext';
11 |
12 | // stylings
13 | import '../stylesheets/App.css';
14 |
15 | function App(props) {
16 | const navigate = useNavigate();
17 | // initialize null user to state
18 | const [user, setUser] = useState(null);
19 | console.log('in APP, user is: ', user);
20 |
21 | useEffect(() => {
22 | // on change, if user === null, check session
23 | console.log('in useEffect, and user is: ', user);
24 | const checkSession = async () => {
25 | try {
26 | // attempt to grab user info
27 | const userInfo = await axios.get('/api/sessions');
28 | console.log('user info is: ', userInfo);
29 | // if they are loggedIn, update state with user info and redirect to map
30 | if (userInfo.data.loggedIn === true) {
31 | setUser({
32 | name: userInfo.data.name,
33 | email: userInfo.data.email,
34 | picture: userInfo.data.picture,
35 | // for later: refactor to be userID instead of id
36 | id: userInfo.data.id,
37 | });
38 | navigate('/map');
39 | }
40 | // else go back to log in
41 | else navigate('login');
42 | } catch (e) {
43 | console.log('Error in checkSession: ', e.message);
44 | }
45 | };
46 | if (user === null) {
47 | console.log('user is null');
48 | checkSession();
49 | }
50 | else {
51 | // if user is not null go to map
52 | console.log('user is not null');
53 | navigate('/map');
54 | }
55 | }, [user]);
56 |
57 | const logout = async () => {
58 | // make server request to logout / destroy session + cookie
59 | try {
60 | // delete session
61 | const response = await axios.delete('/api/sessions');
62 | console.log('successful logout');
63 | // reset user
64 | setUser(null);
65 | } catch (e) {
66 | console.log('error logging out: ', e.message);
67 | }
68 | }
69 |
70 |
71 | return (
72 | <>
73 |
74 |
75 | Poppin
76 |
77 |
78 | {user &&
79 |
80 | Logout
81 | {user.name}
82 |
83 | }
84 |
85 |
86 | {/* look into usercontext provider */}
87 |
88 |
89 | setUser(u)}
92 | setUserJWT={(jwt) => setUserJWT(jwt)}>}
93 | />
94 | you are on path= /}
97 | />
98 | }
101 | />
102 |
103 |
104 | >
105 | );
106 | }
107 |
108 | export default App;
109 |
--------------------------------------------------------------------------------
/server/controllers/attendeeController.js:
--------------------------------------------------------------------------------
1 | const db = require('../models/dbModel');
2 |
3 | const attendeeController = {};
4 |
5 | attendeeController.getEventsByUser = (req, res, next) => {
6 | const { userID } = req.query;
7 |
8 | const queryStr = `SELECT e.id, e.name, e.description, e.date, e.loc_name, e.lat, e.lng, e.address, e.rsvp_url, e.end_date, e.image_url, e.ticketmaster_evt_id, e.evt_origin_type_id,
9 | u.id AS organizer_id, u.name AS organizer_name, u.email AS organizer_email, u.picture AS organizer_picture, a.rsvp_level_id
10 | FROM events e
11 | INNER JOIN attendees a ON e.id = a.events_id
12 | INNER JOIN users u ON a.users_id = u.id
13 | WHERE a.users_id = $1`;
14 | db.query(queryStr, [userID])
15 | .then((data) => {
16 | const rsvpEvents = data.rows;
17 |
18 | const normalizedEvents = [];
19 | for (let i = 0; i < rsvpEvents.length; i += 1) {
20 | const event = rsvpEvents[i];
21 | const eventKeys = Object.keys(event);
22 | const normalEvent = {
23 | location: {},
24 | organizer: {},
25 | };
26 | for (let j = 0; j < eventKeys.length; j += 1) {
27 | const key = eventKeys[j];
28 | switch (key) {
29 | case 'loc_name':
30 | normalEvent.locName = event[key];
31 | break;
32 | case 'lat':
33 | normalEvent.location[key] = event[key];
34 | break;
35 | case 'lng':
36 | normalEvent.location[key] = event[key];
37 | break;
38 | case 'organizer_id':
39 | normalEvent.organizer.id = event[key];
40 | break;
41 | case 'organizer_name':
42 | normalEvent.organizer.name = event[key];
43 | break;
44 | case 'organizer_email':
45 | normalEvent.organizer.email = event[key];
46 | break;
47 | case 'organizer_picture':
48 | normalEvent.organizer.picture = event[key];
49 | break;
50 | case 'rsvp_level_id':
51 | break;
52 | default:
53 | normalEvent[key] = event[key];
54 | }
55 | }
56 | const rsvpData = {
57 | event: normalEvent,
58 | rsvp: event.rsvp_level_id,
59 | };
60 | normalizedEvents.push(rsvpData);
61 | }
62 |
63 | res.locals.rsvpEvents = normalizedEvents;
64 | return next();
65 | })
66 | .catch((error) => next({
67 | log: 'Error in attendeeController.getEventsByUser',
68 | message: { err: error },
69 | }));
70 | };
71 |
72 | attendeeController.addAttendee = (req, res, next) => {
73 | const user_id = req.body.organizer.id;
74 | const rsvp_level = Number(req.params.rsvp_level);
75 | let event_id; // this needs to be here for some reason - defining the let/const in the if statement throws an error
76 | if (res.locals.dbEvent) {
77 | event_id = res.locals.dbEvent;
78 | } else {
79 | event_id = res.locals.id.id;
80 | }
81 |
82 | const queryStr = 'INSERT INTO attendees (users_id, events_id, rsvp_level_id) VALUES ($1, $2, $3) RETURNING *';
83 | const args = [user_id, event_id, rsvp_level];
84 | db.query(queryStr, args)
85 | .then((data) => {
86 | const newAttendee = data.rows[0]; // newAttendee should be an object with key/value pairs for id (primary key), users_id, events_id, rsvp_level_id
87 | res.locals.newAttendee = newAttendee;
88 | return next();
89 | })
90 | .catch((error) => next({
91 | log: 'Error in attendeeController.addAttendee',
92 | message: { err: error },
93 | }));
94 | };
95 |
96 | module.exports = attendeeController;
97 |
--------------------------------------------------------------------------------
/client/src/stylesheets/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | font-family: 'Poppins', sans-serif;
6 | padding-top: 80px;
7 | background-color: #1B1B31;
8 | background-size: cover;
9 | }
10 |
11 | button, input {
12 | font-family: 'Poppins', sans-serif;
13 | color:white;
14 | }
15 |
16 | button {
17 | cursor: pointer;
18 | }
19 | .rsvp-button {
20 | width: 150px;
21 | height: 30px;
22 | }
23 |
24 | .event-buttons-container {
25 | margin-top: 5px;
26 | }
27 |
28 | .edit-button, .delete-button {
29 | width: 100px;
30 | height: 30px;
31 | }
32 | .edit-button {
33 | margin-right: 10px;
34 | }
35 | #root {
36 | width: 100%;
37 | display: flex;
38 | flex-direction: column;
39 | /* align-items: center; */
40 | }
41 |
42 | * {
43 | /* outline: 1px solid lime; */
44 | }
45 |
46 | .info-list {
47 | list-style: none;
48 | margin:0;
49 | padding:0;
50 | }
51 |
52 | .create-event-and-rsvp{
53 | display:flex;
54 | flex-direction:row;
55 | }
56 |
57 | .rsvp-button-div{
58 | margin-top: 30px;
59 | }
60 |
61 | .info-container {
62 | /* outline: 2px solid black; */
63 | display: flex;
64 | flex-direction: column;
65 | justify-content:space-between;
66 | align-items: center;
67 | height: min-content;
68 | text-align: center;
69 | padding: 20px;
70 | min-width: 600px;
71 | min-height: 220px;
72 | margin-top: 20px;
73 | color:white;
74 | /* background-color: rgba(255, 255, 255, 0.10); */
75 | background: rgba(237, 71, 74, 0.8);
76 | /* background: linear-gradient(to right, #fb5283, #ff3527) */
77 | /* background: linear-gradient(rgba(237, 71, 74, 0.5)); */
78 | }
79 | .event-title {
80 | margin-top: -4px;
81 | }
82 | .event-description {
83 | font-style: italic;
84 | margin: -3px 0;
85 | }
86 | .map-container {
87 |
88 | min-width: 700px;
89 | min-height: 700px;
90 | margin: 0 30px;
91 | }
92 |
93 | .box-shadow-1 {
94 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), -1px 6px 9px rgba(0, 0, 0, 0.24);
95 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
96 | }
97 |
98 | .box-shadow-1:hover {
99 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
100 | }
101 |
102 | .create-event-container {
103 | text-align: center;
104 | /* outline: 1px solid gray; */
105 | height: min-content;
106 | padding: 20px;
107 | padding-top: 10px;
108 | border-radius: 5px;
109 | width: 300px;
110 | color:white;
111 | margin-right: 25px;
112 |
113 | }
114 |
115 | .button-primary {
116 | padding: 12px 0;
117 | width: 50%;
118 | margin-top: 5px;
119 | }
120 |
121 | .create-form {
122 | display: flex;
123 | flex-direction: column;
124 | /* width: 300px; */
125 | /* outline: 1px solid gray; */
126 | align-items: center;
127 | }
128 |
129 | .create-form input {
130 | border: none;
131 | outline: none;
132 | border-bottom: 1px solid rgb(126, 126, 126);
133 | text-align: center;
134 | margin-bottom: 18px;
135 | font-size: 15px;
136 | width: 85%;
137 | background: none;
138 | }
139 |
140 |
141 |
142 | .brand-container {
143 | display: flex;
144 | justify-content: center;
145 | align-items: center;
146 | }
147 |
148 | .brand-logo {
149 | width: 40px;
150 | }
151 |
152 | /* .autocomplete-container {
153 | display: inline-block;
154 | width: 100%;
155 | } */
156 | .autocomplete-container {
157 | /* outline: 2px solid red; */
158 | /* background: blue; */
159 | width: 100%;
160 | }
161 | .autocomplete-container .autocomplete-input {
162 | /* background: red; */
163 | /* width: 98%;
164 | display: inline-block; */
165 |
166 | }
167 |
168 | input:focus::placeholder {
169 | color: transparent;
170 | }
171 |
172 | input:-webkit-autofill,
173 | input:-webkit-autofill:hover,
174 | input:-webkit-autofill:focus,
175 | input:-webkit-autofill:active {
176 | transition: background-color 5000s ease-in-out 0s;
177 | -webkit-text-fill-color: white !important;
178 | }
179 |
180 |
181 |
182 | .screen-reader-text {
183 | border: 0;
184 | clip: rect(1px, 1px, 1px, 1px);
185 | clip-path: inset(50%);
186 | height: 1px;
187 | margin: -1px;
188 | width: 1px;
189 | overflow: hidden;
190 | position: absolute !important;
191 | word-wrap: normal !important;
192 | /* color:white; */
193 | }
194 |
195 |
196 |
197 | .navbar {
198 | background: rgb(234, 248, 255);
199 | /* opacity: .5; */
200 | position: fixed;
201 | top: 0;
202 | width: 100%;
203 | display: flex;
204 | justify-content: space-between;
205 | align-items: center;
206 | z-index: 200;
207 | height: 60px;
208 | }
209 |
210 | .nav-list {
211 | display: flex;
212 | list-style: none;
213 | }
214 | .nav-list li{
215 | margin-right: 20px;
216 | }
217 |
218 | a {
219 | cursor: pointer;
220 | }
221 |
222 | a:hover {
223 | text-decoration: underline;
224 | }
225 |
226 | .brand-heading {
227 | margin-left: 20px;
228 | display: inline-block;
229 | font-style: italic;
230 | /* text-decoration: underline; */
231 | font-weight: 800;
232 | font-size: 28px;
233 | color: rgb(136, 54, 244);
234 | }
235 |
236 | .map-section {
237 | /* outline: 2px solid gold; */
238 | display: flex;
239 | width: 80%;
240 |
241 | }
242 |
243 | .current-location-button {
244 | position: absolute;
245 | top: 9px;
246 | left: 50%;
247 | transform: translateX(-50%);
248 | padding: 10px;
249 | background-color:white;
250 | color:black;
251 | }
252 |
253 | button {
254 | background: none;
255 | border-radius: 3px;
256 | border: 1px solid rgb(126, 126, 126);
257 | transition: .15s ease;
258 | color:white;
259 | }
260 | button:hover {
261 | background: rgba(246, 255, 238, 0.853);
262 | color:black;
263 | }
264 |
265 | .delete-button:hover, .cancel-button:hover{
266 | background: rgba(255, 209, 220, 0.453);
267 | }
268 |
269 | #event-date{
270 | color:white;
271 | color-scheme: dark;
272 | }
273 |
274 | .right-section{
275 | display:flex;
276 | flex-direction:row;
277 | /* flex-wrap:wrap; */
278 | /* background-color:green; */
279 | }
280 |
281 | .whole-right-section {
282 | display:flex;
283 | flex-direction:column;
284 | }
--------------------------------------------------------------------------------
/client/src/components/MarkerUpdator.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import axios from 'axios';
3 | import { useJsApiLoader } from '@react-google-maps/api';
4 | import { UserContext } from './UserContext';
5 | import Autocomplete from 'react-google-autocomplete';
6 |
7 | const libraries = ['places'];
8 | export default function MarkerUpdator(props) {
9 | const { isLoaded } = useJsApiLoader({
10 | googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
11 | libraries,
12 | });
13 |
14 | const { user } = useContext(UserContext);
15 |
16 | const [name, setName] = useState(props.eventData.name);
17 | const [address, setAddress] = useState(props.eventData.address);
18 | const [date, setDate] = useState(props.eventData.date.slice(0,props.eventData.date.length-8));
19 | const [description, setDescription] = useState(props.eventData.description);
20 | const [locName, setLocName] = useState(props.eventData.locName ? props.eventData.locName : props.eventData.locname);
21 | let autocomplete = null;
22 | console.log(props.eventData);
23 |
24 | // "2023-03-22T20:21:00.000Z" database format
25 | // 2023-03-22T20:21 date format
26 | // Wed, 22 Mar 2023 20:21:00 GMT
27 | // props.eventData.date.slice(0,props.eventData.date.length-8)
28 | // graveyard of broken souls
29 |
30 | // "023-03-22T20:21"
31 | // cancel handler
32 | const cancelHandler = () => {
33 | props.setUpdating(false);
34 | }
35 |
36 | // submit handler
37 | const eventUpdate = async (e) => {
38 | e.preventDefault();
39 | try {
40 | console.log('in MARKER CREATOR user is: ', user.id);
41 | const { id, email, picture, name: username } = user;
42 | // event object for the database
43 | const event = {
44 | name,
45 | address,
46 | locName,
47 | date,
48 | description,
49 | endDate: null, // TODO: Figure out how we'll fill out this portion
50 | image_url: null, // TODO: Ticket master, Figure out how we'll fill out this portion later
51 | organizer: {
52 | id,
53 | username,
54 | email,
55 | picture
56 | },
57 | ticketmaster_evt_id: null,
58 | rsvp_url: null,
59 | evt_origin_type_id: 1, // user created = 1, ticketmaster = 2
60 | id: props.eventData.id
61 | };
62 | // console.log('eventID: ', props.eventData.id);
63 | // encode the address and geocode it
64 | const encoded = address.replaceAll(' ', '+');
65 | const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encoded}&key=${import.meta.env.VITE_GOOGLE_MAPS_API_KEY}`;
66 | const response = await axios.get(url);
67 | const data = response.data.results[0];
68 | event.location = {
69 | lat: data.geometry.location.lat,
70 | lng: data.geometry.location.lng,
71 | };
72 | // send the update request to the database
73 | const eventID = await axios.put('/api/events', event);
74 | event.eventID = eventID.data;
75 | // replace the MarkerData in state with the updated array
76 | props.setMarkerData(prevMarkerData => {
77 | // remove the edited event
78 | // could make more performant with map instead of filter
79 | const updatedMarkers = prevMarkerData.filter(event => {
80 | return event.id !== event.eventID;
81 | });
82 | // spread in the filtered old events with the new event added in
83 | return [...updatedMarkers, event];
84 | });
85 | // console.log('MarkerUpdator: updatedMarkers: ', updatedMarkers);
86 | // update window closes and is replaced with add event
87 | props.setUpdating(false);
88 | //console.log('most recent marker is: ', markerData[markerData.length - 1]);
89 | // email from context and organizer from context
90 | // get event id to store in state
91 | } catch (err) {
92 | console.log('error in post: ', err.message);
93 | }
94 | };
95 |
96 | // autocomplete onLoad
97 | function onLoad(ac) {
98 | console.log('here in ONLOAD, ac is: ', ac);
99 | autocomplete = ac;
100 | }
101 |
102 | // autocomplete change handler
103 | function handleChange() {
104 | console.log('autocomplete is currently: ', autocomplete);
105 | if(autocomplete !== null) console.log('autocomplete place is: ', autocomplete.getPlace());
106 | }
107 |
108 | return (
109 |
110 |
Edit your Event
111 |
142 |
Cancel
143 |
144 | );
145 | }
146 |
147 |
148 |
--------------------------------------------------------------------------------
/client/src/components/MarkerCreator.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useRef } from "react";
2 | import axios from "axios";
3 | import { useJsApiLoader } from "@react-google-maps/api";
4 | import Autocomplete from "react-google-autocomplete";
5 | import { UserContext } from "./UserContext";
6 |
7 | const libraries = ["places"];
8 | export default function MarkerCreator(props) {
9 | const { isLoaded } = useJsApiLoader({
10 | googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
11 | libraries,
12 | });
13 |
14 | const { user } = useContext(UserContext);
15 | // state for controlled inputs
16 | const [name, setName] = useState("");
17 | const [address, setAddress] = useState("");
18 | const [date, setDate] = useState("");
19 | const [endDate, setEndDate] = useState("");
20 | const [description, setDescription] = useState("");
21 | const [locName, setLocName] = useState("");
22 | // const [endDate,]
23 | let autocomplete = null;
24 |
25 | // submit handler
26 | const eventSubmit = async (e) => {
27 | e.preventDefault();
28 | try {
29 | console.log("in MARKER CREATOR user is: ", user.id);
30 | const { id, email, picture, name: username } = user;
31 | // new event object for database
32 | // const event = {
33 | // name,
34 | // address,
35 | // locName,
36 | // date,
37 | // description,
38 | // userID: id,
39 | // };
40 |
41 |
42 | // Modified event object: testing
43 | const event = {
44 | name,
45 | address,
46 | id: null,
47 | date,
48 | description,
49 | locName,
50 | endDate: null, // TODO: Figure out how we'll fill out this portion
51 | image_url: null, // TODO: Ticket master, Figure out how we'll fill out this portion later
52 | organizer: {
53 | id,
54 | username,
55 | email,
56 | picture
57 | },
58 | ticketmaster_evt_id: null,
59 | rsvp_url: null,
60 | evt_origin_type_id: 1, // user created = 1, ticketmaster = 2
61 | // location is added below on line 72 - 74 after we've received data from google's geocode api
62 | };
63 |
64 | // encode the address
65 | const encoded = address.replaceAll(" ", "+");
66 | // geocode the address (https://developers.google.com/maps/documentation/geocoding/requests-geocoding)
67 | const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encoded}&key=${
68 | import.meta.env.VITE_GOOGLE_MAPS_API_KEY
69 | }`;
70 | const response = await axios.get(url);
71 | const data = response.data.results[0];
72 | event.location = {
73 | lat: data.geometry.location.lat,
74 | lng: data.geometry.location.lng,
75 | }
76 | // send the post request to the server
77 | const eventID = await axios.post("/api/events", event);
78 | // add other pairs to the event object for the front-end to read
79 | event.id = eventID.data.id;
80 | // add the new event into state (from parent component) to rerender the map + markers
81 | props.setMarkerData((prevMarkerData) => [...prevMarkerData, event]);
82 | } catch (err) {
83 | console.log("error in post: ", err.message);
84 | }
85 | };
86 |
87 | // autocomplete onLoad
88 | function onLoad(ac) {
89 | console.log("here in ONLOAD, ac is: ", ac);
90 | autocomplete = ac;
91 | }
92 |
93 | // autocomplete change handler
94 | function handleChange() {
95 | console.log("autocomplete is currently: ", autocomplete);
96 | if (autocomplete !== null) {
97 | console.log("autocomplete place is: ", autocomplete.getPlace());
98 | }
99 | }
100 |
101 | // component imported from @react-google-maps/api to have autocomplete address
102 | return (
103 |
104 |
105 |
Create an Event
106 |
176 |
177 |
178 | );
179 | }
180 |
--------------------------------------------------------------------------------
/server/controllers/eventController.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | // import the event model
3 | const EventModel = require('../models/eventModel.js');
4 | const db = require('../models/dbModel.js');
5 |
6 | const EVENT_ORIGIN_TYPE = {
7 | 'USER': 1,
8 | 'TICKETMASTER': 2
9 | };
10 |
11 | const eventController = {};
12 |
13 | // find a specific event in the db that the user is rsvp-ing to, to see if we need to add the event to the db
14 | eventController.findEvent = (req, res, next) => {
15 | // check what type of event it is - if it is a user event then it is already in the events table then return next()
16 | const { evt_origin_type_id } = req.body;
17 | if (evt_origin_type_id === 1) {
18 | res.locals.dbEvent = req.body.id;
19 | return next();
20 | }
21 | // if the event is a ticketmaster event then we need to check if it's already in the events table
22 | const { ticketmaster_evt_id } = req.body;
23 | const queryStr = 'SELECT id, ticketmaster_evt_id FROM events WHERE evt_origin_type_id = $1';
24 | db.query(queryStr, [evt_origin_type_id])
25 | .then((data) => {
26 | const ticketmasterEvents = data.rows;
27 | for (let i = 0; i < ticketmasterEvents.length; i += 1) {
28 | if (ticketmasterEvents[i].ticketmaster_evt_id === ticketmaster_evt_id) {
29 | res.locals.dbEvent = ticketmasterEvents[i].id;
30 | return next();
31 | }
32 | }
33 | // if the ticketmaster event ID is not in the events table then we need to create an event
34 | res.locals.dbEvent = false;
35 | return next();
36 | })
37 | .catch((error) => next({
38 | log: 'Error in eventController.findEvent',
39 | message: { err: error },
40 | }));
41 | };
42 |
43 | // get all events from database
44 | eventController.getEvents = async (req, res, next) => {
45 | try {
46 | /* select event information,
47 | using jsonb_agg to create a json object out of lat and lng by declaring key/value pairs */
48 | const queryStr = `
49 | SELECT e.id,
50 | e.name,
51 | e.description,
52 | e.date,
53 | e.loc_name AS locName,
54 | e.address,
55 | e.lat,
56 | e.lng,
57 | e.end_date,
58 | e.image_url,
59 | e.ticketmaster_evt_id,
60 | e.evt_origin_type_id,
61 | e.rsvp_url,
62 | u.id AS organizer_id,
63 | u.name AS organizer_name,
64 | u.email AS organizer_email,
65 | u.picture AS organizer_picture
66 | FROM events e
67 | LEFT OUTER JOIN users u
68 | ON e.organizer_id = u.id
69 | WHERE e.evt_origin_type_id = ${EVENT_ORIGIN_TYPE.USER}
70 | GROUP BY e.id, u.name, u.email, u.picture, u.id`;
71 |
72 | const queryResult = await db.query(queryStr);
73 |
74 | // Iterate through the rows, creating an EventModel for each row
75 | // Push all EventModels into the events array
76 | const eventsArr = [];
77 | queryResult.rows.forEach(row => {
78 | eventsArr.push(new EventModel(
79 | row.name,
80 | row.address,
81 | row.date,
82 | row.description,
83 | row.id,
84 | row.lat,
85 | row.lng,
86 | row.locName,
87 | row.end_date,
88 | row.image_url,
89 | row.ticketmaster_evt_id,
90 | row.rsvp_url,
91 | row.evt_origin_type_id,
92 | row.organizer_id,
93 | row.organizer_name,
94 | row.organizer_email,
95 | row.organizer_picture,
96 | ));
97 | });
98 |
99 | res.locals.events = eventsArr;
100 | // query shape: {something: x, rows:[{data}, {data2}], blah: y, ....}
101 | return next();
102 | } catch (error) {
103 | return next({
104 | log: 'eventController.getEvents error',
105 | message: { err: error.message },
106 | });
107 | }
108 | };
109 |
110 | // create a new event in the database
111 | eventController.createEvent = async (req, res, next) => {
112 | try {
113 | if (!res.locals.dbEvent) {
114 | console.log('in event creator with req: ', req.body);
115 |
116 | const {
117 | name,
118 | address,
119 | date,
120 | description,
121 | location,
122 | locName,
123 | end_date,
124 | image_url,
125 | ticketmaster_evt_id,
126 | rsvp_url,
127 | evt_origin_type_id,
128 | organizer,
129 | } = req.body;
130 |
131 | const { lat, lng } = req.body.location;
132 | const organizer_id = req.body.organizer.id;
133 |
134 | // Make sure organizer_id and evt_origin_type_id are populated in the request body
135 | // If not, throw an error
136 | if (organizer_id === undefined
137 | || organizer_id === null
138 | || evt_origin_type_id === undefined
139 | || evt_origin_type_id === null) {
140 | throw new Error('Missing required fields');
141 | }
142 |
143 | // updated: insert the event into the database using subquery for the organizer id
144 | const addEventQuery = `INSERT INTO events
145 | (
146 | name,
147 | address,
148 | date,
149 | description,
150 | lat,
151 | lng,
152 | loc_name,
153 | end_date,
154 | image_url,
155 | ticketmaster_evt_id,
156 | rsvp_url,
157 | evt_origin_type_id,
158 | organizer_id
159 | )
160 | VALUES
161 | (
162 | $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
163 | )
164 | RETURNING id`;
165 |
166 | // const newEventVals = [name, description, date, locName, address, lat, lng, userID];
167 |
168 | const newEventVals = [
169 | name,
170 | address,
171 | date,
172 | description,
173 | lat,
174 | lng,
175 | locName,
176 | end_date,
177 | image_url,
178 | ticketmaster_evt_id,
179 | rsvp_url,
180 | evt_origin_type_id,
181 | organizer_id,
182 | ];
183 | const newEvent = await db.query(addEventQuery, newEventVals);
184 |
185 | // **note - that rows[0] will actually be an OBJECT containing {id: } ** !
186 | res.locals.id = newEvent.rows[0];
187 | }
188 | return next();
189 | } catch (error) {
190 | return next({
191 | log: 'eventController.createEvent error',
192 | message: { err: error.message },
193 | });
194 | }
195 | };
196 |
197 | // update an event in the database
198 | eventController.updateEvent = async (req, res, next) => {
199 | const {
200 | name, description, date, locName, address, userID, eventID,
201 | } = req.body;
202 | const { lat, lng } = req.body.location;
203 | const values = [name, description, date, locName, address, lat, lng, userID, eventID];
204 | const text = 'UPDATE events SET name = $1, description = $2, date = $3, loc_name = $4, address = $5, lat = $6, lng = $7 WHERE organizer_id = $8 AND id = $9;';
205 | try {
206 | await db.query(text, values);
207 | return next();
208 | } catch (error) {
209 | return next({
210 | log: 'eventController.updateEvent error',
211 | message: { err: 'Error updating event in database' },
212 | });
213 | }
214 | };
215 |
216 | // delete an event from the database
217 | eventController.deleteEvent = async (req, res, next) => {
218 | const { eventID, userID } = req.body.deleteReq;
219 | const values = [eventID, userID];
220 | const text = 'DELETE FROM events WHERE id = $1 AND organizer_id = $2';
221 | try {
222 | await db.query(text, values);
223 | return next();
224 | } catch (error) {
225 | return next({
226 | log: 'eventController.deleteEvent error',
227 | message: { err: 'Error deleting event from database' },
228 | });
229 | }
230 | };
231 |
232 | module.exports = eventController;
233 |
--------------------------------------------------------------------------------
/client/src/components/Map.jsx:
--------------------------------------------------------------------------------
1 | import "../stylesheets/App.css";
2 | import React, { useState, useEffect, useContext } from "react";
3 | import { GoogleMap, useJsApiLoader, MarkerF } from "@react-google-maps/api";
4 | /* https://www.npmjs.com/package/@react-google-maps/api */
5 |
6 | import axios from "axios";
7 | import MarkerCreator from "./MarkerCreator";
8 | import MarkerUpdator from "./MarkerUpdator";
9 | import { UserContext } from "./UserContext";
10 | import RsvpDisplay from "./RsvpDisplay";
11 |
12 | function Map() {
13 | // state for map center positioning
14 | const [mapPos, setMapPos] = useState({
15 | lat: 0.37766641e2,
16 | lng: -0.123098308e3,
17 | });
18 |
19 | // state for the data for marker from the database
20 | const [markerData, setMarkerData] = useState([]);
21 | const [ticketMasterData, setTicketMasterData] = useState([]);
22 | const [rsvp, setRSVP] = useState(0);
23 |
24 | // state to display the event data to the page after clicking a marker
25 | const [eventData, setEventData] = useState(null);
26 |
27 | // get the userID from the context
28 | // userID is used to determine if the user is the creator of the event
29 | const { user } = useContext(UserContext);
30 | const [updating, setUpdating] = useState(false);
31 | // in-the-works refactor to clarify userID vs eventID from .id
32 | const userID = user === null ? null : user.id;
33 |
34 | // Load the script for google maps API
35 | const { isLoaded } = useJsApiLoader({
36 | googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
37 | // we don't think this is actually used, but removing it breaks EVERYTHING?!
38 | libraries: ["places"],
39 | });
40 |
41 | // get all marker data from database on mount
42 | useEffect(() => {
43 | try {
44 | const getEvents = async () => {
45 | const response = await axios.get("/api/events");
46 | const { data } = response;
47 | // console.log(data);
48 | setMarkerData(data);
49 | };
50 |
51 | const getTicketMasterEvents = async () => {
52 | // const posi = {};
53 | // navigator.geolocation.getCurrentPosition((position) => {
54 | // posi.lat = position.coords.latitude;
55 | // posi.lng = position.coords.longitude;
56 | // })
57 |
58 | // console.log('This posi is: ', Object.keys(posi));
59 | // console.log('This is lat: ', posi.lat);
60 | // console.log('This is lng: ', posi.lng);
61 | const response = await axios.get('/api/ticketmaster/34.0522/-118.2437');
62 |
63 | // const response = await axios.get(`/api/ticketmaster/${posi.lat}/${posi.lng}`);
64 | const { data } = response;
65 | console.log(data);
66 | // console.log("getTicketMasterEvents: ", Object.values(data));
67 | setTicketMasterData(Object.values(data));
68 | };
69 | getEvents();
70 | getTicketMasterEvents();
71 | // get current user location and set the center of the map to that location
72 | if (navigator.geolocation) {
73 | // native browser geolocation functionality
74 | navigator.geolocation.getCurrentPosition((position) => {
75 | const pos = {
76 | lat: position.coords.latitude,
77 | lng: position.coords.longitude,
78 | };
79 | // change map center positioning state
80 | setMapPos(pos);
81 | });
82 | }
83 | } catch (e) {
84 | console.log("error in getEvents: ", e.message);
85 | }
86 | }, []);
87 |
88 | // change google map position to current user location on button click
89 | const currPosition = () => {
90 | if (navigator.geolocation) {
91 | navigator.geolocation.getCurrentPosition((position) => {
92 | const pos = {
93 | lat: position.coords.latitude,
94 | lng: position.coords.longitude,
95 | };
96 | // change map center positioning state
97 | setMapPos(pos);
98 | });
99 | }
100 | };
101 |
102 | // handle click on update button
103 | const editUsersEvent = () => {
104 | setUpdating(true);
105 | };
106 |
107 | // handle click on delete button
108 | const deleteUsersEvent = async (eID, uID) => {
109 | // create the object for the db query on backend
110 | const deleteReq = {
111 | eventID: eID,
112 | userID: uID,
113 | };
114 | // send object to the server to delete the event
115 | const response = await axios.delete("/api/events/", {
116 | data: { deleteReq },
117 | });
118 | // filter the removed event from the marker data array
119 | setMarkerData((prevMarkerData) =>
120 | prevMarkerData.filter((event) => event.id !== eID)
121 | );
122 | setEventData(null);
123 | };
124 |
125 | const sendRSVP = async (e) => {
126 | const rsvpVal = e.target.value;
127 | console.log(eventData);
128 |
129 | const response = await axios.post(`/api/rsvp/${rsvpVal}`, eventData);
130 | console.log(response);
131 | };
132 |
133 | // ensures that a div exists for the map even when the map API key is not loaded successfully. DO NOT DELETE
134 | if (!isLoaded) return Loading... 🥺
;
135 | // component imported from @react-google-maps/api used to render google maps
136 | // https://react-google-maps-api-docs.netlify.app/#googlemap
137 |
138 | return (
139 |
140 |
145 | currPosition()}
148 | >
149 | Search Near Me
150 |
151 | {/* If markerData is changed, places corresponding Markers in the map */}
152 | {/* component imported from @react-google-maps/api renders markers on the map */}
153 | {markerData.length > 0 &&
154 | markerData.map((event) => (
155 | // console.log('User created Events: ', event);
156 |
157 | setEventData(event)}
166 | />
167 | ))}
168 | {ticketMasterData.length > 0 &&
169 | ticketMasterData.map((event, index) => (
170 | // console.log({
171 | // lat: event.lat,
172 | // lng: event.lng,
173 | // });
174 | // console.log('ticketMaster Events: ', event);
175 | // Lines 170 & 171: Have to parseFloat() to avoid type coercion
176 | setEventData(event)}
187 | />
188 | ))}
189 |
190 | {/* If a Marker is being added, call MarkerCreator and if updated, call MarkerUpdator */}
191 |
192 |
193 | {!updating && }
194 | {updating && (
195 |
201 | )}
202 |
203 |
204 |
205 | {/* If eventData and user are not null, display the event data */}
206 | {/* TODO: Ensure this will work for both ticketmaster and user created events once backends changes have been merged into dev */}
207 | {eventData && user && (
208 |
209 |
{eventData.name}
210 |
{eventData.description}
211 |
240 | {/* If the user is the creator of the event, display the edit and delete buttons */}
241 | {eventData.organizer.email === user.email && (
242 |
243 |
248 | {" "}
249 | Edit{" "}
250 |
251 | deleteUsersEvent(eventData.id, user.id)}
255 | >
256 | {" "}
257 | Delete{" "}
258 |
259 |
260 | )}
261 |
262 | )}
263 |
264 |
265 | );
266 | }
267 |
268 | export default Map;
269 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "server",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "cookie-parser": "^1.4.6",
13 | "express": "^4.18.2",
14 | "express-session": "^1.17.3",
15 | "google-auth-library": "^8.7.0",
16 | "jwt-decode": "^3.1.2",
17 | "pg": "^8.9.0"
18 | },
19 | "devDependencies": {
20 | "dotenv": "^16.0.3"
21 | }
22 | },
23 | "node_modules/accepts": {
24 | "version": "1.3.8",
25 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
26 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
27 | "dependencies": {
28 | "mime-types": "~2.1.34",
29 | "negotiator": "0.6.3"
30 | },
31 | "engines": {
32 | "node": ">= 0.6"
33 | }
34 | },
35 | "node_modules/agent-base": {
36 | "version": "6.0.2",
37 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
38 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
39 | "dependencies": {
40 | "debug": "4"
41 | },
42 | "engines": {
43 | "node": ">= 6.0.0"
44 | }
45 | },
46 | "node_modules/agent-base/node_modules/debug": {
47 | "version": "4.3.4",
48 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
49 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
50 | "dependencies": {
51 | "ms": "2.1.2"
52 | },
53 | "engines": {
54 | "node": ">=6.0"
55 | },
56 | "peerDependenciesMeta": {
57 | "supports-color": {
58 | "optional": true
59 | }
60 | }
61 | },
62 | "node_modules/agent-base/node_modules/ms": {
63 | "version": "2.1.2",
64 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
65 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
66 | },
67 | "node_modules/array-flatten": {
68 | "version": "1.1.1",
69 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
70 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
71 | },
72 | "node_modules/arrify": {
73 | "version": "2.0.1",
74 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
75 | "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
76 | "engines": {
77 | "node": ">=8"
78 | }
79 | },
80 | "node_modules/base64-js": {
81 | "version": "1.5.1",
82 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
83 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
84 | "funding": [
85 | {
86 | "type": "github",
87 | "url": "https://github.com/sponsors/feross"
88 | },
89 | {
90 | "type": "patreon",
91 | "url": "https://www.patreon.com/feross"
92 | },
93 | {
94 | "type": "consulting",
95 | "url": "https://feross.org/support"
96 | }
97 | ]
98 | },
99 | "node_modules/bignumber.js": {
100 | "version": "9.1.1",
101 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz",
102 | "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==",
103 | "engines": {
104 | "node": "*"
105 | }
106 | },
107 | "node_modules/body-parser": {
108 | "version": "1.20.1",
109 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
110 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
111 | "dependencies": {
112 | "bytes": "3.1.2",
113 | "content-type": "~1.0.4",
114 | "debug": "2.6.9",
115 | "depd": "2.0.0",
116 | "destroy": "1.2.0",
117 | "http-errors": "2.0.0",
118 | "iconv-lite": "0.4.24",
119 | "on-finished": "2.4.1",
120 | "qs": "6.11.0",
121 | "raw-body": "2.5.1",
122 | "type-is": "~1.6.18",
123 | "unpipe": "1.0.0"
124 | },
125 | "engines": {
126 | "node": ">= 0.8",
127 | "npm": "1.2.8000 || >= 1.4.16"
128 | }
129 | },
130 | "node_modules/buffer-equal-constant-time": {
131 | "version": "1.0.1",
132 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
133 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
134 | },
135 | "node_modules/buffer-writer": {
136 | "version": "2.0.0",
137 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
138 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
139 | "engines": {
140 | "node": ">=4"
141 | }
142 | },
143 | "node_modules/bytes": {
144 | "version": "3.1.2",
145 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
146 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
147 | "engines": {
148 | "node": ">= 0.8"
149 | }
150 | },
151 | "node_modules/call-bind": {
152 | "version": "1.0.2",
153 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
154 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
155 | "dependencies": {
156 | "function-bind": "^1.1.1",
157 | "get-intrinsic": "^1.0.2"
158 | },
159 | "funding": {
160 | "url": "https://github.com/sponsors/ljharb"
161 | }
162 | },
163 | "node_modules/content-disposition": {
164 | "version": "0.5.4",
165 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
166 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
167 | "dependencies": {
168 | "safe-buffer": "5.2.1"
169 | },
170 | "engines": {
171 | "node": ">= 0.6"
172 | }
173 | },
174 | "node_modules/content-type": {
175 | "version": "1.0.5",
176 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
177 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
178 | "engines": {
179 | "node": ">= 0.6"
180 | }
181 | },
182 | "node_modules/cookie": {
183 | "version": "0.5.0",
184 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
185 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
186 | "engines": {
187 | "node": ">= 0.6"
188 | }
189 | },
190 | "node_modules/cookie-parser": {
191 | "version": "1.4.6",
192 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
193 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
194 | "dependencies": {
195 | "cookie": "0.4.1",
196 | "cookie-signature": "1.0.6"
197 | },
198 | "engines": {
199 | "node": ">= 0.8.0"
200 | }
201 | },
202 | "node_modules/cookie-parser/node_modules/cookie": {
203 | "version": "0.4.1",
204 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
205 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
206 | "engines": {
207 | "node": ">= 0.6"
208 | }
209 | },
210 | "node_modules/cookie-signature": {
211 | "version": "1.0.6",
212 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
213 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
214 | },
215 | "node_modules/debug": {
216 | "version": "2.6.9",
217 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
218 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
219 | "dependencies": {
220 | "ms": "2.0.0"
221 | }
222 | },
223 | "node_modules/depd": {
224 | "version": "2.0.0",
225 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
226 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
227 | "engines": {
228 | "node": ">= 0.8"
229 | }
230 | },
231 | "node_modules/destroy": {
232 | "version": "1.2.0",
233 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
234 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
235 | "engines": {
236 | "node": ">= 0.8",
237 | "npm": "1.2.8000 || >= 1.4.16"
238 | }
239 | },
240 | "node_modules/dotenv": {
241 | "version": "16.0.3",
242 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
243 | "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
244 | "dev": true,
245 | "engines": {
246 | "node": ">=12"
247 | }
248 | },
249 | "node_modules/ecdsa-sig-formatter": {
250 | "version": "1.0.11",
251 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
252 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
253 | "dependencies": {
254 | "safe-buffer": "^5.0.1"
255 | }
256 | },
257 | "node_modules/ee-first": {
258 | "version": "1.1.1",
259 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
260 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
261 | },
262 | "node_modules/encodeurl": {
263 | "version": "1.0.2",
264 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
265 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
266 | "engines": {
267 | "node": ">= 0.8"
268 | }
269 | },
270 | "node_modules/escape-html": {
271 | "version": "1.0.3",
272 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
273 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
274 | },
275 | "node_modules/etag": {
276 | "version": "1.8.1",
277 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
278 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
279 | "engines": {
280 | "node": ">= 0.6"
281 | }
282 | },
283 | "node_modules/express": {
284 | "version": "4.18.2",
285 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
286 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
287 | "dependencies": {
288 | "accepts": "~1.3.8",
289 | "array-flatten": "1.1.1",
290 | "body-parser": "1.20.1",
291 | "content-disposition": "0.5.4",
292 | "content-type": "~1.0.4",
293 | "cookie": "0.5.0",
294 | "cookie-signature": "1.0.6",
295 | "debug": "2.6.9",
296 | "depd": "2.0.0",
297 | "encodeurl": "~1.0.2",
298 | "escape-html": "~1.0.3",
299 | "etag": "~1.8.1",
300 | "finalhandler": "1.2.0",
301 | "fresh": "0.5.2",
302 | "http-errors": "2.0.0",
303 | "merge-descriptors": "1.0.1",
304 | "methods": "~1.1.2",
305 | "on-finished": "2.4.1",
306 | "parseurl": "~1.3.3",
307 | "path-to-regexp": "0.1.7",
308 | "proxy-addr": "~2.0.7",
309 | "qs": "6.11.0",
310 | "range-parser": "~1.2.1",
311 | "safe-buffer": "5.2.1",
312 | "send": "0.18.0",
313 | "serve-static": "1.15.0",
314 | "setprototypeof": "1.2.0",
315 | "statuses": "2.0.1",
316 | "type-is": "~1.6.18",
317 | "utils-merge": "1.0.1",
318 | "vary": "~1.1.2"
319 | },
320 | "engines": {
321 | "node": ">= 0.10.0"
322 | }
323 | },
324 | "node_modules/express-session": {
325 | "version": "1.17.3",
326 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
327 | "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
328 | "dependencies": {
329 | "cookie": "0.4.2",
330 | "cookie-signature": "1.0.6",
331 | "debug": "2.6.9",
332 | "depd": "~2.0.0",
333 | "on-headers": "~1.0.2",
334 | "parseurl": "~1.3.3",
335 | "safe-buffer": "5.2.1",
336 | "uid-safe": "~2.1.5"
337 | },
338 | "engines": {
339 | "node": ">= 0.8.0"
340 | }
341 | },
342 | "node_modules/express-session/node_modules/cookie": {
343 | "version": "0.4.2",
344 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
345 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
346 | "engines": {
347 | "node": ">= 0.6"
348 | }
349 | },
350 | "node_modules/extend": {
351 | "version": "3.0.2",
352 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
353 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
354 | },
355 | "node_modules/fast-text-encoding": {
356 | "version": "1.0.6",
357 | "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
358 | "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="
359 | },
360 | "node_modules/finalhandler": {
361 | "version": "1.2.0",
362 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
363 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
364 | "dependencies": {
365 | "debug": "2.6.9",
366 | "encodeurl": "~1.0.2",
367 | "escape-html": "~1.0.3",
368 | "on-finished": "2.4.1",
369 | "parseurl": "~1.3.3",
370 | "statuses": "2.0.1",
371 | "unpipe": "~1.0.0"
372 | },
373 | "engines": {
374 | "node": ">= 0.8"
375 | }
376 | },
377 | "node_modules/forwarded": {
378 | "version": "0.2.0",
379 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
380 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
381 | "engines": {
382 | "node": ">= 0.6"
383 | }
384 | },
385 | "node_modules/fresh": {
386 | "version": "0.5.2",
387 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
388 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
389 | "engines": {
390 | "node": ">= 0.6"
391 | }
392 | },
393 | "node_modules/function-bind": {
394 | "version": "1.1.1",
395 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
396 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
397 | },
398 | "node_modules/gaxios": {
399 | "version": "5.0.2",
400 | "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz",
401 | "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==",
402 | "dependencies": {
403 | "extend": "^3.0.2",
404 | "https-proxy-agent": "^5.0.0",
405 | "is-stream": "^2.0.0",
406 | "node-fetch": "^2.6.7"
407 | },
408 | "engines": {
409 | "node": ">=12"
410 | }
411 | },
412 | "node_modules/gcp-metadata": {
413 | "version": "5.2.0",
414 | "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz",
415 | "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==",
416 | "dependencies": {
417 | "gaxios": "^5.0.0",
418 | "json-bigint": "^1.0.0"
419 | },
420 | "engines": {
421 | "node": ">=12"
422 | }
423 | },
424 | "node_modules/get-intrinsic": {
425 | "version": "1.2.0",
426 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
427 | "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
428 | "dependencies": {
429 | "function-bind": "^1.1.1",
430 | "has": "^1.0.3",
431 | "has-symbols": "^1.0.3"
432 | },
433 | "funding": {
434 | "url": "https://github.com/sponsors/ljharb"
435 | }
436 | },
437 | "node_modules/google-auth-library": {
438 | "version": "8.7.0",
439 | "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz",
440 | "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==",
441 | "dependencies": {
442 | "arrify": "^2.0.0",
443 | "base64-js": "^1.3.0",
444 | "ecdsa-sig-formatter": "^1.0.11",
445 | "fast-text-encoding": "^1.0.0",
446 | "gaxios": "^5.0.0",
447 | "gcp-metadata": "^5.0.0",
448 | "gtoken": "^6.1.0",
449 | "jws": "^4.0.0",
450 | "lru-cache": "^6.0.0"
451 | },
452 | "engines": {
453 | "node": ">=12"
454 | }
455 | },
456 | "node_modules/google-p12-pem": {
457 | "version": "4.0.1",
458 | "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz",
459 | "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==",
460 | "dependencies": {
461 | "node-forge": "^1.3.1"
462 | },
463 | "bin": {
464 | "gp12-pem": "build/src/bin/gp12-pem.js"
465 | },
466 | "engines": {
467 | "node": ">=12.0.0"
468 | }
469 | },
470 | "node_modules/gtoken": {
471 | "version": "6.1.2",
472 | "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz",
473 | "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==",
474 | "dependencies": {
475 | "gaxios": "^5.0.1",
476 | "google-p12-pem": "^4.0.0",
477 | "jws": "^4.0.0"
478 | },
479 | "engines": {
480 | "node": ">=12.0.0"
481 | }
482 | },
483 | "node_modules/has": {
484 | "version": "1.0.3",
485 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
486 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
487 | "dependencies": {
488 | "function-bind": "^1.1.1"
489 | },
490 | "engines": {
491 | "node": ">= 0.4.0"
492 | }
493 | },
494 | "node_modules/has-symbols": {
495 | "version": "1.0.3",
496 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
497 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
498 | "engines": {
499 | "node": ">= 0.4"
500 | },
501 | "funding": {
502 | "url": "https://github.com/sponsors/ljharb"
503 | }
504 | },
505 | "node_modules/http-errors": {
506 | "version": "2.0.0",
507 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
508 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
509 | "dependencies": {
510 | "depd": "2.0.0",
511 | "inherits": "2.0.4",
512 | "setprototypeof": "1.2.0",
513 | "statuses": "2.0.1",
514 | "toidentifier": "1.0.1"
515 | },
516 | "engines": {
517 | "node": ">= 0.8"
518 | }
519 | },
520 | "node_modules/https-proxy-agent": {
521 | "version": "5.0.1",
522 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
523 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
524 | "dependencies": {
525 | "agent-base": "6",
526 | "debug": "4"
527 | },
528 | "engines": {
529 | "node": ">= 6"
530 | }
531 | },
532 | "node_modules/https-proxy-agent/node_modules/debug": {
533 | "version": "4.3.4",
534 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
535 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
536 | "dependencies": {
537 | "ms": "2.1.2"
538 | },
539 | "engines": {
540 | "node": ">=6.0"
541 | },
542 | "peerDependenciesMeta": {
543 | "supports-color": {
544 | "optional": true
545 | }
546 | }
547 | },
548 | "node_modules/https-proxy-agent/node_modules/ms": {
549 | "version": "2.1.2",
550 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
551 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
552 | },
553 | "node_modules/iconv-lite": {
554 | "version": "0.4.24",
555 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
556 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
557 | "dependencies": {
558 | "safer-buffer": ">= 2.1.2 < 3"
559 | },
560 | "engines": {
561 | "node": ">=0.10.0"
562 | }
563 | },
564 | "node_modules/inherits": {
565 | "version": "2.0.4",
566 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
567 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
568 | },
569 | "node_modules/ipaddr.js": {
570 | "version": "1.9.1",
571 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
572 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
573 | "engines": {
574 | "node": ">= 0.10"
575 | }
576 | },
577 | "node_modules/is-stream": {
578 | "version": "2.0.1",
579 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
580 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
581 | "engines": {
582 | "node": ">=8"
583 | },
584 | "funding": {
585 | "url": "https://github.com/sponsors/sindresorhus"
586 | }
587 | },
588 | "node_modules/json-bigint": {
589 | "version": "1.0.0",
590 | "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
591 | "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
592 | "dependencies": {
593 | "bignumber.js": "^9.0.0"
594 | }
595 | },
596 | "node_modules/jwa": {
597 | "version": "2.0.0",
598 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
599 | "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
600 | "dependencies": {
601 | "buffer-equal-constant-time": "1.0.1",
602 | "ecdsa-sig-formatter": "1.0.11",
603 | "safe-buffer": "^5.0.1"
604 | }
605 | },
606 | "node_modules/jws": {
607 | "version": "4.0.0",
608 | "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
609 | "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
610 | "dependencies": {
611 | "jwa": "^2.0.0",
612 | "safe-buffer": "^5.0.1"
613 | }
614 | },
615 | "node_modules/jwt-decode": {
616 | "version": "3.1.2",
617 | "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
618 | "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
619 | },
620 | "node_modules/lru-cache": {
621 | "version": "6.0.0",
622 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
623 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
624 | "dependencies": {
625 | "yallist": "^4.0.0"
626 | },
627 | "engines": {
628 | "node": ">=10"
629 | }
630 | },
631 | "node_modules/media-typer": {
632 | "version": "0.3.0",
633 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
634 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
635 | "engines": {
636 | "node": ">= 0.6"
637 | }
638 | },
639 | "node_modules/merge-descriptors": {
640 | "version": "1.0.1",
641 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
642 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
643 | },
644 | "node_modules/methods": {
645 | "version": "1.1.2",
646 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
647 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
648 | "engines": {
649 | "node": ">= 0.6"
650 | }
651 | },
652 | "node_modules/mime": {
653 | "version": "1.6.0",
654 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
655 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
656 | "bin": {
657 | "mime": "cli.js"
658 | },
659 | "engines": {
660 | "node": ">=4"
661 | }
662 | },
663 | "node_modules/mime-db": {
664 | "version": "1.52.0",
665 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
666 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
667 | "engines": {
668 | "node": ">= 0.6"
669 | }
670 | },
671 | "node_modules/mime-types": {
672 | "version": "2.1.35",
673 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
674 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
675 | "dependencies": {
676 | "mime-db": "1.52.0"
677 | },
678 | "engines": {
679 | "node": ">= 0.6"
680 | }
681 | },
682 | "node_modules/ms": {
683 | "version": "2.0.0",
684 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
685 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
686 | },
687 | "node_modules/negotiator": {
688 | "version": "0.6.3",
689 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
690 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
691 | "engines": {
692 | "node": ">= 0.6"
693 | }
694 | },
695 | "node_modules/node-fetch": {
696 | "version": "2.6.9",
697 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
698 | "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==",
699 | "dependencies": {
700 | "whatwg-url": "^5.0.0"
701 | },
702 | "engines": {
703 | "node": "4.x || >=6.0.0"
704 | },
705 | "peerDependencies": {
706 | "encoding": "^0.1.0"
707 | },
708 | "peerDependenciesMeta": {
709 | "encoding": {
710 | "optional": true
711 | }
712 | }
713 | },
714 | "node_modules/node-forge": {
715 | "version": "1.3.1",
716 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
717 | "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
718 | "engines": {
719 | "node": ">= 6.13.0"
720 | }
721 | },
722 | "node_modules/object-inspect": {
723 | "version": "1.12.3",
724 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
725 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
726 | "funding": {
727 | "url": "https://github.com/sponsors/ljharb"
728 | }
729 | },
730 | "node_modules/on-finished": {
731 | "version": "2.4.1",
732 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
733 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
734 | "dependencies": {
735 | "ee-first": "1.1.1"
736 | },
737 | "engines": {
738 | "node": ">= 0.8"
739 | }
740 | },
741 | "node_modules/on-headers": {
742 | "version": "1.0.2",
743 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
744 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
745 | "engines": {
746 | "node": ">= 0.8"
747 | }
748 | },
749 | "node_modules/packet-reader": {
750 | "version": "1.0.0",
751 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
752 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
753 | },
754 | "node_modules/parseurl": {
755 | "version": "1.3.3",
756 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
757 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
758 | "engines": {
759 | "node": ">= 0.8"
760 | }
761 | },
762 | "node_modules/path-to-regexp": {
763 | "version": "0.1.7",
764 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
765 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
766 | },
767 | "node_modules/pg": {
768 | "version": "8.9.0",
769 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.9.0.tgz",
770 | "integrity": "sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg==",
771 | "dependencies": {
772 | "buffer-writer": "2.0.0",
773 | "packet-reader": "1.0.0",
774 | "pg-connection-string": "^2.5.0",
775 | "pg-pool": "^3.5.2",
776 | "pg-protocol": "^1.6.0",
777 | "pg-types": "^2.1.0",
778 | "pgpass": "1.x"
779 | },
780 | "engines": {
781 | "node": ">= 8.0.0"
782 | },
783 | "peerDependencies": {
784 | "pg-native": ">=3.0.1"
785 | },
786 | "peerDependenciesMeta": {
787 | "pg-native": {
788 | "optional": true
789 | }
790 | }
791 | },
792 | "node_modules/pg-connection-string": {
793 | "version": "2.5.0",
794 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
795 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
796 | },
797 | "node_modules/pg-int8": {
798 | "version": "1.0.1",
799 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
800 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
801 | "engines": {
802 | "node": ">=4.0.0"
803 | }
804 | },
805 | "node_modules/pg-pool": {
806 | "version": "3.5.2",
807 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
808 | "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
809 | "peerDependencies": {
810 | "pg": ">=8.0"
811 | }
812 | },
813 | "node_modules/pg-protocol": {
814 | "version": "1.6.0",
815 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
816 | "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
817 | },
818 | "node_modules/pg-types": {
819 | "version": "2.2.0",
820 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
821 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
822 | "dependencies": {
823 | "pg-int8": "1.0.1",
824 | "postgres-array": "~2.0.0",
825 | "postgres-bytea": "~1.0.0",
826 | "postgres-date": "~1.0.4",
827 | "postgres-interval": "^1.1.0"
828 | },
829 | "engines": {
830 | "node": ">=4"
831 | }
832 | },
833 | "node_modules/pgpass": {
834 | "version": "1.0.5",
835 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
836 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
837 | "dependencies": {
838 | "split2": "^4.1.0"
839 | }
840 | },
841 | "node_modules/postgres-array": {
842 | "version": "2.0.0",
843 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
844 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
845 | "engines": {
846 | "node": ">=4"
847 | }
848 | },
849 | "node_modules/postgres-bytea": {
850 | "version": "1.0.0",
851 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
852 | "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
853 | "engines": {
854 | "node": ">=0.10.0"
855 | }
856 | },
857 | "node_modules/postgres-date": {
858 | "version": "1.0.7",
859 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
860 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
861 | "engines": {
862 | "node": ">=0.10.0"
863 | }
864 | },
865 | "node_modules/postgres-interval": {
866 | "version": "1.2.0",
867 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
868 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
869 | "dependencies": {
870 | "xtend": "^4.0.0"
871 | },
872 | "engines": {
873 | "node": ">=0.10.0"
874 | }
875 | },
876 | "node_modules/proxy-addr": {
877 | "version": "2.0.7",
878 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
879 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
880 | "dependencies": {
881 | "forwarded": "0.2.0",
882 | "ipaddr.js": "1.9.1"
883 | },
884 | "engines": {
885 | "node": ">= 0.10"
886 | }
887 | },
888 | "node_modules/qs": {
889 | "version": "6.11.0",
890 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
891 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
892 | "dependencies": {
893 | "side-channel": "^1.0.4"
894 | },
895 | "engines": {
896 | "node": ">=0.6"
897 | },
898 | "funding": {
899 | "url": "https://github.com/sponsors/ljharb"
900 | }
901 | },
902 | "node_modules/random-bytes": {
903 | "version": "1.0.0",
904 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
905 | "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
906 | "engines": {
907 | "node": ">= 0.8"
908 | }
909 | },
910 | "node_modules/range-parser": {
911 | "version": "1.2.1",
912 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
913 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
914 | "engines": {
915 | "node": ">= 0.6"
916 | }
917 | },
918 | "node_modules/raw-body": {
919 | "version": "2.5.1",
920 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
921 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
922 | "dependencies": {
923 | "bytes": "3.1.2",
924 | "http-errors": "2.0.0",
925 | "iconv-lite": "0.4.24",
926 | "unpipe": "1.0.0"
927 | },
928 | "engines": {
929 | "node": ">= 0.8"
930 | }
931 | },
932 | "node_modules/safe-buffer": {
933 | "version": "5.2.1",
934 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
935 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
936 | "funding": [
937 | {
938 | "type": "github",
939 | "url": "https://github.com/sponsors/feross"
940 | },
941 | {
942 | "type": "patreon",
943 | "url": "https://www.patreon.com/feross"
944 | },
945 | {
946 | "type": "consulting",
947 | "url": "https://feross.org/support"
948 | }
949 | ]
950 | },
951 | "node_modules/safer-buffer": {
952 | "version": "2.1.2",
953 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
954 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
955 | },
956 | "node_modules/send": {
957 | "version": "0.18.0",
958 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
959 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
960 | "dependencies": {
961 | "debug": "2.6.9",
962 | "depd": "2.0.0",
963 | "destroy": "1.2.0",
964 | "encodeurl": "~1.0.2",
965 | "escape-html": "~1.0.3",
966 | "etag": "~1.8.1",
967 | "fresh": "0.5.2",
968 | "http-errors": "2.0.0",
969 | "mime": "1.6.0",
970 | "ms": "2.1.3",
971 | "on-finished": "2.4.1",
972 | "range-parser": "~1.2.1",
973 | "statuses": "2.0.1"
974 | },
975 | "engines": {
976 | "node": ">= 0.8.0"
977 | }
978 | },
979 | "node_modules/send/node_modules/ms": {
980 | "version": "2.1.3",
981 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
982 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
983 | },
984 | "node_modules/serve-static": {
985 | "version": "1.15.0",
986 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
987 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
988 | "dependencies": {
989 | "encodeurl": "~1.0.2",
990 | "escape-html": "~1.0.3",
991 | "parseurl": "~1.3.3",
992 | "send": "0.18.0"
993 | },
994 | "engines": {
995 | "node": ">= 0.8.0"
996 | }
997 | },
998 | "node_modules/setprototypeof": {
999 | "version": "1.2.0",
1000 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1001 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1002 | },
1003 | "node_modules/side-channel": {
1004 | "version": "1.0.4",
1005 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
1006 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
1007 | "dependencies": {
1008 | "call-bind": "^1.0.0",
1009 | "get-intrinsic": "^1.0.2",
1010 | "object-inspect": "^1.9.0"
1011 | },
1012 | "funding": {
1013 | "url": "https://github.com/sponsors/ljharb"
1014 | }
1015 | },
1016 | "node_modules/split2": {
1017 | "version": "4.1.0",
1018 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
1019 | "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==",
1020 | "engines": {
1021 | "node": ">= 10.x"
1022 | }
1023 | },
1024 | "node_modules/statuses": {
1025 | "version": "2.0.1",
1026 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1027 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1028 | "engines": {
1029 | "node": ">= 0.8"
1030 | }
1031 | },
1032 | "node_modules/toidentifier": {
1033 | "version": "1.0.1",
1034 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1035 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1036 | "engines": {
1037 | "node": ">=0.6"
1038 | }
1039 | },
1040 | "node_modules/tr46": {
1041 | "version": "0.0.3",
1042 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
1043 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
1044 | },
1045 | "node_modules/type-is": {
1046 | "version": "1.6.18",
1047 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1048 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1049 | "dependencies": {
1050 | "media-typer": "0.3.0",
1051 | "mime-types": "~2.1.24"
1052 | },
1053 | "engines": {
1054 | "node": ">= 0.6"
1055 | }
1056 | },
1057 | "node_modules/uid-safe": {
1058 | "version": "2.1.5",
1059 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
1060 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
1061 | "dependencies": {
1062 | "random-bytes": "~1.0.0"
1063 | },
1064 | "engines": {
1065 | "node": ">= 0.8"
1066 | }
1067 | },
1068 | "node_modules/unpipe": {
1069 | "version": "1.0.0",
1070 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1071 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1072 | "engines": {
1073 | "node": ">= 0.8"
1074 | }
1075 | },
1076 | "node_modules/utils-merge": {
1077 | "version": "1.0.1",
1078 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1079 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1080 | "engines": {
1081 | "node": ">= 0.4.0"
1082 | }
1083 | },
1084 | "node_modules/vary": {
1085 | "version": "1.1.2",
1086 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1087 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1088 | "engines": {
1089 | "node": ">= 0.8"
1090 | }
1091 | },
1092 | "node_modules/webidl-conversions": {
1093 | "version": "3.0.1",
1094 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1095 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
1096 | },
1097 | "node_modules/whatwg-url": {
1098 | "version": "5.0.0",
1099 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1100 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1101 | "dependencies": {
1102 | "tr46": "~0.0.3",
1103 | "webidl-conversions": "^3.0.0"
1104 | }
1105 | },
1106 | "node_modules/xtend": {
1107 | "version": "4.0.2",
1108 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
1109 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
1110 | "engines": {
1111 | "node": ">=0.4"
1112 | }
1113 | },
1114 | "node_modules/yallist": {
1115 | "version": "4.0.0",
1116 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
1117 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
1118 | }
1119 | }
1120 | }
1121 |
--------------------------------------------------------------------------------