├── .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 | 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 | ![pg_schema](/docs/schema.png) 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 | ![event_data_format](/docs/eventDataFormat.png) 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 | 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 |
    112 | 115 | setName(e.target.value)} value={name} required /> 116 | 119 | setDescription(e.target.value)} value={description} required /> 120 | 123 | setLocName(e.target.value)} value={locName} required /> 124 | 127 | { 131 | console.log('PLACE in autocomplete IS: ', place); 132 | setAddress(place.formatted_address); 133 | }} 134 | value={address} 135 | /> 136 | 139 | setDate(e.target.value)} value={date} required /> 140 | 141 | 142 | 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 |
    107 | 110 | setName(e.target.value)} 115 | value={name} 116 | required 117 | /> 118 | 121 | setDescription(e.target.value)} 126 | value={description} 127 | required 128 | /> 129 | 132 | setLocName(e.target.value)} 137 | value={locName} 138 | required 139 | /> 140 | 143 | { 148 | console.log("PLACE in autocomplete IS: ", place); 149 | setAddress(place.formatted_address); 150 | }} 151 | /> 152 | 155 | setDate(e.target.value)} 160 | value={date} 161 | required 162 | /> 163 | {/* */} 166 | {/* 167 | setEndDate(e.target.value)} 172 | value={date} 173 | /> */} 174 | 175 | 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 | 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 |
      212 |
    • 213 | Organizer: {eventData.organizer.username} 214 |
    • 215 |
    • Location: {eventData.address}
    • 216 |
    • 217 | Date: {new Date(eventData.date).toLocaleString()} 218 |
    • 219 | {/*
    • RSVP: {eventData.organizer.email}
    • */} 220 |
    • 221 | {" "} 222 | RSVP: 223 |
      224 | 227 | 230 | 233 | 237 |
      238 |
    • 239 |
    240 | {/* If the user is the creator of the event, display the edit and delete buttons */} 241 | {eventData.organizer.email === user.email && ( 242 |
    243 | 251 | 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 | --------------------------------------------------------------------------------