├── backend ├── log │ └── .gitkeep ├── .gitignore ├── src │ ├── types │ │ ├── user.ts │ │ └── session.ts │ ├── services │ │ └── platformAPIClient.ts │ ├── handlers │ │ ├── notifications.ts │ │ ├── users.ts │ │ └── payments.ts │ ├── environments.ts │ └── index.ts ├── docker │ └── processes.config.js ├── .env.example ├── Dockerfile ├── package.json ├── README.md ├── tsconfig.json └── yarn.lock ├── .gitignore ├── frontend ├── src │ ├── react-app-env.d.ts │ ├── typedefs.d.ts │ ├── defaults.css │ ├── index.tsx │ └── Shop │ │ ├── components │ │ ├── SignIn.tsx │ │ ├── ProductCard.tsx │ │ └── Header.tsx │ │ └── index.tsx ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── .env.development ├── .gitignore ├── tsconfig.json ├── docker │ ├── entrypoint.sh │ └── nginx.conf ├── README.md ├── package.json └── Dockerfile ├── doc ├── img │ ├── api_key.png │ ├── app_checklist.png │ ├── register_app.PNG │ ├── sandbox_url.png │ ├── domain_verified.png │ ├── sandbox_firefox.png │ ├── developer_portal.png │ └── domain_verification.png ├── development.md └── deployment.md ├── reverse-proxy ├── Dockerfile └── docker │ ├── nginx.conf.template │ ├── entrypoint.sh │ └── nginx-ssl.conf.template ├── README.md ├── docker-compose.yml ├── .env.example ├── LICENSE.md └── FLOWS.md /backend/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | docker-data 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/typedefs.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | Pi: any; 3 | } 4 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | /log/* 4 | !/log/.gitkeep 5 | .env -------------------------------------------------------------------------------- /doc/img/api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/api_key.png -------------------------------------------------------------------------------- /doc/img/app_checklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/app_checklist.png -------------------------------------------------------------------------------- /doc/img/register_app.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/register_app.PNG -------------------------------------------------------------------------------- /doc/img/sandbox_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/sandbox_url.png -------------------------------------------------------------------------------- /doc/img/domain_verified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/domain_verified.png -------------------------------------------------------------------------------- /doc/img/sandbox_firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/sandbox_firefox.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /doc/img/developer_portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/developer_portal.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /doc/img/domain_verification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pi-apps/demo/HEAD/doc/img/domain_verification.png -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | PORT=3314 2 | REACT_APP_BACKEND_URL=http://localhost:8000 3 | REACT_APP_SANDBOX_SDK=true 4 | -------------------------------------------------------------------------------- /backend/src/types/user.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb"; 2 | 3 | export interface UserData { 4 | _id: ObjectId, 5 | username: string, 6 | uid: string, 7 | roles: Array, 8 | accessToken: string 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/defaults.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | h1, h2, h3 { 6 | margin-top: 0; 7 | margin-bottom: 16px; 8 | } 9 | 10 | h4, h5, h6 { 11 | margin-top: 0; 12 | margin-bottom: 8px; 13 | } 14 | 15 | *, *::before, *::after { 16 | box-sizing: border-box; 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/types/session.ts: -------------------------------------------------------------------------------- 1 | import { UserData } from "./user"; 2 | 3 | // https://stackoverflow.com/questions/65108033/property-user-does-not-exist-on-type-session-partialsessiondata 4 | declare module 'express-session' { 5 | export interface SessionData { 6 | currentUser: UserData | null, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'normalize.css'; 4 | import './defaults.css'; 5 | import Shop from './Shop'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); -------------------------------------------------------------------------------- /backend/src/services/platformAPIClient.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import env from "../environments"; 3 | 4 | const platformAPIClient = axios.create({ 5 | baseURL: env.platform_api_url, 6 | timeout: 20000, 7 | headers: { 'Authorization': `Key ${env.pi_api_key}` } 8 | }); 9 | 10 | export default platformAPIClient; -------------------------------------------------------------------------------- /backend/docker/processes.config.js: -------------------------------------------------------------------------------- 1 | // PM2 config file to run the app in production mode. 2 | // Make sure the name of this file ends with config.js 3 | 4 | module.exports = { 5 | apps: [{ 6 | name: "demoapp-backend", 7 | script: "/usr/src/app/build/index.js", 8 | exec_mode: "cluster", 9 | instances: 4, 10 | }] 11 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Generate a random string, or roll your face on the keyboard to fill this value: 2 | SESSION_SECRET= 3 | 4 | # Get this on the Pi Developer Portal (develop.pi in the Pi Browser) 5 | PI_API_KEY= 6 | 7 | # Platform API 8 | PLATFORM_API_URL=https://api.minepi.com 9 | 10 | # MongoDB database connection details: 11 | MONGO_HOST=localhost:27017 12 | MONGODB_DATABASE_NAME=demoapp-development 13 | MONGODB_USERNAME=demoapp 14 | MONGODB_PASSWORD=dev_password 15 | 16 | # Frontend app URL 17 | FRONTEND_URL=http://localhost:3314 18 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "Running demo-app-frontend with the following configuration:" 6 | echo "Backend URL: $BACKEND_URL" 7 | 8 | if [ -z "$BACKEND_URL" ] 9 | then 10 | echo "ERROR! INVALID CONFIGURATION: BACKEND_URL must be defined in the environment. Exiting." 11 | exit 1 12 | fi 13 | 14 | # Use semicolons instead of slashes as delimiters for sed, to prevent any conflicts with the slashes in the URL params: 15 | # https://unix.stackexchange.com/questions/379572/escaping-both-forward-slash-and-back-slash-with-sed 16 | sed -i "s;%REACT_APP_BACKEND_URL%;${BACKEND_URL};" /var/www/webapp/index.html 17 | sed -i "s;%REACT_APP_SANDBOX_SDK%;${SANDBOX_SDK};" /var/www/webapp/index.html 18 | 19 | # Call the command that the base image was initally supposed to run 20 | # See: XXX 21 | nginx -g "daemon off;" 22 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Pi Demo App Frontend 2 | 3 | Pi Demo App is an example React app created with [Create React App](https://create-react-app.dev/). 4 | 5 | ### 1. Install dependencies: 6 | 7 | You will need a working NodeJS installation, and `yarn`. **The demo app frontend isn't meant to support npm**. 8 | In most cases, `yarn` will come along with your NodeJS installation. 9 | 10 | Install dependencies by running `yarn install`. 11 | 12 | ### 2. Start the app: 13 | 14 | Run the following command to start the app's development server: `yarn start` 15 | This will open a browser window on http://localhost:3314 which you can close, since the demo app mostly needs 16 | to run inside of the Pi Sandbox environment in order to work correctly in development. 17 | 18 | --- 19 | You've completed the frontend setup, return to [`doc/development.md`](../doc/development.md) to finish setting up the demo app 20 | -------------------------------------------------------------------------------- /frontend/src/Shop/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | 3 | interface Props { 4 | onSignIn: () => void, 5 | onModalClose: () => void, 6 | } 7 | 8 | const modalStyle: CSSProperties = { 9 | background: 'white', 10 | position: 'absolute', 11 | left: '15vw', 12 | top: '40%', 13 | width: '70vw', 14 | height: '25vh', 15 | border: '1px solid black', 16 | textAlign: 'center', 17 | display: 'flex', 18 | flexDirection: 'column', 19 | justifyContent: 'center' 20 | } 21 | 22 | export default function SignIn(props: Props) { 23 | return ( 24 |
25 |

You need to sign in first.

26 |
27 | 28 | 29 |
30 |
31 | ) 32 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package.json ./ 10 | COPY yarn.lock ./ 11 | 12 | # When building for production, use the lockfile as-is: 13 | RUN yarn install --frozen-lockfile 14 | # RUN npm ci --only=production 15 | 16 | # Bundle app source 17 | # TODO make this only copy the necessary files instead of copying the whole directory 18 | COPY ./src ./src 19 | COPY ./tsconfig.json ./tsconfig.json 20 | 21 | RUN yarn build 22 | 23 | RUN yarn global add pm2 24 | COPY ./docker/processes.config.js ./processes.config.js 25 | 26 | RUN mkdir -p log && touch log/.keep 27 | 28 | EXPOSE 8080 29 | CMD [ "pm2-runtime", "./processes.config.js" ] 30 | 31 | # CMD [ "pm2-runtime", "build/index.js" ] 32 | # CMD [ "node", "build/index.js" ] 33 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-platform-demo-backend", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "description": "A demo app to showcase usage of the Pi App Platform.", 6 | "author": { 7 | "name": "Pi Core Team" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "start": "NODE_ENV=development ts-node-dev ./src/index.ts" 12 | }, 13 | "dependencies": { 14 | "@types/connect-mongo": "^3.1.3", 15 | "@types/cookie-parser": "^1.4.2", 16 | "@types/express-session": "^1.17.5", 17 | "axios": "^0.21.1", 18 | "connect-mongo": "^4.4.1", 19 | "cookie-parser": "^1.4.5", 20 | "cors": "^2.8.5", 21 | "dotenv": "^10.0.0", 22 | "express": "^4.17.1", 23 | "express-session": "^1.17.2", 24 | "mongodb": "^4.0.0", 25 | "morgan": "^1.10.0" 26 | }, 27 | "devDependencies": { 28 | "@types/cors": "^2.8.11", 29 | "@types/express": "^4.17.12", 30 | "@types/morgan": "^1.9.2", 31 | "@types/node": "^18.7.23", 32 | "ts-node-dev": "^2.0.0", 33 | "typescript": "^4.7.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /reverse-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | ## REVERSE PROXY IMAGE 3 | ## 4 | 5 | FROM nginx:1.23.1 6 | 7 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-nginx 8 | 9 | COPY ./docker/nginx.conf.template /nginx.conf.template 10 | COPY ./docker/nginx-ssl.conf.template /nginx-ssl.conf.template 11 | COPY ./docker/entrypoint.sh /var/entrypoint.sh 12 | 13 | RUN chmod +x /var/entrypoint.sh 14 | 15 | # Default nginx configuration has only one worker process running. "Auto" is a better setting for scalability. 16 | # Commenting out any existing setting, and adding the desired one is more robust against new docker image versions. 17 | RUN sed -i "s/worker_processes/#worker_processes/" /etc/nginx/nginx.conf && \ 18 | echo "worker_processes auto;" >> /etc/nginx/nginx.conf && \ 19 | echo "worker_rlimit_nofile 16384;" >> /etc/nginx/nginx.conf 20 | 21 | # Override the default command of the base image: 22 | # See: https://github.com/nginxinc/docker-nginx/blob/1.15.7/mainline/stretch/Dockerfile#L99 23 | CMD ["/var/entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /frontend/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | root /var/www/webapp; 4 | 5 | #charset koi8-r; 6 | #access_log /var/log/nginx/log/host.access.log main; 7 | 8 | gzip on; 9 | gzip_comp_level 2; 10 | gzip_min_length 1000; 11 | gzip_proxied expired no-cache no-store private auth; 12 | gzip_types text/plain application/x-javascript text/xml text/css application/xml; 13 | 14 | location / { 15 | # First attempt to serve the requested file, then directory, then fall back to index.html, 16 | # which loads the react app, as compiled by webpack using `react-scripts build`. 17 | try_files $uri $uri/ /index.html; 18 | 19 | # Don't cache the HTML files so that the hashed JS filenames work: 20 | location ~ ^.+\.(html?)$ { 21 | add_header Cache-Control no-cache; 22 | } 23 | } 24 | 25 | 26 | # redirect server error pages to the static page /50x.html 27 | # 28 | # error_page 500 502 503 504 /50x.html; 29 | # location = /50x.html { 30 | # root /usr/share/nginx/html; 31 | # } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/handlers/notifications.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import platformAPIClient from "../services/platformAPIClient"; 3 | 4 | export default function mountNotificationEndpoints(router: Router) { 5 | router.post("/send", async (req, res) => { 6 | try { 7 | const { notifications } = req.body || {}; 8 | 9 | if (!Array.isArray(notifications) || notifications.length === 0) { 10 | return res 11 | .status(400) 12 | .json({ message: "notifications array is required" }); 13 | } 14 | 15 | // Forward to Platform API: POST /v2/in_app_notifications/notify 16 | const response = await platformAPIClient.post( 17 | "/v2/in_app_notifications/notify", 18 | { notifications } 19 | ); 20 | 21 | return res.status(200).json(response.data); 22 | } catch (err: any) { 23 | const status = err?.response?.status || 500; 24 | const data = err?.response?.data || { 25 | message: "Failed to send notifications", 26 | }; 27 | return res.status(status).json(data); 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/Shop/components/ProductCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | name: string, 5 | description: string, 6 | price: number, 7 | pictureCaption: string, 8 | pictureURL: string, 9 | onClickBuy: () => void, 10 | } 11 | 12 | export default function ProductCard(props: Props) { 13 | return ( 14 |
15 |
16 |
17 | {props.name} 18 |
19 | 20 |
21 |

{props.name}

22 |

{props.description}

23 |
24 |
25 | 26 |
27 | {props.price} Test-π
28 | 29 |
30 | 31 | {props.pictureCaption} 32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pi Demo App 2 | 3 | Pi Demo App is an example of how you can implement the various required flows in your app's code. 4 | It aims to show you how to use Pi Platform API on the backend side and Pi SDK on the frontend side of your app. 5 | 6 | 7 | It is composed of two major parts: 8 | 9 | * **backend**: a backend app (a very simple JSON API built using Node and ExpressJS) 10 | * **frontend**: a single-page frontend app (built using React and create-react-app) 11 | 12 | 13 | ## Initial Development 14 | 15 | Read [`doc/development.md`](./doc/development.md) to get started and learn how to run this app in development. 16 | 17 | > **WARNING** 18 | > 19 | > The demo app uses express session cookies which, in the Sandbox environment, are not correctly saved on the client on some browsers. 20 | > To properly test all of the features of the Demo App, we recommend you to open the sandbox app using Mozilla Firefox. 21 | 22 | 23 | ## Deployment 24 | 25 | Read [`doc/deployment.md`](./doc/deployment.md) to learn how to deploy this app on a server using Docker and docker-compose. 26 | 27 | 28 | ## Flows 29 | 30 | To dive into the implementation of the flows that support the demo app features, please refer to 31 | [Pi Demo App Flows](./FLOWS.md). 32 | -------------------------------------------------------------------------------- /frontend/src/Shop/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from "react"; 2 | import { User } from "../"; 3 | 4 | interface Props { 5 | onSignIn: () => void; 6 | onSignOut: () => void; 7 | onSendTestNotification: () => void; 8 | user: User | null; 9 | } 10 | 11 | const headerStyle: CSSProperties = { 12 | padding: 8, 13 | backgroundColor: "gray", 14 | color: "white", 15 | width: "100%", 16 | display: "flex", 17 | alignItems: "center", 18 | justifyContent: "space-between", 19 | }; 20 | 21 | export default function Header(props: Props) { 22 | return ( 23 |
24 |
Pi Bakery
25 | 26 |
27 | {props.user === null ? ( 28 | 29 | ) : ( 30 |
31 | @{props.user.username}{" "} 32 | 35 | {props.user.roles.includes("core_team") && ( 36 | 39 | )} 40 |
41 | )} 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-platform-demo-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^17.0.0", 12 | "@types/react-dom": "^17.0.0", 13 | "axios": "^0.27.2", 14 | "normalize.css": "^8.0.1", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-scripts": "4.0.3", 18 | "typescript": "^4.1.2", 19 | "web-vitals": "^1.0.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "startNode17": "react-scripts --openssl-legacy-provider start", 24 | "build": "GENERATE_SOURCEMAP=false react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/environments.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | console.log("NODE_ENV: " + process.env.NODE_ENV); 4 | 5 | const result = dotenv.config() 6 | 7 | if (result.error) { 8 | if (process.env.NODE_ENV === "development") { 9 | console.error(".env file not found. This is an error condition in development. Additional error is logged below"); 10 | throw result.error; 11 | } 12 | 13 | // In production, environment variables are injected into the container environment. We should not even have 14 | // a .env file inside the running container. 15 | } 16 | 17 | interface Environment { 18 | session_secret: string, 19 | pi_api_key: string, 20 | platform_api_url: string, 21 | mongo_host: string, 22 | mongo_db_name: string, 23 | mongo_user: string, 24 | mongo_password: string, 25 | frontend_url: string, 26 | } 27 | 28 | const env: Environment = { 29 | session_secret: process.env.SESSION_SECRET || "This is my session secret", 30 | pi_api_key: process.env.PI_API_KEY || '', 31 | platform_api_url: process.env.PLATFORM_API_URL || '', 32 | mongo_host: process.env.MONGO_HOST || 'localhost:27017', 33 | mongo_db_name: process.env.MONGODB_DATABASE_NAME || 'demo-app', 34 | mongo_user: process.env.MONGODB_USERNAME || '', 35 | mongo_password: process.env.MONGODB_PASSWORD || '', 36 | frontend_url: process.env.FRONTEND_URL || 'http://localhost:3314', 37 | }; 38 | 39 | export default env; 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | reverse-proxy: 4 | build: ./reverse-proxy 5 | environment: 6 | HTTPS: ${HTTPS} 7 | FRONTEND_DOMAIN_NAME: ${FRONTEND_DOMAIN_NAME} 8 | BACKEND_DOMAIN_NAME: ${BACKEND_DOMAIN_NAME} 9 | DOMAIN_VALIDATION_KEY: ${DOMAIN_VALIDATION_KEY} 10 | ports: 11 | - "80:80" 12 | - "443:443" 13 | healthcheck: 14 | test: ["CMD", "curl", "-f", "http://localhost:80"] 15 | interval: 30s 16 | timeout: 10s 17 | retries: 5 18 | volumes: 19 | - ${DATA_DIRECTORY}/reverse-proxy/etc-letsencrypt:/etc/letsencrypt/ 20 | 21 | frontend: 22 | build: ./frontend 23 | environment: 24 | BACKEND_URL: ${BACKEND_URL} 25 | 26 | backend: 27 | build: ./backend 28 | environment: 29 | NODE_ENV: ${ENVIRONMENT} 30 | SESSION_SECRET: ${SESSION_SECRET} 31 | PI_API_KEY: ${PI_API_KEY} 32 | PLATFORM_API_URL: ${PLATFORM_API_URL} 33 | MONGO_HOST: mongo 34 | MONGODB_DATABASE_NAME: ${MONGODB_DATABASE_NAME} 35 | MONGODB_USERNAME: ${MONGODB_USERNAME} 36 | MONGODB_PASSWORD: ${MONGODB_PASSWORD} 37 | FRONTEND_URL: ${FRONTEND_URL} 38 | 39 | mongo: 40 | image: mongo:5.0 41 | environment: 42 | MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME} 43 | MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} 44 | ports: 45 | - 27027:27017 46 | volumes: 47 | - ${DATA_DIRECTORY}/mongo/data:/data/db 48 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | ## BUILDER CONTAINER 3 | ## 4 | 5 | FROM node:12.20.0 as builder 6 | 7 | RUN mkdir /app 8 | 9 | COPY ./package.json /app/package.json 10 | COPY ./yarn.lock /app/yarn.lock 11 | 12 | WORKDIR /app 13 | 14 | RUN yarn install 15 | 16 | # Copy the resources needed to build the app 17 | # We could copy ./ but it weigh more and bust cache more often 18 | COPY ./src /app/src 19 | COPY ./public /app/public 20 | COPY ./tsconfig.json /app/tsconfig.json 21 | 22 | RUN NODE_PATH=./src yarn build 23 | 24 | # We have set GENERATE_SOURCEMAP=false in our build script but we're doing this to add an extra layer 25 | # of safety - we want to keep JS source maps from getting deployed on the production servers: 26 | RUN rm -rf ./build/static/js/*.map 27 | 28 | ## 29 | ## RUNNER CONTAINER 30 | ## 31 | 32 | FROM nginx:1.15.7 33 | 34 | COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf 35 | COPY ./docker/entrypoint.sh /var/entrypoint.sh 36 | RUN chmod +x /var/entrypoint.sh 37 | 38 | RUN mkdir -p /var/www/webapp 39 | 40 | COPY --from=builder /app/build /var/www/webapp 41 | 42 | # Default nginx configuration has only one worker process running. "Auto" is a better setting. 43 | # Commenting out any existing setting, and adding the desired one is more robust against new docker image versions. 44 | RUN sed -i "s/worker_processes/#worker_processes/" /etc/nginx/nginx.conf && \ 45 | echo "worker_processes auto;" >> /etc/nginx/nginx.conf && \ 46 | echo "worker_rlimit_nofile 16384;" >> /etc/nginx/nginx.conf 47 | 48 | # Override the default command of the base image: 49 | # See: https://github.com/nginxinc/docker-nginx/blob/1.15.7/mainline/stretch/Dockerfile#L99 50 | CMD ["/var/entrypoint.sh"] 51 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # WARNING: Commenting after a variable's value DOES NOT WORK in .env files. 2 | # In other words, don't to this: `FOO=value # this is a comment`. 3 | # This would give the value "value # this is a comment" to the FOO variable. 4 | # You need to use single-line comments instead, e.g: 5 | # ``` 6 | # # this is a comment: 7 | # FOO=value 8 | # ``` 9 | 10 | # 11 | # 12 | # 13 | 14 | # Frontend app URL and bare domain name: 15 | FRONTEND_URL=https://frontend.example.com 16 | FRONTEND_DOMAIN_NAME=frontend.example.com 17 | 18 | # Backend app URL and bare domain name: 19 | BACKEND_URL=https://backend.example.com 20 | BACKEND_DOMAIN_NAME=backend.example.com 21 | 22 | 23 | # Obtain the following 2 values on the Pi Developer Portal (open develop.pi in the Pi Browser). 24 | 25 | # Domain validation key: 26 | DOMAIN_VALIDATION_KEY=todo_developer_portal 27 | # Pi Platform API Key: 28 | PI_API_KEY=todo_developer_portal 29 | 30 | # Generate a random string, or roll your face on the keyboard to fill this value: 31 | SESSION_SECRET=abcd1324_TODO 32 | 33 | # MongoDB database connection details: 34 | MONGODB_DATABASE_NAME=demoapp 35 | MONGODB_USERNAME=demoapp 36 | MONGODB_PASSWORD=abcd1234 37 | 38 | 39 | # This will be prepended to all container names. 40 | # Changing this will make docker-compose lose track of all your containers. 41 | # Run `docker-compose down` before changing it. 42 | COMPOSE_PROJECT_NAME=pi-demo-app 43 | 44 | # Set this to either "development" or "production" (XXX "staging"?): 45 | ENVIRONMENT=production 46 | 47 | # This directory will be used to store all persistent data needed by Docker (using volumes): 48 | DATA_DIRECTORY=./docker-data 49 | 50 | # URL of the Pi Platform API - you should not need to change this. 51 | PLATFORM_API_URL=https://api.minepi.com 52 | -------------------------------------------------------------------------------- /backend/src/handlers/users.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import platformAPIClient from "../services/platformAPIClient"; 4 | 5 | 6 | export default function mountUserEndpoints(router: Router) { 7 | // handle the user auth accordingly 8 | router.post('/signin', async (req, res) => { 9 | const auth = req.body.authResult; 10 | const userCollection = req.app.locals.userCollection; 11 | 12 | try { 13 | // Verify the user's access token with the /me endpoint: 14 | const me = await platformAPIClient.get(`/v2/me`, { headers: { 'Authorization': `Bearer ${auth.accessToken}` } }); 15 | console.log(me); 16 | } catch (err) { 17 | console.log(err); 18 | return res.status(401).json({error: "Invalid access token"}) 19 | } 20 | 21 | let currentUser = await userCollection.findOne({ uid: auth.user.uid }); 22 | 23 | if (currentUser) { 24 | await userCollection.updateOne({ 25 | _id: currentUser._id 26 | }, { 27 | $set: { 28 | accessToken: auth.accessToken, 29 | } 30 | }); 31 | } else { 32 | const insertResult = await userCollection.insertOne({ 33 | username: auth.user.username, 34 | uid: auth.user.uid, 35 | roles: auth.user.roles, 36 | accessToken: auth.accessToken 37 | }); 38 | 39 | currentUser = await userCollection.findOne(insertResult.insertedId); 40 | } 41 | 42 | req.session.currentUser = currentUser; 43 | 44 | return res.status(200).json({ message: "User signed in" }); 45 | }); 46 | 47 | // handle the user auth accordingly 48 | router.get('/signout', async (req, res) => { 49 | req.session.currentUser = null; 50 | return res.status(200).json({ message: "User signed out" }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /reverse-proxy/docker/nginx.conf.template: -------------------------------------------------------------------------------- 1 | # 2 | # Frontend config: 3 | # 4 | server { 5 | # Use Docker's built-in DNS resolver to enable resolving container hostnames used in the proxy_pass 6 | # directives below. 7 | # https://stackoverflow.com/questions/35744650/docker-network-nginx-resolver 8 | resolver 127.0.0.11 ipv6=off; 9 | 10 | # Enable nginx to start even when upstream hosts are unreachable (i.e containers not started yet) 11 | # https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/ 12 | set $frontend_upstream http://frontend; 13 | 14 | server_name ${FRONTEND_DOMAIN_NAME}; 15 | listen 80; 16 | 17 | location /.well-known/acme-challenge/ { 18 | root /var/www/certbot; 19 | } 20 | 21 | location / { 22 | proxy_pass $frontend_upstream; 23 | } 24 | } 25 | 26 | # 27 | # Backend config: 28 | # 29 | server { 30 | # Use Docker's built-in DNS resolver to enable resolving container hostnames used in the proxy_pass 31 | # directives below. 32 | # https://stackoverflow.com/questions/35744650/docker-network-nginx-resolver 33 | resolver 127.0.0.11 ipv6=off; 34 | 35 | # Enable nginx to start even when upstream hosts are unreachable (i.e containers not started yet) 36 | # https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/ 37 | set $backend_upstream http://backend:8000; 38 | 39 | server_name ${BACKEND_DOMAIN_NAME}; 40 | listen 80; 41 | 42 | # Enable nginx to start even when upstream hosts are unreachable (i.e containers not started yet) 43 | # https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/ 44 | 45 | location /.well-known/acme-challenge/ { 46 | root /var/www/certbot; 47 | } 48 | 49 | location / { 50 | proxy_pass $backend_upstream; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /reverse-proxy/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo; echo 4 | echo "reverse-proxy: Starting up!" 5 | echo " - HTTPS: ${HTTPS}" 6 | echo " - FRONTEND_DOMAIN_NAME: ${FRONTEND_DOMAIN_NAME}" 7 | echo " - BACKEND_DOMAIN_NAME: ${BACKEND_DOMAIN_NAME}" 8 | 9 | set -e 10 | 11 | # Directory used by certbot to serve certificate requests challenges: 12 | mkdir -p /var/www/certbot 13 | 14 | if [ $HTTPS = "true" ]; then 15 | echo "Starting in SSL mode" 16 | 17 | rm /etc/nginx/conf.d/default.conf 18 | 19 | echo 20 | echo "Obtaining SSL certificate for frontend domain name: ${FRONTEND_DOMAIN_NAME}" 21 | certbot certonly --noninteractive --agree-tos --register-unsafely-without-email --nginx -d ${FRONTEND_DOMAIN_NAME} 22 | 23 | echo 24 | echo "Obtaining SSL certificate for backend domain name: ${BACKEND_DOMAIN_NAME}" 25 | certbot certonly --noninteractive --agree-tos --register-unsafely-without-email --nginx -d ${BACKEND_DOMAIN_NAME} 26 | 27 | # The above certbot command will start the nginx service in the background as a service. 28 | # However, we need the `nginx -g "daemon off;"` to be the main nginx process running on the container, and 29 | # we need it to be able to start listening on ports 80/443. If we don't stop the nginx process here, we'll 30 | # encounter the following error: `nginx: [emerg] bind() to 0.0.0.0:443 failed (98: Address already in use)`. 31 | service nginx stop 32 | 33 | envsubst '$FRONTEND_DOMAIN_NAME $BACKEND_DOMAIN_NAME $DOMAIN_VALIDATION_KEY' < /nginx-ssl.conf.template > /etc/nginx/conf.d/default.conf 34 | else 35 | echo "Starting in http mode" 36 | envsubst '$FRONTEND_DOMAIN_NAME $BACKEND_DOMAIN_NAME $DOMAIN_VALIDATION_KEY' < /nginx.conf.template > /etc/nginx/conf.d/default.conf 37 | fi 38 | 39 | # Call the command that the base image was initally supposed to run 40 | # See: XXX 41 | nginx -g "daemon off;" 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | PiOS License 2 | 3 | Copyright (C) 2022 Pi Core Team 4 | 5 | Permission is hereby granted by the application software developer (“Software Developer”), free 6 | of charge, to any person obtaining a copy of this application, software and associated 7 | documentation files (the “Software”), which was developed by the Software Developer for use on 8 | Pi Network, whereby the purpose of this license is to permit the development of derivative works 9 | based on the Software, including the right to use, copy, modify, merge, publish, distribute, 10 | sub-license, and/or sell copies of such derivative works and any Software components incorporated 11 | therein, and to permit persons to whom such derivative works are furnished to do so, in each case, 12 | solely to develop, use and market applications for the official Pi Network. For purposes of this 13 | license, Pi Network shall mean any application, software, or other present or future platform 14 | developed, owned or managed by Pi Community Company, and its parents, affiliates or subsidiaries, 15 | for which the Software was developed, or on which the Software continues to operate. However, 16 | you are prohibited from using any portion of the Software or any derivative works thereof in any 17 | manner (a) which infringes on any Pi Network intellectual property rights, (b) to hack any of Pi 18 | Network’s systems or processes or (c) to develop any product or service which is competitive with 19 | the Pi Network. 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or 22 | substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 25 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 26 | AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS, PUBLISHERS, OR COPYRIGHT HOLDERS OF THIS 27 | SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO BUSINESS INTERRUPTION, LOSS OF USE, DATA OR PROFITS) 29 | HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 30 | TORT (INCLUDING NEGLIGENCE) ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 31 | OR OTHER DEALINGS IN THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 32 | 33 | Pi, Pi Network and the Pi logo are trademarks of the Pi Community Company. 34 | -------------------------------------------------------------------------------- /reverse-proxy/docker/nginx-ssl.conf.template: -------------------------------------------------------------------------------- 1 | # 2 | # Frontend config: 3 | # 4 | server { 5 | # Use Docker's built-in DNS resolver to enable resolving container hostnames used in the proxy_pass 6 | # directives below. 7 | # https://stackoverflow.com/questions/35744650/docker-network-nginx-resolver 8 | resolver 127.0.0.11 ipv6=off; 9 | 10 | # Enable nginx to start even when upstream hosts are unreachable (i.e containers not started yet) 11 | # https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/ 12 | set $frontend_upstream http://frontend; 13 | 14 | server_name ${FRONTEND_DOMAIN_NAME}; 15 | listen 443 ssl; 16 | 17 | ssl_certificate /etc/letsencrypt/live/${FRONTEND_DOMAIN_NAME}/fullchain.pem; 18 | ssl_certificate_key /etc/letsencrypt/live/${FRONTEND_DOMAIN_NAME}/privkey.pem; 19 | include /etc/letsencrypt/options-ssl-nginx.conf; 20 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 21 | 22 | location /.well-known/acme-challenge/ { 23 | root /var/www/certbot; 24 | } 25 | 26 | location /validation-key.txt { 27 | return 200 '${DOMAIN_VALIDATION_KEY}'; 28 | } 29 | 30 | location / { 31 | proxy_pass $frontend_upstream; 32 | } 33 | } 34 | 35 | server { 36 | listen 80; 37 | server_name ${FRONTEND_DOMAIN_NAME}; 38 | 39 | if ($host = ${FRONTEND_DOMAIN_NAME}) { 40 | return 302 https://$host$request_uri; 41 | } 42 | 43 | return 404; 44 | } 45 | 46 | 47 | # 48 | # Backend config: 49 | # 50 | server { 51 | # Use Docker's built-in DNS resolver to enable resolving container hostnames used in the proxy_pass 52 | # directives below. 53 | # https://stackoverflow.com/questions/35744650/docker-network-nginx-resolver 54 | resolver 127.0.0.11 ipv6=off; 55 | 56 | # Enable nginx to start even when upstream hosts are unreachable (i.e containers not started yet) 57 | # https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/ 58 | set $backend_upstream http://backend:8000; 59 | 60 | server_name ${BACKEND_DOMAIN_NAME}; 61 | listen 443 ssl; 62 | 63 | ssl_certificate /etc/letsencrypt/live/${BACKEND_DOMAIN_NAME}/fullchain.pem; 64 | ssl_certificate_key /etc/letsencrypt/live/${BACKEND_DOMAIN_NAME}/privkey.pem; 65 | include /etc/letsencrypt/options-ssl-nginx.conf; 66 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 67 | 68 | location /.well-known/acme-challenge/ { 69 | root /var/www/certbot; 70 | } 71 | 72 | location / { 73 | proxy_pass $backend_upstream; 74 | } 75 | } 76 | 77 | server { 78 | listen 80; 79 | server_name ${BACKEND_DOMAIN_NAME}; 80 | 81 | if ($host = ${BACKEND_DOMAIN_NAME}) { 82 | return 302 https://$host$request_uri; 83 | } 84 | 85 | return 404; 86 | } 87 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 42 | 43 | 44 | 45 | 46 | 47 | 55 | 56 | React App 57 | 58 | 59 | 60 |
61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import cors from 'cors'; 4 | import express from 'express'; 5 | import cookieParser from 'cookie-parser'; 6 | import session from 'express-session'; 7 | import logger from 'morgan'; 8 | import MongoStore from 'connect-mongo'; 9 | import { MongoClient } from 'mongodb'; 10 | import env from './environments'; 11 | import mountPaymentsEndpoints from './handlers/payments'; 12 | import mountUserEndpoints from './handlers/users'; 13 | 14 | // We must import typedefs for ts-node-dev to pick them up when they change (even though tsc would supposedly 15 | // have no problem here) 16 | // https://stackoverflow.com/questions/65108033/property-user-does-not-exist-on-type-session-partialsessiondata#comment125163548_65381085 17 | import "./types/session"; 18 | import mountNotificationEndpoints from './handlers/notifications'; 19 | 20 | const dbName = env.mongo_db_name; 21 | const mongoUri = `mongodb://${env.mongo_host}/${dbName}`; 22 | const mongoClientOptions = { 23 | authSource: "admin", 24 | auth: { 25 | username: env.mongo_user, 26 | password: env.mongo_password, 27 | }, 28 | } 29 | 30 | 31 | // 32 | // I. Initialize and set up the express app and various middlewares and packages: 33 | // 34 | 35 | const app: express.Application = express(); 36 | 37 | // Log requests to the console in a compact format: 38 | app.use(logger('dev')); 39 | 40 | // Full log of all requests to /log/access.log: 41 | app.use(logger('common', { 42 | stream: fs.createWriteStream(path.join(__dirname, '..', 'log', 'access.log'), { flags: 'a' }), 43 | })); 44 | 45 | // Enable response bodies to be sent as JSON: 46 | app.use(express.json()) 47 | 48 | // Handle CORS: 49 | app.use(cors({ 50 | origin: env.frontend_url, 51 | credentials: true 52 | })); 53 | 54 | // Handle cookies 🍪 55 | app.use(cookieParser()); 56 | 57 | // Use sessions: 58 | app.use(session({ 59 | secret: env.session_secret, 60 | resave: false, 61 | saveUninitialized: false, 62 | store: MongoStore.create({ 63 | mongoUrl: mongoUri, 64 | mongoOptions: mongoClientOptions, 65 | dbName: dbName, 66 | collectionName: 'user_sessions' 67 | }), 68 | })); 69 | 70 | 71 | // 72 | // II. Mount app endpoints: 73 | // 74 | 75 | // Payments endpoint under /payments: 76 | const paymentsRouter = express.Router(); 77 | mountPaymentsEndpoints(paymentsRouter); 78 | app.use('/payments', paymentsRouter); 79 | 80 | // User endpoints (e.g signin, signout) under /user: 81 | const userRouter = express.Router(); 82 | mountUserEndpoints(userRouter); 83 | app.use('/user', userRouter); 84 | 85 | 86 | // Notification endpoints under /notifications: 87 | const notificationRouter = express.Router(); 88 | mountNotificationEndpoints(notificationRouter); 89 | app.use("/notifications", notificationRouter); 90 | 91 | // Hello World page to check everything works: 92 | app.get('/', async (_, res) => { 93 | res.status(200).send({ message: "Hello, World!" }); 94 | }); 95 | 96 | 97 | // III. Boot up the app: 98 | 99 | app.listen(8000, async () => { 100 | try { 101 | const client = await MongoClient.connect(mongoUri, mongoClientOptions) 102 | const db = client.db(dbName); 103 | app.locals.orderCollection = db.collection('orders'); 104 | app.locals.userCollection = db.collection('users'); 105 | console.log('Connected to MongoDB on: ', mongoUri) 106 | } catch (err) { 107 | console.error('Connection to MongoDB failed: ', err) 108 | } 109 | 110 | console.log('App platform demo app - Backend listening on port 8000!'); 111 | console.log(`CORS config: configured to respond to a frontend hosted on ${env.frontend_url}`); 112 | }); 113 | -------------------------------------------------------------------------------- /backend/src/handlers/payments.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Router } from "express"; 3 | import platformAPIClient from "../services/platformAPIClient"; 4 | import "../types/session"; 5 | 6 | export default function mountPaymentsEndpoints(router: Router) { 7 | // handle the incomplete payment 8 | router.post('/incomplete', async (req, res) => { 9 | const payment = req.body.payment; 10 | const paymentId = payment.identifier; 11 | const txid = payment.transaction && payment.transaction.txid; 12 | const txURL = payment.transaction && payment.transaction._link; 13 | 14 | /* 15 | implement your logic here 16 | e.g. verifying the payment, delivering the item to the user, etc... 17 | 18 | below is a naive example 19 | */ 20 | 21 | // find the incomplete order 22 | const app = req.app; 23 | const orderCollection = app.locals.orderCollection; 24 | const order = await orderCollection.findOne({ pi_payment_id: paymentId }); 25 | 26 | // order doesn't exist 27 | if (!order) { 28 | return res.status(400).json({ message: "Order not found" }); 29 | } 30 | 31 | // check the transaction on the Pi blockchain 32 | const horizonResponse = await axios.create({ timeout: 20000 }).get(txURL); 33 | const paymentIdOnBlock = horizonResponse.data.memo; 34 | 35 | // and check other data as well e.g. amount 36 | if (paymentIdOnBlock !== order.pi_payment_id) { 37 | return res.status(400).json({ message: "Payment id doesn't match." }); 38 | } 39 | 40 | // mark the order as paid 41 | await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { txid, paid: true } }); 42 | 43 | // let Pi Servers know that the payment is completed 44 | await platformAPIClient.post(`/v2/payments/${paymentId}/complete`, { txid }); 45 | return res.status(200).json({ message: `Handled the incomplete payment ${paymentId}` }); 46 | }); 47 | 48 | // approve the current payment 49 | router.post('/approve', async (req, res) => { 50 | if (!req.session.currentUser) { 51 | return res.status(401).json({ error: 'unauthorized', message: "User needs to sign in first" }); 52 | } 53 | 54 | const app = req.app; 55 | 56 | const paymentId = req.body.paymentId; 57 | const currentPayment = await platformAPIClient.get(`/v2/payments/${paymentId}`); 58 | const orderCollection = app.locals.orderCollection; 59 | 60 | /* 61 | implement your logic here 62 | e.g. creating an order record, reserve an item if the quantity is limited, etc... 63 | */ 64 | 65 | await orderCollection.insertOne({ 66 | pi_payment_id: paymentId, 67 | product_id: currentPayment.data.metadata.productId, 68 | user: req.session.currentUser.uid, 69 | txid: null, 70 | paid: false, 71 | cancelled: false, 72 | created_at: new Date() 73 | }); 74 | 75 | // let Pi Servers know that you're ready 76 | await platformAPIClient.post(`/v2/payments/${paymentId}/approve`); 77 | return res.status(200).json({ message: `Approved the payment ${paymentId}` }); 78 | }); 79 | 80 | // complete the current payment 81 | router.post('/complete', async (req, res) => { 82 | const app = req.app; 83 | 84 | const paymentId = req.body.paymentId; 85 | const txid = req.body.txid; 86 | const orderCollection = app.locals.orderCollection; 87 | 88 | /* 89 | implement your logic here 90 | e.g. verify the transaction, deliver the item to the user, etc... 91 | */ 92 | 93 | await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { txid: txid, paid: true } }); 94 | 95 | // let Pi server know that the payment is completed 96 | await platformAPIClient.post(`/v2/payments/${paymentId}/complete`, { txid }); 97 | return res.status(200).json({ message: `Completed the payment ${paymentId}` }); 98 | }); 99 | 100 | // handle the cancelled payment 101 | router.post('/cancelled_payment', async (req, res) => { 102 | const app = req.app; 103 | 104 | const paymentId = req.body.paymentId; 105 | const orderCollection = app.locals.orderCollection; 106 | 107 | /* 108 | implement your logic here 109 | e.g. mark the order record to cancelled, etc... 110 | */ 111 | 112 | await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { cancelled: true } }); 113 | return res.status(200).json({ message: `Cancelled the payment ${paymentId}` }); 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Pi Demo App Backend 2 | 3 | The only variable you need to provide is `PI_API_KEY`, which is required to authorize payments. You receive it 4 | upon app registration. For more guidelines on registering your app refer to 5 | the [Pi Developer Guide](https://pi-apps.github.io/community-developer-guide/docs/gettingStarted/devPortal/). 6 | 7 | `FRONTEND_URL` specifies the URL of the frontend app, which by default is `http://localhost:3314`. 8 | Depending on sandbox settings you're using to preview demo app, you may need to change this value accordingly. 9 | 10 | The demo app's backend uses a local MongoDB server to store user data and session details. 11 | 12 | ## Setup 13 | 14 | ### 1. Install dependencies: 15 | 16 | You will need a working NodeJS installation, and `yarn`. **The demo app frontend isn't meant to support npm**. 17 | In most cases, `yarn` will come along with your NodeJS installation. 18 | 19 | Install dependencies by running `yarn install`. 20 | 21 | 22 | ### 2. Set up environment variables 23 | 24 | The demo app's backend use several environment variables, from which most have default values. In order to specify 25 | these, create `.env` file in the root backend directory. 26 | 27 | Create your `.env` file from the template: 28 | 29 | ```shell 30 | cp .env.example .env 31 | 32 | 33 | # Edit the resulting file: 34 | vi .env 35 | # or: 36 | nano .env 37 | ``` 38 | 39 | Obtain the following values from the developer portal: 40 | 41 | **Session secret**: Generate a random string (e.g 64 alphanumeric characters). 42 | 43 | **API key**: obtained by tapping the "Get API Key" button 44 | 45 | ![](./img/api_key.png) 46 | 47 | Then, copy-paste those values in the following two keys of your .env file: 48 | 49 | ``` 50 | # Add your API key here: 51 | PI_API_KEY= 52 | 53 | # Add your session secret here: 54 | SESSION_SECRET= 55 | ``` 56 | 57 | 58 | ### 2. Set up MongoDB 59 | 60 | The default port for MongoDB is `27017`. If you have decided to change either default port or username and password, 61 | make sure to update environment variables in the backend `.env` file accordingly. 62 | Additionally, you can specify MongoDB name env variable, which if not specified will be named `demo-app` by default. 63 | 64 | **Option 1: using Docker:** 65 | 66 | Start a MongoDB server using the following command: 67 | 68 | ``` 69 | docker run --name demoapp-mongo -d \ 70 | -e MONGO_INITDB_ROOT_USERNAME=demoapp -e MONGO_INITDB_ROOT_PASSWORD=dev_password \ 71 | -p 27017:27017 mongo:5.0 72 | ``` 73 | 74 | Down the road, you can use the following commands to stop and start your mongo container: 75 | 76 | ``` 77 | docker stop demoapp-mongo 78 | docker start demoapp-mongo 79 | ``` 80 | 81 | To reinitialize everything (and **drop all the data**) you can run the following command: 82 | 83 | ``` 84 | docker kill demoapp-mongo; docker rm demoapp-mongo 85 | ``` 86 | 87 | Then, recreate the container using the `docker run` command above. 88 | 89 | 90 | **Options 2: directly install MongoDB on your machine:** 91 | 92 | Install MongoDB Community following the 93 | [official documentation](https://www.mongodb.com/docs/manual/administration/install-community/). 94 | 95 | Run the server and create a database and a user: 96 | 97 | Open a Mongo shell by running `mongosh`, then paste the following JS code into it: 98 | 99 | ```javascript 100 | var MONGODB_DATABASE_NAME = "demoapp-development" 101 | var MONGODB_USERNAME = "demoapp" 102 | var MONGODB_PASSWORD = "dev_password" 103 | 104 | db.getSiblingDB("admin").createUser( 105 | { 106 | user: MONGODB_USERNAME, 107 | pwd: MONGODB_PASSWORD, 108 | roles: [ 109 | { 110 | role: "dbOwner", 111 | db: MONGODB_DATABASE_NAME, 112 | } 113 | ] 114 | } 115 | ); 116 | ``` 117 | 118 | To preview the database, you can use Robo3T or MongoDB Compass. 119 | 120 | ### 3. Run the server 121 | 122 | Start the server with the following command (inside of the `backend` directory): `yarn start`. 123 | 124 | If everything is set up correctly you should see the following output in your terminal: 125 | 126 | ``` 127 | NODE_ENV: development 128 | Connected to MongoDB on: mongodb://localhost:27017/demoapp-development 129 | App platform demo app - Backend listening on port 8000! 130 | CORS config: configured to respond to a frontend hosted on http://localhost:3314 131 | ``` 132 | 133 | --- 134 | You've completed the backend setup, return to [`doc/development.md`](../doc/deployment.md) to finish setting up the demo app 135 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | # Pi Platform Demo App: Development Environment 2 | 3 | The following document explains how to set up a development environment and run the Pi Platform Demo App in the 4 | sandbox environment. 5 | 6 | ## Prerequisites: 7 | 8 | This guides assumes you have the following two tools installed on your development machine: 9 | 10 | - a NodeJS installation (recommended version: Node v16 LTS **or lower**) 11 | - if you're running with node 17 or higher, you'll need to use `yarn startNode17` to work around [this issue](https://github.com/facebook/create-react-app/issues/11562) 12 | - a functional Docker or Mongosh installation 13 | 14 | 15 | ## 1. Clone the Github repo and navigate to the project directory: 16 | 17 | ```sh 18 | git clone git@github.com:pi-apps/platform-demo-app.git 19 | cd platform-demo-app 20 | ``` 21 | 22 | 23 | ## 2. Register your app on the developer portal 24 | 25 | Open `develop.pi` in the Pi Browser, on your mobile phone, and go through the prerequisite steps 26 | (e.g verifying your email address). 27 | 28 | Create a new app by clicking the "Register an App" button. 29 | 30 | The Process is as follows: 31 | 32 | - Register an App: 33 | - Name: The name of your app 34 | - Description: A public user-facing description of your app 35 | - Network: select "Pi Testnet" 36 | - Submit the form 37 | 38 | Registration Form for an App 39 | 40 |
41 | 42 | This will bring you to the App Dashboard, from this screen you can continue the development of the demo app. 43 | 44 | - App Checklist: Complete Steps 1-5 to prepare the app for launch in development mode 45 | - Step 1: "Register an App" was completed previously 46 | - Step 2: Configure App Hosting - Hosting type: select "Self hosted" 47 | - Step 3: Create a Pi Wallet, see this [Pi Wallet Introduction](https://pi-apps.github.io/community-developer-guide/docs/importantTopics/paymentFlow/piWallet/) for more information 48 | - Step 4: Review this documentation and our [Community Developer Guide](https://pi-apps.github.io/community-developer-guide/) for help getting setup 49 | - Step 5: Configure App Development URL: the URL on which your app is running on your development environment. If your using the default 50 | setup for the demo app frontend, this is "http://localhost:3314. **Note:** If you need, you can change the port by specifying it in 51 | `frontend/.env` file, as the value of the `PORT` environment variable. 52 | 53 | App Checklist 54 | 55 |
56 | 57 | - App Configuration: Additional information that can be adjusted 58 | - Whitelisted usernames: you can leave this blank at this point 59 | - App URL: This is irrelevant for development. You can use the intended production URL of your app (e.g "https://mydemoapp.com"), 60 | or simply set it up to an example value (e.g "https://example.com"). This must be an HTTPs URL 61 | - Development URL: The URL on which your app is running on your development environment. If your using the default 62 | setup for the demo 63 | 64 | ## 3. Run the frontend development server 65 | 66 | Setup the frontend app following the [Pi Demo App Frontend documentation](../frontend/README.md). 67 | 68 | 69 | ## 4. Run the backend development server 70 | 71 | Setup backend server following the [Pi Demo App Backend documentation](../backend/README.md). 72 | 73 | ## 5. Open the app in the Sandbox 74 | 75 | To test all of the Demo App features, you need to run it in the Pi Sandbox. 76 | 77 | The purpose of the Sandbox is to enable running and debugging a Pi App in a desktop browser, although a Pi App 78 | is meant to be opened in the Pi Browser by end-users. 79 | 80 | For more information on the Sandbox head to our [Community Developer Guide](https://pi-apps.github.io/community-developer-guide/docs/gettingStarted/piAppPlatform/piAppPlatformSDK/#the-sandbox-flag). 81 | 82 | Lastly, **on your desktop browser** open the sandbox URL from Step 6 of App Checklist: 83 | 84 | ![Sandbox URL](./img/sandbox_url.png) 85 | 86 | ![Sandbox URL](./img/sandbox_firefox.png) 87 | 88 | > **WARNING** 89 | > 90 | > The demo app uses express session cookies which, in the Sandbox environment, are not correctly saved on the client on some browsers. 91 | > To properly test all of the features of the Demo App, we recommend you to open the sandbox app using Mozilla Firefox. 92 | 93 | #### Congratulations! The app should work in the developer portal and enable you to sign in, place an order and make a testnet payment. 94 | 95 | # More Information - Developing on Pi 96 | For guidelines on how to register an app and get the Sandbox URL, please refer to the 97 | [Pi Developer Guide](https://pi-apps.github.io/community-developer-guide/). 98 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/Shop/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import axios from 'axios'; 3 | import ProductCard from './components/ProductCard'; 4 | import SignIn from './components/SignIn'; 5 | import Header from './components/Header'; 6 | 7 | type MyPaymentMetadata = {}; 8 | 9 | type AuthResult = { 10 | accessToken: string, 11 | user: { 12 | uid: string, 13 | username: string, 14 | roles: string[], 15 | } 16 | }; 17 | 18 | export type User = AuthResult['user']; 19 | 20 | interface PaymentDTO { 21 | amount: number, 22 | user_uid: string, 23 | created_at: string, 24 | identifier: string, 25 | metadata: Object, 26 | memo: string, 27 | status: { 28 | developer_approved: boolean, 29 | transaction_verified: boolean, 30 | developer_completed: boolean, 31 | cancelled: boolean, 32 | user_cancelled: boolean, 33 | }, 34 | to_address: string, 35 | transaction: null | { 36 | txid: string, 37 | verified: boolean, 38 | _link: string, 39 | }, 40 | }; 41 | 42 | // Make TS accept the existence of our window.__ENV object - defined in index.html: 43 | interface WindowWithEnv extends Window { 44 | __ENV?: { 45 | backendURL: string, // REACT_APP_BACKEND_URL environment variable 46 | sandbox: "true" | "false", // REACT_APP_SANDBOX_SDK environment variable - string, not boolean! 47 | } 48 | } 49 | 50 | const _window: WindowWithEnv = window; 51 | const backendURL = _window.__ENV && _window.__ENV.backendURL; 52 | 53 | const axiosClient = axios.create({ baseURL: `${backendURL}`, timeout: 20000, withCredentials: true}); 54 | const config = {headers: {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}}; 55 | 56 | 57 | export default function Shop() { 58 | const [user, setUser] = useState(null); 59 | const [showModal, setShowModal] = useState(false); 60 | 61 | const signIn = async () => { 62 | const scopes = ['username', 'payments', 'roles', 'in_app_notifications']; 63 | const authResult: AuthResult = await window.Pi.authenticate(scopes, onIncompletePaymentFound); 64 | signInUser(authResult); 65 | setUser(authResult.user); 66 | } 67 | 68 | const signOut = () => { 69 | setUser(null); 70 | signOutUser(); 71 | } 72 | 73 | const onSendTestNotification = () => { 74 | const notification = { 75 | title: "Test Notification", 76 | body: "This is a test notification", 77 | user_uid: user?.uid, 78 | subroute: "/shop" 79 | }; 80 | axiosClient.post( 81 | "/notifications/send", 82 | { notifications: [notification] }, 83 | config 84 | ); 85 | }; 86 | 87 | const signInUser = (authResult: AuthResult) => { 88 | axiosClient.post('/user/signin', {authResult}); 89 | return setShowModal(false); 90 | } 91 | 92 | const signOutUser = () => { 93 | return axiosClient.get('/user/signout'); 94 | } 95 | 96 | const onModalClose = () => { 97 | setShowModal(false); 98 | } 99 | 100 | const orderProduct = async (memo: string, amount: number, paymentMetadata: MyPaymentMetadata) => { 101 | if(user === null) { 102 | return setShowModal(true); 103 | } 104 | const paymentData = { amount, memo, metadata: paymentMetadata }; 105 | const callbacks = { 106 | onReadyForServerApproval, 107 | onReadyForServerCompletion, 108 | onCancel, 109 | onError 110 | }; 111 | const payment = await window.Pi.createPayment(paymentData, callbacks); 112 | console.log(payment); 113 | } 114 | 115 | const onIncompletePaymentFound = (payment: PaymentDTO) => { 116 | console.log("onIncompletePaymentFound", payment); 117 | return axiosClient.post('/payments/incomplete', {payment}); 118 | } 119 | 120 | const onReadyForServerApproval = (paymentId: string) => { 121 | console.log("onReadyForServerApproval", paymentId); 122 | axiosClient.post('/payments/approve', {paymentId}, config); 123 | } 124 | 125 | const onReadyForServerCompletion = (paymentId: string, txid: string) => { 126 | console.log("onReadyForServerCompletion", paymentId, txid); 127 | axiosClient.post('/payments/complete', {paymentId, txid}, config); 128 | } 129 | 130 | const onCancel = (paymentId: string) => { 131 | console.log("onCancel", paymentId); 132 | return axiosClient.post('/payments/cancelled_payment', {paymentId}); 133 | } 134 | 135 | const onError = (error: Error, payment?: PaymentDTO) => { 136 | console.log("onError", error); 137 | if (payment) { 138 | console.log(payment); 139 | // handle the error accordingly 140 | } 141 | } 142 | 143 | return ( 144 | <> 145 |
146 | 147 | orderProduct("Order Apple Pie", 3, { productId: 'apple_pie_1' })} 154 | /> 155 | orderProduct("Order Lemon Meringue Pie", 5, { productId: 'lemon_pie_1' })} 162 | /> 163 | 164 | { showModal && } 165 | 166 | ); 167 | } 168 | -------------------------------------------------------------------------------- /doc/deployment.md: -------------------------------------------------------------------------------- 1 | # Pi Platform Demo app: Deployment Guide 2 | 3 | **Warning: The purpose of this project is to be simple to run, with very few configuration steps. There is no** 4 | **intent to make it 100% secure, infinitely scalable, or suitable for a live production app handling** 5 | **real Pi on the mainnet. Use it at your own risk and customize it as needed to improve security and scalability.** 6 | 7 | The production deployment uses docker-compose, and needs to run 4 containers: 8 | 9 | * **reverse-proxy**: a simple nginx setup to reverse-proxy requests to the frontend and backend containers 10 | * **backend**: the backend app (a very simple JSON API built using Node) 11 | * **mongo**: the database (uses an extremely simple single-instance MongoDB setup) 12 | * **frontend**: the SPA frontend app (built using React and create-react-app) 13 | 14 | ## Setup steps 15 | 16 | The following setup process was tested on Ubuntu Server 22.04.1. You may need to adapt it to your own needs 17 | depending on your distro (specifically, the "Install Docker" step). 18 | 19 | It assumes you're running as root. If that's not the case, prepend all commands with `sudo` or start 20 | a root shell with `sudo bash`. 21 | 22 | 23 | ### 1. Install Docker and docker-compose: 24 | 25 | Run the following command in order to get the latest docker version and the latest docker-compose version 26 | (required because of [this issue](https://github.com/datahub-project/datahub/issues/2020#issuecomment-736850314)) 27 | 28 | ```shell 29 | apt-get update && \ 30 | apt-get install -y zip apt-transport-https ca-certificates curl gnupg-agent software-properties-common && \ 31 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && \ 32 | add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" && \ 33 | apt-get update && \ 34 | apt-get install -y docker-ce docker-ce-cli containerd.io && \ 35 | curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \ 36 | chmod +x /usr/local/bin/docker-compose 37 | ``` 38 | 39 | 40 | ### 2. Get a server and set up your DNS 41 | 42 | Obtain a domain name and set up 2 DNS records pointing to your server. One will be used for the frontend app, and one 43 | will be used for the backend app. 44 | 45 | Here is an example, assuming your domain name is mydemoapp.com and your server IP is 123.123.123.123. 46 | 47 | ```DNS 48 | A mydemoapp.com 123.123.123.123 49 | A backend.mydemoapp.com 123.123.123.123 50 | ``` 51 | 52 | 53 | ### 3. Register your app on the developer portal 54 | 55 | Open `develop.pi` in the Pi Browser, on your mobile phone, and go through the prerequisite steps 56 | (e.g verifying your email address). 57 | 58 | Two options here: 59 | - if you already have a development app set up, you can reuse it here (see [the development guide](./development.md)) 60 | - if you're setting things up for the first time, you can create a new app by clicking the "Register An App" button. 61 | 62 | Register an App: 63 | - Name: The name of your app 64 | - Description: A public user-facing description of your app 65 | 66 | Registration Form for an App 67 | 68 |
69 | 70 | This will bring you to the App Dashboard, from this screen you can continue the setup of the demo app for deployment. 71 | 72 | - App Checklist: Complete Steps 6-9 to prepare the app for deployment 73 | - Step 6: Deploy the App to the Production Environment - Add the Production URL 74 | - Step 7: Verify Domain Ownership - Add the string supplied in this step to your .env file. This is deailed in step 4 of this list. 75 | - Step 8: Process a transaction - Visit your app in the Pi Browser and complete a payment 76 | 77 | App Checklist 78 |
79 | 80 | - App Configuration: Additional information that can be adjusted 81 | - Whitelisted usernames: you can leave this blank at this point 82 | - Hosting type: select "Self hosted" 83 | - App URL: Enter the intended production URL of your app (e.g "https://mydemoapp.com"), 84 | or simply set it up to an example value (e.g "https://example.com"). This must be an HTTPs URL. 85 | - Development URL: This is irrelevant for deployment. 86 | 87 | 88 | ### 4. Set up environment variables (1/3) 89 | 90 | Create a .env config file in order to enable docker-compose to pick up your environmnent variables: 91 | 92 | ```shell 93 | # Move to the app's directory: 94 | cd platform-demo-app 95 | 96 | # Create .env from the starter template: 97 | cp .env.example .env 98 | 99 | # Edit the resulting file: 100 | vi .env 101 | # or: 102 | nano .env 103 | ``` 104 | 105 | Set up the following environment variables. Using the example above: 106 | 107 | ``` 108 | FRONTEND_URL=https://mydemoapp.com 109 | FRONTEND_DOMAIN_NAME=mydemoapp.com 110 | BACKEND_URL=https://backend.mydemoapp.com 111 | BACKEND_DOMAIN_NAME=backend.mydemoapp.com 112 | ``` 113 | 114 | 115 | ### 5. Set up environment variables (2/3) 116 | 117 | Obtain the following values from the developer portal: 118 | 119 | **Domain Verification Key**: Go to the developer portal, and on your list of registered apps click on the app you are verifying. Next click on the "App Checklist" and click on step 7. From this page you can obtain the string that needs added to your `.env` file. 120 | 121 | 122 | 123 | **API key**: obtained by tapping the "API Key" button on the App Dashboard 124 | 125 | 126 | 127 | Then, copy-paste those values in the following two keys of your .env file: 128 | 129 | ``` 130 | # Add your Domain Validation Key here: 131 | DOMAIN_VALIDATION_KEY= 132 | 133 | # Add your API key here: 134 | PI_API_KEY= 135 | ``` 136 | 137 | 138 | ### 6. Set up environment variables (3/3) 139 | 140 | Configure the following environment variables: 141 | 142 | ``` 143 | # Generate a secure password for your MongoDB database and paste it here: 144 | MONGODB_PASSWORD= 145 | 146 | # Generate a random string (e.g 64 alphanumeric characters) and paste it here: 147 | SESSION_SECRET= 148 | ``` 149 | 150 | 151 | ### 7. Run the docker containers: 152 | 153 | **Make sure you are in the `platform-demo-app` directory before running any `docker-compose` command!** 154 | 155 | ``` 156 | docker-compose build 157 | docker-compose up -d 158 | ``` 159 | 160 | You can check which containers are running using either one of the following two commands: 161 | 162 | ``` 163 | docker ps 164 | docker-compose ps 165 | ``` 166 | 167 | 168 | You can print out logs of the Docker containers using the following command: 169 | 170 | ``` 171 | docker-compose logs -f 172 | 173 | # e.g: 174 | docker-compose logs -f reverse-proxy 175 | ``` 176 | 177 | 178 | ### 8. Verify Domain Ownership 179 | 180 | Go to the developer portal, and on your list of registered apps click on the app you are verifying. While the app is deployed, click on the "Verify Domain" button. 181 | This should verify your frontend domain and mark it as verified (green check mark on the developer portal). 182 | 183 | ![](./img/domain_verified.png) 184 | 185 | 186 | ### 9. Open your app in the Pi Browser and make sure it worked: 187 | 188 | Open your frontend app's URL in the developer portal and make sure everything works by signing it and placing 189 | an order, then proceeding with the payment (you will need a testnet wallet in order to complete this step). 190 | -------------------------------------------------------------------------------- /FLOWS.md: -------------------------------------------------------------------------------- 1 | # Pi Demo App Flows 2 | 3 | - [Authentication](#authentication) 4 | - [`Pi.authenticate()`](#1-obtain-user-data-with-piauthenticate-sdk-method) 5 | - [`GET` `/me Platform API endpoint`](#2-verify-the-user-by-calling-pi-platform-api-get-me-endpoint) 6 | - [Payments](#payments) 7 | - [`onReadyForServerApproval`](#1-onreadyforserverapproval) 8 | - [`onReadyForServerCompletion`](#2-onreadyforservercompletion) 9 | - [`onCancel`](#3-oncancel) 10 | - [`onError`](#4-onerror) 11 | - [`onIncompletePaymentFound`](#onincompletepaymentfound) 12 | 13 | ## Authentication 14 | 15 | User authentication consists of two steps: 16 | 17 | 1. obtaining user `accessToken` using Pi SDK 18 | 2. verifying `accessToken` using Pi Platform API 19 | 20 |
21 | 22 | > **Disclaimer:** 23 | > 24 | > **Pi SDK** method `Pi.authenticate()` should only be used to retrieve user `accessToken` and **MUST be verified on the backend side of your app**. 25 | > 26 | > For a detailed guide on how to use Pi SDK, please refer to [SDK Docs](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md) 27 | > 28 | > **Pi Platform API** should remain the only source of truth about user data in your app (a malicious user could tamper with the requests and send you wrong data). 29 | > 30 | > For a detailed guide on how to use Pi Platform API, please refer to [Platform API Docs](https://github.com/pi-apps/pi-platform-docs/blob/master/platform_API.md) 31 | 32 |
33 | 34 | ### 1. Obtain user data with `Pi.authenticate()` SDK method 35 | 36 | `Pi.authenticate()` method takes in two arguments: `scopes` and `onIncompletePaymentFound` and returns `AuthResult` object with different keys available. 37 | 38 | `scopes` determine what keys are available on the `AuthResult` object. For full documentation on available scopes, please refer to [SDK Docs](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#scopes). 39 | 40 | `onIncompletePaymentFound` is a function that connects both Authorization and Payments flows. To preview example implementation, proceed to [`onIncompletePaymentFound`](#onincompletepaymentfound) section. 41 | 42 | ```typescript 43 | // frontend/src/shop/index.ts 44 | 45 | const signIn = async () => { 46 | const scopes = ["username", "payments"]; 47 | const authResponse = await window.Pi.authenticate(scopes, onIncompletePaymentFound); 48 | 49 | /* pass obtained data to backend */ 50 | await signInUser(authResponse); 51 | 52 | /* use the obtained data however you want */ 53 | setUser(authResponse.user); 54 | }; 55 | 56 | const signInUser = (authResult: any) => { 57 | axiosClient.post("/signin", { authResult }, config); 58 | 59 | return setShowModal(false); 60 | }; 61 | ``` 62 | 63 |
64 | 65 | ### 2. Verify the user by calling Pi Platform API `GET /me` endpoint 66 | 67 |
68 | 69 | `GET /me` endpoint: 70 | 71 | - uses `accessToken` as `Authorization` header 72 | - returns [UserDTO](https://github.com/pi-apps/pi-platform-docs/blob/master/platform_API.md#UserDTO) object if the user was successfully authorized 73 | - responds with status `401` if the user was not successfully authorized 74 | 75 |
76 | 77 | ```typescript 78 | // backend/src/index.ts 79 | 80 | app.post('/signin', async (req, res) => { 81 | try { 82 | /* verify with the user's access token */ 83 | const me = await axiosClient.get(`/v2/me`, { headers: { 'Authorization': `Bearer ${currentUser.accessToken}` } }); 84 | console.log(me); 85 | } 86 | catch (err) { 87 | console.error(err); 88 | return res.status(401).json({error: "User not authorized"}); 89 | } 90 | return res.status(200).json({ message: "User signed in" }); 91 | } 92 | ``` 93 | 94 | ## Payments 95 | 96 | To request a payment from the current user to your app's account, use the `Pi.createPayment()` SDK method, which accepts two arguments: 97 | 98 | 1. PaymentData object consists of three fields: [`amount`](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#amount), [`memo`](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#memo) and [`metadata`](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#metadata), from which `amount` and `memo` are required by Pi Platform API, while `metadata` is for your app use. 99 | 2. Callbacks object consisting of four callbacks: 100 | 1. `onReadyForServerApproval` 101 | 2. `onReadyForServerCompletion` 102 | 3. `onCancel` 103 | 4. `onError` 104 | 105 | To learn more about `Pi.createPayment()` method and its arguments, please refer to [Pi SDK Docs](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#payments) 106 | 107 | ```typescript 108 | // frontend/src/Shop/index.ts 109 | 110 | const orderProduct = async (memo: string, amount: number, paymentMetadata: MyPaymentMetadata) => { 111 | if (user == null) { 112 | return setShowModal(true); 113 | } 114 | 115 | const paymentData = { amount: amount, memo: memo, metadata: paymentMetadata }; 116 | const callbacks = { 117 | onReadyForServerApproval, 118 | onReadyForServerCompletion, 119 | onCancel, 120 | onError, 121 | }; 122 | 123 | const payment = await window.Pi.createPayment(paymentData, callbacks); 124 | console.log("payment", payment); 125 | }; 126 | ``` 127 | 128 | ### 1. `onReadyForServerApproval` 129 | 130 | `onReadyForServerApproval` receives the payment identifier (`paymentId`) and should pass it to your app's backend for Server-Side approval. 131 | It is called when the payment identifier (`paymentId`) is obtained from Pi Servers. 132 | 133 | ```typescript 134 | // frontend/src/Shop/index.ts 135 | 136 | const onReadyForServerApproval = (paymentId: string) => { 137 | console.log("onReadyForServerApproval", paymentId); 138 | axiosClient.post("/approve", { paymentId }, config); 139 | }; 140 | ``` 141 | 142 | On the backend side of your app make an API call to Pi Platform `POST /payments/:paymentId/approve` to approve payment on the Pi Servers. 143 | 144 | ```typescript 145 | // backend/src/index.ts 146 | 147 | app.post('/approve', async (req, res) => { 148 | ... 149 | const paymentId = req.body.paymentId; 150 | 151 | ... 152 | 153 | /* let Pi server know that you're ready */ 154 | await axiosClient.post(`/v2/payments/${paymentId}/approve`, {}, config); 155 | return res.status(200).json({ message: `Approved the payment ${paymentId}` }); 156 | }); 157 | ``` 158 | 159 | ### 2. `onReadyForServerCompletion` 160 | 161 | `onReadyForServerCompletion` receives payment identifier (`paymentId`) and blockchain transaction identifier (`txid`). You need this value for the Server-Side Completion flow. 162 | It is called when the user has submitted the transaction to the Pi blockchain. 163 | 164 | ```typescript 165 | // frontend/src/Shop/index.ts 166 | 167 | const onReadyForServerCompletion = (paymentId: string, txid: string) => { 168 | console.log("onReadyForServerCompletion", paymentId, txid); 169 | axiosClient.post("/complete", { paymentId, txid }, config); 170 | }; 171 | ``` 172 | 173 | On the backend side of your app make an API call to Pi Platform `POST /payments/:paymentId/approve` to let Pi Servers know that payment has been completed. 174 | 175 | ```typescript 176 | // backend/src/index.ts 177 | 178 | app.post('/complete', async (req, res) => { 179 | const paymentId = req.body.paymentId; 180 | const txid = req.body.txid; 181 | 182 | ... 183 | 184 | /* let Pi server know that the payment is completed */ 185 | await axiosClient.post(`/v2/payments/${paymentId}/complete`, { txid }, config); 186 | return res.status(200).json({ message: `Completed the payment ${paymentId}` }); 187 | }); 188 | ``` 189 | 190 | ### 3. `onCancel` 191 | 192 | `onCancel` receives payment identifier (`paymentId`). 193 | It is called when the payment is canceled - this can be triggered by a user action, programmatically or automatically if your app's backend doesn't approve the payment within 60 seconds. 194 | 195 | ```typescript 196 | // frontend/src/Shop/index.ts 197 | 198 | const onCancel = (paymentId: string) => { 199 | console.log("onCancel", paymentId); 200 | return axiosClient.post("/cancelled_payment", { paymentId }, config); 201 | }; 202 | ``` 203 | 204 | ### 4. `onError` 205 | 206 | `onError` receives Error Object (`error`) and Payment DTO (`payment`). 207 | It is called when an error occurs and the payment cannot be made. 208 | If the payment has been created, the second argument will be present and you may use it to investigate the error. 209 | Otherwise, only the first argument will be provided. 210 | 211 | `onError` callback is provided for informational purposes only and doesn't need to be passed and handled on the backend side of your app. 212 | 213 | ```typescript 214 | // frontend/src/Shop/index.ts 215 | 216 | const onError = (error: Error, payment?: PaymentDTO) => { 217 | console.log("onError", error); 218 | if (payment) { 219 | console.log(payment); 220 | /* handle the error accordingly */ 221 | } 222 | }; 223 | ``` 224 | 225 | ### `onIncompletePaymentFound` 226 | 227 | `onIncompletePaymentFound` connects both Authentication and Payment flows. It is the second argument required by `Pi.authenticate()` SDK method, which checks for the user's incomplete payment each time the user is authenticated. If an incomplete payment is found, `onIncompletePaymentFound` callback will be invoked with the payment's [PaymentDTO](https://github.com/pi-apps/pi-platform-docs/blob/master/platform_API.md#paymentdto) object and the corresponding payment must be completed inside of your app. For more details about `onIncompletePaymentFound` please refer to [SDK Docs](https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#onincompletepaymentfound) 228 | 229 | ```typescript 230 | // frontend/src/Shop/index.ts 231 | 232 | const onIncompletePaymentFound = (payment: PaymentDTO) => { 233 | console.log("onIncompletePaymentFound", payment); 234 | return axiosClient.post("/incomplete", { payment }, config); 235 | }; 236 | 237 | const signIn = async () => { 238 | // ... 239 | const authResponse = await window.Pi.authenticate(scopes, onIncompletePaymentFound); 240 | // ... 241 | }; 242 | ``` 243 | 244 | ```typescript 245 | // backend/src/index.ts 246 | 247 | app.post("/incomplete", async (req, res) => { 248 | const payment = req.body.payment; 249 | const paymentId = payment.identifier; 250 | const txid = payment.transaction && payment.transaction.txid; 251 | const txURL = payment.transaction && payment.transaction._link; 252 | 253 | /* your custom logic checking against incomplete order in DB */ 254 | const order = ... 255 | // ... 256 | 257 | /* check the transaction on the Pi blockchain */ 258 | const horizonResponse = await axios.create({ timeout: 20000 }).get(txURL); 259 | const paymentIdOnBlock = horizonResponse.data.memo; 260 | 261 | /* check other data as well e.g. amount */ 262 | if (paymentIdOnBlock !== order.pi_payment_id) { 263 | return res.status(400).json({ message: "Payment id doesn't match." }); 264 | } 265 | 266 | /* mark the order as paid in your DB */ 267 | // ... 268 | 269 | /* let Pi Servers know that the payment is completed */ 270 | await axiosClient.post(`/v2/payments/${paymentId}/complete`, { txid }, config); 271 | return res.status(200).json({ message: `Handled the incomplete payment ${paymentId}` }); 272 | }); 273 | ``` 274 | -------------------------------------------------------------------------------- /backend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-support@^0.8.0": 6 | version "0.8.1" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" 8 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 9 | dependencies: 10 | "@jridgewell/trace-mapping" "0.3.9" 11 | 12 | "@jridgewell/resolve-uri@^3.0.3": 13 | version "3.0.8" 14 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.8.tgz#687cc2bbf243f4e9a868ecf2262318e2658873a1" 15 | integrity sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w== 16 | 17 | "@jridgewell/sourcemap-codec@^1.4.10": 18 | version "1.4.14" 19 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" 20 | integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 21 | 22 | "@jridgewell/trace-mapping@0.3.9": 23 | version "0.3.9" 24 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" 25 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 26 | dependencies: 27 | "@jridgewell/resolve-uri" "^3.0.3" 28 | "@jridgewell/sourcemap-codec" "^1.4.10" 29 | 30 | "@tsconfig/node10@^1.0.7": 31 | version "1.0.9" 32 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" 33 | integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== 34 | 35 | "@tsconfig/node12@^1.0.7": 36 | version "1.0.11" 37 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" 38 | integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== 39 | 40 | "@tsconfig/node14@^1.0.0": 41 | version "1.0.3" 42 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" 43 | integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== 44 | 45 | "@tsconfig/node16@^1.0.2": 46 | version "1.0.3" 47 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" 48 | integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== 49 | 50 | "@types/body-parser@*": 51 | version "1.19.2" 52 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" 53 | integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== 54 | dependencies: 55 | "@types/connect" "*" 56 | "@types/node" "*" 57 | 58 | "@types/connect-mongo@^3.1.3": 59 | version "3.1.3" 60 | resolved "https://registry.yarnpkg.com/@types/connect-mongo/-/connect-mongo-3.1.3.tgz#4ff124a30e530dd2dae4725cfd28ca0b9badbfd1" 61 | integrity sha512-h162kWzfphobvJOttkYKLXEQQmZuOlOSA1IszOusQhguCGf+/B8k4H373SJ0BtVv+qkXP/lziEuUfZDNfzZ1tw== 62 | dependencies: 63 | connect-mongo "*" 64 | 65 | "@types/connect@*": 66 | version "3.4.35" 67 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" 68 | integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== 69 | dependencies: 70 | "@types/node" "*" 71 | 72 | "@types/cookie-parser@^1.4.2": 73 | version "1.4.3" 74 | resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.3.tgz#3a01df117c5705cf89a84c876b50c5a1fd427a21" 75 | integrity sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w== 76 | dependencies: 77 | "@types/express" "*" 78 | 79 | "@types/cors@^2.8.11": 80 | version "2.8.12" 81 | resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" 82 | integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== 83 | 84 | "@types/express-serve-static-core@^4.17.18": 85 | version "4.17.29" 86 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" 87 | integrity sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q== 88 | dependencies: 89 | "@types/node" "*" 90 | "@types/qs" "*" 91 | "@types/range-parser" "*" 92 | 93 | "@types/express-session@^1.17.5": 94 | version "1.17.5" 95 | resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.5.tgz#13f48852b4aa60ff595835faeb4b4dda0ba0866e" 96 | integrity sha512-l0DhkvNVfyUPEEis8fcwbd46VptfA/jmMwHfob2TfDMf3HyPLiB9mKD71LXhz5TMUobODXPD27zXSwtFQLHm+w== 97 | dependencies: 98 | "@types/express" "*" 99 | 100 | "@types/express@*", "@types/express@^4.17.12": 101 | version "4.17.13" 102 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" 103 | integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== 104 | dependencies: 105 | "@types/body-parser" "*" 106 | "@types/express-serve-static-core" "^4.17.18" 107 | "@types/qs" "*" 108 | "@types/serve-static" "*" 109 | 110 | "@types/mime@^1": 111 | version "1.3.2" 112 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" 113 | integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== 114 | 115 | "@types/morgan@^1.9.2": 116 | version "1.9.3" 117 | resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540" 118 | integrity sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q== 119 | dependencies: 120 | "@types/node" "*" 121 | 122 | "@types/node@*": 123 | version "18.0.0" 124 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" 125 | integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== 126 | 127 | "@types/node@^18.7.23": 128 | version "18.7.23" 129 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f" 130 | integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg== 131 | 132 | "@types/qs@*": 133 | version "6.9.7" 134 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" 135 | integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== 136 | 137 | "@types/range-parser@*": 138 | version "1.2.4" 139 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" 140 | integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== 141 | 142 | "@types/serve-static@*": 143 | version "1.13.10" 144 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" 145 | integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== 146 | dependencies: 147 | "@types/mime" "^1" 148 | "@types/node" "*" 149 | 150 | "@types/strip-bom@^3.0.0": 151 | version "3.0.0" 152 | resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" 153 | integrity sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ== 154 | 155 | "@types/strip-json-comments@0.0.30": 156 | version "0.0.30" 157 | resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" 158 | integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== 159 | 160 | "@types/webidl-conversions@*": 161 | version "6.1.1" 162 | resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e" 163 | integrity sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q== 164 | 165 | "@types/whatwg-url@^8.2.1": 166 | version "8.2.2" 167 | resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-8.2.2.tgz#749d5b3873e845897ada99be4448041d4cc39e63" 168 | integrity sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA== 169 | dependencies: 170 | "@types/node" "*" 171 | "@types/webidl-conversions" "*" 172 | 173 | accepts@~1.3.8: 174 | version "1.3.8" 175 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 176 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 177 | dependencies: 178 | mime-types "~2.1.34" 179 | negotiator "0.6.3" 180 | 181 | acorn-walk@^8.1.1: 182 | version "8.2.0" 183 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 184 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 185 | 186 | acorn@^8.4.1: 187 | version "8.7.1" 188 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" 189 | integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== 190 | 191 | anymatch@~3.1.2: 192 | version "3.1.2" 193 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 194 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 195 | dependencies: 196 | normalize-path "^3.0.0" 197 | picomatch "^2.0.4" 198 | 199 | arg@^4.1.0: 200 | version "4.1.3" 201 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 202 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 203 | 204 | array-flatten@1.1.1: 205 | version "1.1.1" 206 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 207 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 208 | 209 | asn1.js@^5.4.1: 210 | version "5.4.1" 211 | resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" 212 | integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== 213 | dependencies: 214 | bn.js "^4.0.0" 215 | inherits "^2.0.1" 216 | minimalistic-assert "^1.0.0" 217 | safer-buffer "^2.1.0" 218 | 219 | axios@^0.21.1: 220 | version "0.21.4" 221 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" 222 | integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== 223 | dependencies: 224 | follow-redirects "^1.14.0" 225 | 226 | balanced-match@^1.0.0: 227 | version "1.0.2" 228 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 229 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 230 | 231 | base64-js@^1.3.1: 232 | version "1.5.1" 233 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 234 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 235 | 236 | basic-auth@~2.0.1: 237 | version "2.0.1" 238 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 239 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 240 | dependencies: 241 | safe-buffer "5.1.2" 242 | 243 | binary-extensions@^2.0.0: 244 | version "2.2.0" 245 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 246 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 247 | 248 | bn.js@^4.0.0: 249 | version "4.12.0" 250 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" 251 | integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== 252 | 253 | body-parser@1.20.0: 254 | version "1.20.0" 255 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" 256 | integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== 257 | dependencies: 258 | bytes "3.1.2" 259 | content-type "~1.0.4" 260 | debug "2.6.9" 261 | depd "2.0.0" 262 | destroy "1.2.0" 263 | http-errors "2.0.0" 264 | iconv-lite "0.4.24" 265 | on-finished "2.4.1" 266 | qs "6.10.3" 267 | raw-body "2.5.1" 268 | type-is "~1.6.18" 269 | unpipe "1.0.0" 270 | 271 | brace-expansion@^1.1.7: 272 | version "1.1.11" 273 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 274 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 275 | dependencies: 276 | balanced-match "^1.0.0" 277 | concat-map "0.0.1" 278 | 279 | braces@~3.0.2: 280 | version "3.0.2" 281 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 282 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 283 | dependencies: 284 | fill-range "^7.0.1" 285 | 286 | bson@^4.6.3: 287 | version "4.6.4" 288 | resolved "https://registry.yarnpkg.com/bson/-/bson-4.6.4.tgz#e66d4a334f1ab230dfcfb9ec4ea9091476dd372e" 289 | integrity sha512-TdQ3FzguAu5HKPPlr0kYQCyrYUYh8tFM+CMTpxjNzVzxeiJY00Rtuj3LXLHSgiGvmaWlZ8PE+4KyM2thqE38pQ== 290 | dependencies: 291 | buffer "^5.6.0" 292 | 293 | buffer-from@^1.0.0: 294 | version "1.1.2" 295 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 296 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 297 | 298 | buffer@^5.6.0: 299 | version "5.7.1" 300 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 301 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 302 | dependencies: 303 | base64-js "^1.3.1" 304 | ieee754 "^1.1.13" 305 | 306 | bytes@3.1.2: 307 | version "3.1.2" 308 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 309 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 310 | 311 | call-bind@^1.0.0: 312 | version "1.0.2" 313 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 314 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 315 | dependencies: 316 | function-bind "^1.1.1" 317 | get-intrinsic "^1.0.2" 318 | 319 | chokidar@^3.5.1: 320 | version "3.5.3" 321 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 322 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 323 | dependencies: 324 | anymatch "~3.1.2" 325 | braces "~3.0.2" 326 | glob-parent "~5.1.2" 327 | is-binary-path "~2.1.0" 328 | is-glob "~4.0.1" 329 | normalize-path "~3.0.0" 330 | readdirp "~3.6.0" 331 | optionalDependencies: 332 | fsevents "~2.3.2" 333 | 334 | concat-map@0.0.1: 335 | version "0.0.1" 336 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 337 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 338 | 339 | connect-mongo@*, connect-mongo@^4.4.1: 340 | version "4.6.0" 341 | resolved "https://registry.yarnpkg.com/connect-mongo/-/connect-mongo-4.6.0.tgz#1bf62868efc9f28ecf1459ae9a9d6caaf90ae8a6" 342 | integrity sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg== 343 | dependencies: 344 | debug "^4.3.1" 345 | kruptein "^3.0.0" 346 | 347 | content-disposition@0.5.4: 348 | version "0.5.4" 349 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 350 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 351 | dependencies: 352 | safe-buffer "5.2.1" 353 | 354 | content-type@~1.0.4: 355 | version "1.0.4" 356 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 357 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 358 | 359 | cookie-parser@^1.4.5: 360 | version "1.4.6" 361 | resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" 362 | integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== 363 | dependencies: 364 | cookie "0.4.1" 365 | cookie-signature "1.0.6" 366 | 367 | cookie-signature@1.0.6: 368 | version "1.0.6" 369 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 370 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 371 | 372 | cookie@0.4.1: 373 | version "0.4.1" 374 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" 375 | integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== 376 | 377 | cookie@0.4.2: 378 | version "0.4.2" 379 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" 380 | integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== 381 | 382 | cookie@0.5.0: 383 | version "0.5.0" 384 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 385 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 386 | 387 | cors@^2.8.5: 388 | version "2.8.5" 389 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 390 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 391 | dependencies: 392 | object-assign "^4" 393 | vary "^1" 394 | 395 | create-require@^1.1.0: 396 | version "1.1.1" 397 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 398 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 399 | 400 | debug@2.6.9: 401 | version "2.6.9" 402 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 403 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 404 | dependencies: 405 | ms "2.0.0" 406 | 407 | debug@^4.3.1: 408 | version "4.3.4" 409 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 410 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 411 | dependencies: 412 | ms "2.1.2" 413 | 414 | denque@^2.0.1: 415 | version "2.0.1" 416 | resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a" 417 | integrity sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ== 418 | 419 | depd@2.0.0, depd@~2.0.0: 420 | version "2.0.0" 421 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 422 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 423 | 424 | destroy@1.2.0: 425 | version "1.2.0" 426 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 427 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 428 | 429 | diff@^4.0.1: 430 | version "4.0.2" 431 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 432 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 433 | 434 | dotenv@^10.0.0: 435 | version "10.0.0" 436 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" 437 | integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== 438 | 439 | dynamic-dedupe@^0.3.0: 440 | version "0.3.0" 441 | resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" 442 | integrity sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ== 443 | dependencies: 444 | xtend "^4.0.0" 445 | 446 | ee-first@1.1.1: 447 | version "1.1.1" 448 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 449 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 450 | 451 | encodeurl@~1.0.2: 452 | version "1.0.2" 453 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 454 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 455 | 456 | escape-html@~1.0.3: 457 | version "1.0.3" 458 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 459 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 460 | 461 | etag@~1.8.1: 462 | version "1.8.1" 463 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 464 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 465 | 466 | express-session@^1.17.2: 467 | version "1.17.3" 468 | resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.3.tgz#14b997a15ed43e5949cb1d073725675dd2777f36" 469 | integrity sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw== 470 | dependencies: 471 | cookie "0.4.2" 472 | cookie-signature "1.0.6" 473 | debug "2.6.9" 474 | depd "~2.0.0" 475 | on-headers "~1.0.2" 476 | parseurl "~1.3.3" 477 | safe-buffer "5.2.1" 478 | uid-safe "~2.1.5" 479 | 480 | express@^4.17.1: 481 | version "4.18.1" 482 | resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" 483 | integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== 484 | dependencies: 485 | accepts "~1.3.8" 486 | array-flatten "1.1.1" 487 | body-parser "1.20.0" 488 | content-disposition "0.5.4" 489 | content-type "~1.0.4" 490 | cookie "0.5.0" 491 | cookie-signature "1.0.6" 492 | debug "2.6.9" 493 | depd "2.0.0" 494 | encodeurl "~1.0.2" 495 | escape-html "~1.0.3" 496 | etag "~1.8.1" 497 | finalhandler "1.2.0" 498 | fresh "0.5.2" 499 | http-errors "2.0.0" 500 | merge-descriptors "1.0.1" 501 | methods "~1.1.2" 502 | on-finished "2.4.1" 503 | parseurl "~1.3.3" 504 | path-to-regexp "0.1.7" 505 | proxy-addr "~2.0.7" 506 | qs "6.10.3" 507 | range-parser "~1.2.1" 508 | safe-buffer "5.2.1" 509 | send "0.18.0" 510 | serve-static "1.15.0" 511 | setprototypeof "1.2.0" 512 | statuses "2.0.1" 513 | type-is "~1.6.18" 514 | utils-merge "1.0.1" 515 | vary "~1.1.2" 516 | 517 | fill-range@^7.0.1: 518 | version "7.0.1" 519 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 520 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 521 | dependencies: 522 | to-regex-range "^5.0.1" 523 | 524 | finalhandler@1.2.0: 525 | version "1.2.0" 526 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 527 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 528 | dependencies: 529 | debug "2.6.9" 530 | encodeurl "~1.0.2" 531 | escape-html "~1.0.3" 532 | on-finished "2.4.1" 533 | parseurl "~1.3.3" 534 | statuses "2.0.1" 535 | unpipe "~1.0.0" 536 | 537 | follow-redirects@^1.14.0: 538 | version "1.15.1" 539 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" 540 | integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== 541 | 542 | forwarded@0.2.0: 543 | version "0.2.0" 544 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 545 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 546 | 547 | fresh@0.5.2: 548 | version "0.5.2" 549 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 550 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 551 | 552 | fs.realpath@^1.0.0: 553 | version "1.0.0" 554 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 555 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 556 | 557 | fsevents@~2.3.2: 558 | version "2.3.2" 559 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 560 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 561 | 562 | function-bind@^1.1.1: 563 | version "1.1.1" 564 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 565 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 566 | 567 | get-intrinsic@^1.0.2: 568 | version "1.1.2" 569 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" 570 | integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== 571 | dependencies: 572 | function-bind "^1.1.1" 573 | has "^1.0.3" 574 | has-symbols "^1.0.3" 575 | 576 | glob-parent@~5.1.2: 577 | version "5.1.2" 578 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 579 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 580 | dependencies: 581 | is-glob "^4.0.1" 582 | 583 | glob@^7.1.3: 584 | version "7.2.3" 585 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 586 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 587 | dependencies: 588 | fs.realpath "^1.0.0" 589 | inflight "^1.0.4" 590 | inherits "2" 591 | minimatch "^3.1.1" 592 | once "^1.3.0" 593 | path-is-absolute "^1.0.0" 594 | 595 | has-symbols@^1.0.3: 596 | version "1.0.3" 597 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 598 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 599 | 600 | has@^1.0.3: 601 | version "1.0.3" 602 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 603 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 604 | dependencies: 605 | function-bind "^1.1.1" 606 | 607 | http-errors@2.0.0: 608 | version "2.0.0" 609 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 610 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 611 | dependencies: 612 | depd "2.0.0" 613 | inherits "2.0.4" 614 | setprototypeof "1.2.0" 615 | statuses "2.0.1" 616 | toidentifier "1.0.1" 617 | 618 | iconv-lite@0.4.24: 619 | version "0.4.24" 620 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 621 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 622 | dependencies: 623 | safer-buffer ">= 2.1.2 < 3" 624 | 625 | ieee754@^1.1.13: 626 | version "1.2.1" 627 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 628 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 629 | 630 | inflight@^1.0.4: 631 | version "1.0.6" 632 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 633 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 634 | dependencies: 635 | once "^1.3.0" 636 | wrappy "1" 637 | 638 | inherits@2, inherits@2.0.4, inherits@^2.0.1: 639 | version "2.0.4" 640 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 641 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 642 | 643 | ip@^1.1.5: 644 | version "1.1.8" 645 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" 646 | integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== 647 | 648 | ipaddr.js@1.9.1: 649 | version "1.9.1" 650 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 651 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 652 | 653 | is-binary-path@~2.1.0: 654 | version "2.1.0" 655 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 656 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 657 | dependencies: 658 | binary-extensions "^2.0.0" 659 | 660 | is-core-module@^2.9.0: 661 | version "2.9.0" 662 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" 663 | integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== 664 | dependencies: 665 | has "^1.0.3" 666 | 667 | is-extglob@^2.1.1: 668 | version "2.1.1" 669 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 670 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 671 | 672 | is-glob@^4.0.1, is-glob@~4.0.1: 673 | version "4.0.3" 674 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 675 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 676 | dependencies: 677 | is-extglob "^2.1.1" 678 | 679 | is-number@^7.0.0: 680 | version "7.0.0" 681 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 682 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 683 | 684 | kruptein@^3.0.0: 685 | version "3.0.4" 686 | resolved "https://registry.yarnpkg.com/kruptein/-/kruptein-3.0.4.tgz#68dcd32ee9e6b611c86615a4616fee2ca3b9b769" 687 | integrity sha512-614v+4fgOkcw98lI7rMO9HZ+Y2cK6MGYcR/NSVhRXcClUb72LTAf2NibAh8CKSjalY81rfrrjLQgb8TW9RP03Q== 688 | dependencies: 689 | asn1.js "^5.4.1" 690 | 691 | make-error@^1.1.1: 692 | version "1.3.6" 693 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 694 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 695 | 696 | media-typer@0.3.0: 697 | version "0.3.0" 698 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 699 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 700 | 701 | memory-pager@^1.0.2: 702 | version "1.5.0" 703 | resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" 704 | integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== 705 | 706 | merge-descriptors@1.0.1: 707 | version "1.0.1" 708 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 709 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 710 | 711 | methods@~1.1.2: 712 | version "1.1.2" 713 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 714 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 715 | 716 | mime-db@1.52.0: 717 | version "1.52.0" 718 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 719 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 720 | 721 | mime-types@~2.1.24, mime-types@~2.1.34: 722 | version "2.1.35" 723 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 724 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 725 | dependencies: 726 | mime-db "1.52.0" 727 | 728 | mime@1.6.0: 729 | version "1.6.0" 730 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 731 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 732 | 733 | minimalistic-assert@^1.0.0: 734 | version "1.0.1" 735 | resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" 736 | integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== 737 | 738 | minimatch@^3.1.1: 739 | version "3.1.2" 740 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 741 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 742 | dependencies: 743 | brace-expansion "^1.1.7" 744 | 745 | minimist@^1.2.6: 746 | version "1.2.6" 747 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" 748 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== 749 | 750 | mkdirp@^1.0.4: 751 | version "1.0.4" 752 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" 753 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 754 | 755 | mongodb-connection-string-url@^2.5.2: 756 | version "2.5.2" 757 | resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.2.tgz#f075c8d529e8d3916386018b8a396aed4f16e5ed" 758 | integrity sha512-tWDyIG8cQlI5k3skB6ywaEA5F9f5OntrKKsT/Lteub2zgwSUlhqEN2inGgBTm8bpYJf8QYBdA/5naz65XDpczA== 759 | dependencies: 760 | "@types/whatwg-url" "^8.2.1" 761 | whatwg-url "^11.0.0" 762 | 763 | mongodb@^4.0.0: 764 | version "4.7.0" 765 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.7.0.tgz#99f7323271d93659067695b60e7b4efee2de9bf0" 766 | integrity sha512-HhVar6hsUeMAVlIbwQwWtV36iyjKd9qdhY+s4wcU8K6TOj4Q331iiMy+FoPuxEntDIijTYWivwFJkLv8q/ZgvA== 767 | dependencies: 768 | bson "^4.6.3" 769 | denque "^2.0.1" 770 | mongodb-connection-string-url "^2.5.2" 771 | socks "^2.6.2" 772 | optionalDependencies: 773 | saslprep "^1.0.3" 774 | 775 | morgan@^1.10.0: 776 | version "1.10.0" 777 | resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" 778 | integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== 779 | dependencies: 780 | basic-auth "~2.0.1" 781 | debug "2.6.9" 782 | depd "~2.0.0" 783 | on-finished "~2.3.0" 784 | on-headers "~1.0.2" 785 | 786 | ms@2.0.0: 787 | version "2.0.0" 788 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 789 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 790 | 791 | ms@2.1.2: 792 | version "2.1.2" 793 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 794 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 795 | 796 | ms@2.1.3: 797 | version "2.1.3" 798 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 799 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 800 | 801 | negotiator@0.6.3: 802 | version "0.6.3" 803 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 804 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 805 | 806 | normalize-path@^3.0.0, normalize-path@~3.0.0: 807 | version "3.0.0" 808 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 809 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 810 | 811 | object-assign@^4: 812 | version "4.1.1" 813 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 814 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 815 | 816 | object-inspect@^1.9.0: 817 | version "1.12.2" 818 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" 819 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== 820 | 821 | on-finished@2.4.1: 822 | version "2.4.1" 823 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 824 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 825 | dependencies: 826 | ee-first "1.1.1" 827 | 828 | on-finished@~2.3.0: 829 | version "2.3.0" 830 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 831 | integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== 832 | dependencies: 833 | ee-first "1.1.1" 834 | 835 | on-headers@~1.0.2: 836 | version "1.0.2" 837 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" 838 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 839 | 840 | once@^1.3.0: 841 | version "1.4.0" 842 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 843 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 844 | dependencies: 845 | wrappy "1" 846 | 847 | parseurl@~1.3.3: 848 | version "1.3.3" 849 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 850 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 851 | 852 | path-is-absolute@^1.0.0: 853 | version "1.0.1" 854 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 855 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 856 | 857 | path-parse@^1.0.7: 858 | version "1.0.7" 859 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 860 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 861 | 862 | path-to-regexp@0.1.7: 863 | version "0.1.7" 864 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 865 | integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 866 | 867 | picomatch@^2.0.4, picomatch@^2.2.1: 868 | version "2.3.1" 869 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 870 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 871 | 872 | proxy-addr@~2.0.7: 873 | version "2.0.7" 874 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 875 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 876 | dependencies: 877 | forwarded "0.2.0" 878 | ipaddr.js "1.9.1" 879 | 880 | punycode@^2.1.1: 881 | version "2.1.1" 882 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 883 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 884 | 885 | qs@6.10.3: 886 | version "6.10.3" 887 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" 888 | integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== 889 | dependencies: 890 | side-channel "^1.0.4" 891 | 892 | random-bytes@~1.0.0: 893 | version "1.0.0" 894 | resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" 895 | integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== 896 | 897 | range-parser@~1.2.1: 898 | version "1.2.1" 899 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 900 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 901 | 902 | raw-body@2.5.1: 903 | version "2.5.1" 904 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" 905 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 906 | dependencies: 907 | bytes "3.1.2" 908 | http-errors "2.0.0" 909 | iconv-lite "0.4.24" 910 | unpipe "1.0.0" 911 | 912 | readdirp@~3.6.0: 913 | version "3.6.0" 914 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 915 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 916 | dependencies: 917 | picomatch "^2.2.1" 918 | 919 | resolve@^1.0.0: 920 | version "1.22.1" 921 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" 922 | integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== 923 | dependencies: 924 | is-core-module "^2.9.0" 925 | path-parse "^1.0.7" 926 | supports-preserve-symlinks-flag "^1.0.0" 927 | 928 | rimraf@^2.6.1: 929 | version "2.7.1" 930 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" 931 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== 932 | dependencies: 933 | glob "^7.1.3" 934 | 935 | safe-buffer@5.1.2: 936 | version "5.1.2" 937 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 938 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 939 | 940 | safe-buffer@5.2.1: 941 | version "5.2.1" 942 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 943 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 944 | 945 | "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: 946 | version "2.1.2" 947 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 948 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 949 | 950 | saslprep@^1.0.3: 951 | version "1.0.3" 952 | resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" 953 | integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== 954 | dependencies: 955 | sparse-bitfield "^3.0.3" 956 | 957 | send@0.18.0: 958 | version "0.18.0" 959 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 960 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 961 | dependencies: 962 | debug "2.6.9" 963 | depd "2.0.0" 964 | destroy "1.2.0" 965 | encodeurl "~1.0.2" 966 | escape-html "~1.0.3" 967 | etag "~1.8.1" 968 | fresh "0.5.2" 969 | http-errors "2.0.0" 970 | mime "1.6.0" 971 | ms "2.1.3" 972 | on-finished "2.4.1" 973 | range-parser "~1.2.1" 974 | statuses "2.0.1" 975 | 976 | serve-static@1.15.0: 977 | version "1.15.0" 978 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 979 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 980 | dependencies: 981 | encodeurl "~1.0.2" 982 | escape-html "~1.0.3" 983 | parseurl "~1.3.3" 984 | send "0.18.0" 985 | 986 | setprototypeof@1.2.0: 987 | version "1.2.0" 988 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 989 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 990 | 991 | side-channel@^1.0.4: 992 | version "1.0.4" 993 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 994 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 995 | dependencies: 996 | call-bind "^1.0.0" 997 | get-intrinsic "^1.0.2" 998 | object-inspect "^1.9.0" 999 | 1000 | smart-buffer@^4.2.0: 1001 | version "4.2.0" 1002 | resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" 1003 | integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== 1004 | 1005 | socks@^2.6.2: 1006 | version "2.6.2" 1007 | resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.2.tgz#ec042d7960073d40d94268ff3bb727dc685f111a" 1008 | integrity sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA== 1009 | dependencies: 1010 | ip "^1.1.5" 1011 | smart-buffer "^4.2.0" 1012 | 1013 | source-map-support@^0.5.12: 1014 | version "0.5.21" 1015 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 1016 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 1017 | dependencies: 1018 | buffer-from "^1.0.0" 1019 | source-map "^0.6.0" 1020 | 1021 | source-map@^0.6.0: 1022 | version "0.6.1" 1023 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 1024 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 1025 | 1026 | sparse-bitfield@^3.0.3: 1027 | version "3.0.3" 1028 | resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" 1029 | integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== 1030 | dependencies: 1031 | memory-pager "^1.0.2" 1032 | 1033 | statuses@2.0.1: 1034 | version "2.0.1" 1035 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 1036 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1037 | 1038 | strip-bom@^3.0.0: 1039 | version "3.0.0" 1040 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 1041 | integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== 1042 | 1043 | strip-json-comments@^2.0.0: 1044 | version "2.0.1" 1045 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1046 | integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== 1047 | 1048 | supports-preserve-symlinks-flag@^1.0.0: 1049 | version "1.0.0" 1050 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 1051 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 1052 | 1053 | to-regex-range@^5.0.1: 1054 | version "5.0.1" 1055 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 1056 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 1057 | dependencies: 1058 | is-number "^7.0.0" 1059 | 1060 | toidentifier@1.0.1: 1061 | version "1.0.1" 1062 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 1063 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1064 | 1065 | tr46@^3.0.0: 1066 | version "3.0.0" 1067 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" 1068 | integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== 1069 | dependencies: 1070 | punycode "^2.1.1" 1071 | 1072 | tree-kill@^1.2.2: 1073 | version "1.2.2" 1074 | resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" 1075 | integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== 1076 | 1077 | ts-node-dev@^2.0.0: 1078 | version "2.0.0" 1079 | resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065" 1080 | integrity sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w== 1081 | dependencies: 1082 | chokidar "^3.5.1" 1083 | dynamic-dedupe "^0.3.0" 1084 | minimist "^1.2.6" 1085 | mkdirp "^1.0.4" 1086 | resolve "^1.0.0" 1087 | rimraf "^2.6.1" 1088 | source-map-support "^0.5.12" 1089 | tree-kill "^1.2.2" 1090 | ts-node "^10.4.0" 1091 | tsconfig "^7.0.0" 1092 | 1093 | ts-node@^10.4.0: 1094 | version "10.8.1" 1095 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.1.tgz#ea2bd3459011b52699d7e88daa55a45a1af4f066" 1096 | integrity sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g== 1097 | dependencies: 1098 | "@cspotcode/source-map-support" "^0.8.0" 1099 | "@tsconfig/node10" "^1.0.7" 1100 | "@tsconfig/node12" "^1.0.7" 1101 | "@tsconfig/node14" "^1.0.0" 1102 | "@tsconfig/node16" "^1.0.2" 1103 | acorn "^8.4.1" 1104 | acorn-walk "^8.1.1" 1105 | arg "^4.1.0" 1106 | create-require "^1.1.0" 1107 | diff "^4.0.1" 1108 | make-error "^1.1.1" 1109 | v8-compile-cache-lib "^3.0.1" 1110 | yn "3.1.1" 1111 | 1112 | tsconfig@^7.0.0: 1113 | version "7.0.0" 1114 | resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" 1115 | integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== 1116 | dependencies: 1117 | "@types/strip-bom" "^3.0.0" 1118 | "@types/strip-json-comments" "0.0.30" 1119 | strip-bom "^3.0.0" 1120 | strip-json-comments "^2.0.0" 1121 | 1122 | type-is@~1.6.18: 1123 | version "1.6.18" 1124 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1125 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1126 | dependencies: 1127 | media-typer "0.3.0" 1128 | mime-types "~2.1.24" 1129 | 1130 | typescript@^4.7.4: 1131 | version "4.7.4" 1132 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" 1133 | integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== 1134 | 1135 | uid-safe@~2.1.5: 1136 | version "2.1.5" 1137 | resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" 1138 | integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== 1139 | dependencies: 1140 | random-bytes "~1.0.0" 1141 | 1142 | unpipe@1.0.0, unpipe@~1.0.0: 1143 | version "1.0.0" 1144 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1145 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1146 | 1147 | utils-merge@1.0.1: 1148 | version "1.0.1" 1149 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1150 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1151 | 1152 | v8-compile-cache-lib@^3.0.1: 1153 | version "3.0.1" 1154 | resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" 1155 | integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== 1156 | 1157 | vary@^1, vary@~1.1.2: 1158 | version "1.1.2" 1159 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1160 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1161 | 1162 | webidl-conversions@^7.0.0: 1163 | version "7.0.0" 1164 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" 1165 | integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== 1166 | 1167 | whatwg-url@^11.0.0: 1168 | version "11.0.0" 1169 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" 1170 | integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== 1171 | dependencies: 1172 | tr46 "^3.0.0" 1173 | webidl-conversions "^7.0.0" 1174 | 1175 | wrappy@1: 1176 | version "1.0.2" 1177 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1178 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 1179 | 1180 | xtend@^4.0.0: 1181 | version "4.0.2" 1182 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 1183 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 1184 | 1185 | yn@3.1.1: 1186 | version "3.1.1" 1187 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 1188 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 1189 | --------------------------------------------------------------------------------