├── .env.example
├── .gitignore
├── .idea
├── .gitignore
├── helpqueue.iml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .vscode
└── settings.json
├── CONTRIBUTING.md
├── HOWTHISWASMADE.md
├── LICENSE
├── Procfile
├── README.md
├── app.json
├── client
├── .gitignore
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── AppHeader.tsx
│ ├── Types.tsx
│ ├── components
│ │ ├── AdminPage.tsx
│ │ ├── Alert.tsx
│ │ ├── FAQPage.tsx
│ │ ├── LandingPage.tsx
│ │ ├── LoginCallback.tsx
│ │ ├── LoginGithub.tsx
│ │ ├── ProfilePage.tsx
│ │ ├── QueueMentor.tsx
│ │ ├── QueueRequest.tsx
│ │ ├── ServerHelper.tsx
│ │ ├── Types.tsx
│ │ └── info.md
│ ├── hooks
│ │ ├── info.md
│ │ ├── useLogin.tsx
│ │ └── useViewer.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ └── serviceWorker.ts
├── tsconfig.json
└── yarn.lock
├── docs
└── img
│ ├── admin.png
│ ├── mentor.png
│ └── opening.png
├── manage.py
├── migrations
├── README
├── alembic.ini
├── env.py
├── script.py.mako
└── versions
│ ├── 653093fddc44_.py
│ ├── 68d234e0f83e_.py
│ ├── 8a8e177c22c8_.py
│ ├── 8d950e758485_.py
│ ├── a9fd5b5f5b0a_.py
│ ├── de3ab01f4eb0_.py
│ └── eef69a932db1_.py
├── package.json
├── prebuild.py
├── requirements.txt
├── run_dev_server.py
├── run_server.py
├── server
├── __init__.py
├── api
│ └── v1
│ │ ├── __init__.py
│ │ ├── api.py
│ │ ├── api_admin.py
│ │ ├── api_login.py
│ │ ├── api_tickets.py
│ │ ├── api_user.py
│ │ └── info.md
├── app.py
├── cache.py
├── controllers
│ ├── authentication.py
│ ├── cron.py
│ ├── dopeauth.py
│ ├── info.md
│ ├── settings.py
│ ├── tickets.py
│ └── users.py
├── helpers.py
├── models
│ ├── __init__.py
│ ├── client.py
│ ├── setting.py
│ ├── ticket.py
│ └── user.py
└── server_constants.py
├── update_and_deploy.sh
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | DEBUG=True
2 | DEVELOPMENT=True
3 | SECRET_KEY='SECRET'
4 | SQLALCHEMY_DATABASE_URI="postgresql://localhost/helpq"
5 | SQLALCHEMY_TRACK_MODIFICATIONS="False"
6 | FLASK_ENV="developement"
7 | PORT=3000
8 |
9 | MASTER_EMAIL="kevin21@mit.edu"
10 | REACT_APP_SITEURL="http://localhost:3000"
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | env/
2 | .env
3 | **/__pycache__/
4 | service_account.json
5 | env.sh
6 |
7 |
8 | /node_modules
9 | /server/build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/helpqueue.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "/Users/kevin/.pyenv/shims/python"
3 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to HelpLIFO
2 | =====================
3 |
4 | Thank you for using HelpLIFO, and we're really excited that you want to help out!
5 |
6 | Right now the code base is still not docstringed out yet so fun!
7 |
8 | Pull Requests
9 | -------------
10 |
11 | We actively welcome your pull requests!
12 |
13 | However, before you begin, please create an issue so we can determine if the work, feature, or bugfix either has someone already working on it as a part of the current roadmap, or if the feature is not something that would belong in the HELPq as a core feature.
14 |
15 | This is mostly meant for discussion, so we can discuss things fully and make a really awesome product :)
16 |
17 | Issues
18 | ------
19 |
20 | We use issues to track public bugs. Please make sure your description is clear and has sufficient instructions to be able to reproduce the issue.
21 |
22 | We also use issues to track feature requests and discuss. Please mark your issue as 'Feature Request' in this case!
23 |
24 |
25 | Coding Style
26 | ------------
27 |
28 | Keep it clean, and keep things modular!
--------------------------------------------------------------------------------
/HOWTHISWASMADE.md:
--------------------------------------------------------------------------------
1 | `create-react-app client --typescript`
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Kevin Fang (TechX)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run gunicorn
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Help Queue
2 |
3 | The easy to use help queue system, built on top of Flask + React!
4 |
5 | Created and maintained by: [TheReddKing](mailto:kevin21@mit.edu) ([TechX](https://techx.io))
6 |
7 |
8 | ### Heroku Deploy
9 |
10 | [](https://heroku.com/deploy?template=https://github.com/techx/helpqueue/tree/master)
11 |
12 | There are two variables you need to set the master email account and also the URL of the site. These must be edited as heroku env variables.
13 | All other settings can be edited on the `/admin` page
14 |
15 | ### Screenshots
16 |
17 | 
18 | 
19 | 
20 |
21 | ## Dev:
22 | ### Local Installation:
23 |
24 | python -m venv env
25 | source env/bin/activate
26 | pip install --upgrade pip
27 | pip install -r requirements.txt
28 | yarn
29 | cp .env.example .env
30 | cd client && yarn
31 |
32 | Then edit your `.env` file. Once your database url is correct (you can use `createdb helpq` if you have postgres)
33 |
34 | python manage.py db upgrade
35 |
36 | [Windows might mess up things click here to fix](https://stackoverflow.com/questions/18664074/getting-error-peer-authentication-failed-for-user-postgres-when-trying-to-ge)
37 |
38 | ### Dev run
39 |
40 | yarn run dev
41 |
42 | or (if you want to debug server side scripts or if u are on windows)
43 |
44 | yarn start
45 | yarn run dev-server
46 |
47 | ### Modifying for events
48 |
49 | See [FILESTRUCTURE.md](FILESTRUCTURE.md).
50 |
51 | Contributing
52 | ------------
53 |
54 | I'd love to take pull requests! Please read the [Contributing Guide](CONTRIBUTING.md) first!
55 |
56 | Other TODOs:
57 | - App Success Deploy
58 | {
59 | "success_url": "/welcome"
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TechX Help Queue",
3 | "description": "A simple help queue interface for anything!",
4 | "repository": "https://github.com/techx/helpqueue",
5 | "keywords": ["helpq", "help queue"],
6 | "addons": [
7 | {
8 | "plan": "heroku-postgresql",
9 | "options": {
10 | "version": "12"
11 | }
12 | }
13 | ],
14 | "buildpacks": [
15 | {
16 | "url": "heroku/python"
17 | },
18 | {
19 | "url": "heroku/nodejs"
20 | }
21 | ],
22 | "env": {
23 | "REACT_APP_SITEURL": {
24 | "description": "URL of site (usually https://[appname].herokuapp.com) YOU NEED https or http",
25 | "value": "https://[appname].herokuapp.com"
26 | },
27 | "MASTER_EMAIL": {
28 | "description": "Email of the default ADMIN user (prevents their admin rights from being revoked)",
29 | "value": "kevin21@mit.edu"
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/client/.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 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "helplifo",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.26",
7 | "@fortawesome/free-solid-svg-icons": "^5.12.0",
8 | "@fortawesome/react-fontawesome": "^0.1.8",
9 | "@types/jest": "24.0.16",
10 | "@types/node": "12.6.9",
11 | "@types/react": "16.8.24",
12 | "@types/react-dom": "16.8.5",
13 | "@types/react-router": "^5.0.3",
14 | "@types/react-router-dom": "^4.3.4",
15 | "@types/react-s-alert": "^1.3.2",
16 | "@types/react-table": "^6.8.5",
17 | "@types/react-tagsinput": "^3.19.6",
18 | "@types/reactstrap": "^8.0.1",
19 | "@types/styled-components": "^4.4.1",
20 | "bootstrap": "^4.3.1",
21 | "concurrently": "^4.1.1",
22 | "dotenv": "^8.0.0",
23 | "jquery": "^3.4.1",
24 | "jsoneditor": "^8.0.0",
25 | "jsoneditor-react": "^2.0.0",
26 | "popper.js": "^1.15.0",
27 | "prop-types": "^15.7.2",
28 | "rc-rate": "^2.5.0",
29 | "react": "^16.8.6",
30 | "react-cookie": "^4.0.1",
31 | "react-dom": "^16.8.6",
32 | "react-router": "^5.0.1",
33 | "react-router-dom": "^5.0.1",
34 | "react-s-alert": "^1.4.1",
35 | "react-scripts": "^3.0.1",
36 | "react-table": "^7.0.0-rc.10",
37 | "react-table-6": "^6.11.0",
38 | "react-tagsinput": "^3.19.0",
39 | "reactstrap": "^8.0.1",
40 | "semantic-ui-react": "^0.88.2",
41 | "styled-components": "^4.4.1",
42 | "typescript": "3.5.3"
43 | },
44 | "eslintConfig": {
45 | "extends": "react-app"
46 | },
47 | "scripts": {
48 | "start": "react-scripts start",
49 | "build": "react-scripts build",
50 | "test": "react-scripts test",
51 | "eject": "react-scripts eject"
52 | },
53 | "proxy": "http://localhost:5000",
54 | "browserslist": {
55 | "production": [
56 | ">0.2%",
57 | "not dead",
58 | "not op_mini all"
59 | ],
60 | "development": [
61 | "last 1 chrome version",
62 | "last 1 firefox version",
63 | "last 1 safari version"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techx/helpqueue/f524f1bc6ad9675d5a8f80a24bca89b27814beb1/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/client/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 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 |
6 | .App-logo {
7 | animation: App-logo-spin infinite 20s linear;
8 | height: 40vmin;
9 | pointer-events: none;
10 | }
11 |
12 | .App-header {
13 | background-color: #1B4481;
14 | min-height: 100vh;
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | justify-content: center;
19 | font-size: calc(10px + 2vmin);
20 | color: white;
21 | }
22 |
23 | .App-link {
24 | color: #61dafb;
25 | }
26 |
27 | @keyframes App-logo-spin {
28 | from {
29 | transform: rotate(0deg);
30 | }
31 | to {
32 | transform: rotate(360deg);
33 | }
34 | }
35 |
36 | .App .ui.card {
37 | width: 100%;
38 | margin: auto;
39 | box-shadow: 0px 8px 20px -10px rgba(13, 28, 39, 0.6);
40 | background: #fff;
41 | border-radius: 15px;
42 | max-width: 700px;
43 | position: relative;
44 | margin-top: 2px;
45 | margin-bottom: 2px;
46 | padding: 1em;
47 | }
48 |
49 | .App .ui.card h2:after {
50 | margin-top: .1em;
51 | margin-bottom: .1em;
52 | content:' ';
53 | display:block;
54 | border:2px solid #001D55;
55 | border-radius:4px;
56 | -webkit-border-radius:4px;
57 | -moz-border-radius:4px;
58 | box-shadow:inset 0 1px 1px rgba(0, 0, 0, .05);
59 | -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .05);
60 | -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .05);
61 | }
62 |
63 | .App .ui.card .ui.card {
64 | box-shadow: 0px 0px 0px 0px white;
65 | border: 1px solid gray;
66 | }
67 |
68 | .App .card .card .card-title:after {
69 | border:0px solid#3AA5B0;
70 | }
71 | .App .noshadow .card {
72 | box-shadow: 0;
73 | }
74 |
75 | .ui.label.label{
76 | background-color: #FFDF3F
77 | }
78 |
79 | /* FAQ PAGE STYLING */
80 |
81 | .App .accordion {
82 | text-align:left;
83 | }
84 |
--------------------------------------------------------------------------------
/client/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from "react";
2 | import "./App.css";
3 | import { CookiesProvider } from "react-cookie";
4 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
5 |
6 | import AppHeader from "./AppHeader";
7 | import LandingPage from "./components/LandingPage";
8 | import LoginCallback from "./components/LoginCallback";
9 | import LoginGithub from "./components/LoginGithub";
10 | import QueueRequest from "./components/QueueRequest";
11 | import QueueMentor from "./components/QueueMentor";
12 | import AdminPage from "./components/AdminPage";
13 | import ProfilePage from "./components/ProfilePage";
14 | import FAQPage from "./components/FAQPage";
15 |
16 | import Alert from "react-s-alert";
17 |
18 | import "react-s-alert/dist/s-alert-default.css";
19 | import "react-s-alert/dist/s-alert-css-effects/slide.css";
20 |
21 | const App: React.FC = () => {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default App;
47 |
--------------------------------------------------------------------------------
/client/src/AppHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import {
3 | Collapse,
4 | Navbar,
5 | NavbarToggler,
6 | NavbarBrand,
7 | Nav,
8 | NavItem,
9 | NavLink,
10 | UncontrolledDropdown,
11 | DropdownToggle,
12 | DropdownMenu,
13 | DropdownItem,
14 | Button
15 | } from "reactstrap";
16 | import useLogin from "./hooks/useLogin";
17 | import useViewer, { Query } from "./hooks/useViewer";
18 | import ServerHelper, { ServerURL } from "./components/ServerHelper";
19 | import { useCookies } from "react-cookie";
20 | import { ClientSettings } from "./components/Types";
21 |
22 | const AppHeader: React.FC = () => {
23 | const [isOpen, setIsOpen] = React.useState(false);
24 | const [cookies, setCookies] = useCookies(["settings"]);
25 | const settings: ClientSettings | null = cookies["settings"]
26 | ? cookies["settings"]
27 | : null;
28 | const { viewer, isLoggedIn } = useViewer();
29 | const { logout } = useLogin();
30 |
31 | const getSettings = async () => {
32 | const res = await ServerHelper.post(ServerURL.settings, {});
33 | if (res.success) {
34 | setCookies("settings", res.settings, { path: "/" });
35 | }
36 | };
37 |
38 | document.title = (settings ? settings.app_name : "") + " HelpLIFO";
39 | useEffect(() => {
40 | getSettings();
41 | }, []);
42 |
43 | const viewerButton = isLoggedIn ? (
44 |
45 |
46 | Hello {viewer(Query.name)}
47 |
48 |
49 |
50 | (window.location.href = "/profile")}>
51 | Profile
52 |
53 |
54 | Logout
55 |
56 |
57 | ) : null;
58 | return (
59 |
60 |
65 |
66 | {settings ? settings.app_name : null}
67 |
68 | setIsOpen(open => !open)} />
69 |
70 |
71 |
72 |
73 | FAQ
74 |
75 |
76 |
77 |
84 | Contact Us
85 |
86 |
87 | {viewerButton}
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export default AppHeader;
97 |
--------------------------------------------------------------------------------
/client/src/Types.tsx:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/client/src/components/AdminPage.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { JsonEditor as Editor } from "jsoneditor-react";
3 | // TODO(kevinfang): Upgrade to react-table v7
4 | import ReactTable from "react-table-6";
5 | import "react-table-6/react-table.css";
6 | import "jsoneditor-react/es/editor.min.css";
7 | import React, { useState, useEffect } from "react";
8 | import useLogin from "../hooks/useLogin";
9 | import {
10 | CardBody,
11 | Card,
12 | CardTitle,
13 | Row,
14 | Col,
15 | Input,
16 | InputGroup,
17 | InputGroupAddon,
18 | InputGroupText
19 | } from "reactstrap";
20 | import { Button, Tab } from "semantic-ui-react";
21 | import ServerHelper, { ServerURL } from "./ServerHelper";
22 | import createAlert, { AlertType } from "./Alert";
23 | import { User, ClientSettings, AdminStats } from "./Types";
24 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
25 | import { faStar } from "@fortawesome/free-solid-svg-icons";
26 |
27 | const AdminPage = () => {
28 | document.body.classList.add("white");
29 | const { getCredentials, logout } = useLogin();
30 | const [settingsJSON, setSettingsJSON] = useState(null);
31 | const [adminStats, setAdminStats] = useState(null);
32 | const [data, setData] = useState([]);
33 | const [filteredData, setFilteredData] = useState([]);
34 | const [searchValue, setSearchValue] = useState("");
35 |
36 | const promote = async (userID: string, type: string, value: boolean) => {
37 | const res = await ServerHelper.post(ServerURL.promoteUser, {
38 | ...getCredentials(),
39 | user_id: userID,
40 | type: type,
41 | value: value ? 1 : 0
42 | });
43 | if (res.success) {
44 | setSettingsJSON(res.settings);
45 | setData(res.users);
46 | setAdminStats(res.ticket_stats);
47 | createAlert(AlertType.Success, "Updated User");
48 | } else {
49 | createAlert(AlertType.Error, "Failed to update User");
50 | }
51 | };
52 | const columns = [
53 | {
54 | Header: "Name",
55 | accessor: "name"
56 | },
57 | {
58 | Header: "Email",
59 | accessor: "email"
60 | },
61 | {
62 | Header: "Admin",
63 | accessor: "admin_is",
64 | Cell: (row: { original: User; value: string }) => (
65 | <>
66 |
68 | promote("" + row.original.id, "admin", !row.original.admin_is)
69 | }
70 | color={row.original.admin_is ? "blue" : "grey"}
71 | >
72 | admin
73 |
74 |
76 | promote("" + row.original.id, "mentor", !row.original.mentor_is)
77 | }
78 | color={row.original.mentor_is ? "blue" : "grey"}
79 | >
80 | mentor
81 |
82 | >
83 | )
84 | }
85 | ];
86 |
87 | const getData = async () => {
88 | const res = await ServerHelper.post(ServerURL.admin, getCredentials());
89 | if (res.success) {
90 | setSettingsJSON(res.settings);
91 | setData(res.users);
92 | setAdminStats(res.ticket_stats);
93 | } else {
94 | createAlert(
95 | AlertType.Error,
96 | "Failed to get admin data, are you logged in?"
97 | );
98 | }
99 | };
100 |
101 | const updateData = async () => {
102 | const res = await ServerHelper.post(ServerURL.updateAdmin, {
103 | ...getCredentials(),
104 | data: JSON.stringify(settingsJSON)
105 | });
106 | if (res.success) {
107 | setSettingsJSON(res.settings);
108 | setData(res.users);
109 | setAdminStats(res.ticket_stats);
110 | createAlert(AlertType.Success, "Updated JSON");
111 | } else {
112 | createAlert(AlertType.Error, "Failed to update JSON");
113 | }
114 | };
115 | useEffect(() => {
116 | getData();
117 | }, []);
118 | useEffect(() => {
119 | if (searchValue.length === 0) {
120 | setFilteredData(data);
121 | } else {
122 | const value = searchValue.toLowerCase();
123 | // Searchable content
124 | setFilteredData(
125 | data.filter(
126 | (obj: User) =>
127 | (obj.name && obj.name.toLowerCase().includes(value)) ||
128 | (obj.email && obj.email.toLowerCase().includes(value)) ||
129 | (obj.skills && obj.skills.toLowerCase().includes(value))
130 | )
131 | );
132 | }
133 | }, [searchValue, data]);
134 |
135 | const panes = [
136 | {
137 | menuItem: "Users",
138 | render: () => (
139 |
140 |
141 | Users
142 |
143 | setSearchValue(e.target.value)}
147 | />
148 |
149 |
150 | )
151 | },
152 | {
153 | menuItem: "Stats",
154 | render: () => (
155 |
156 |
157 | Stats
158 |
159 |
160 | Average Wait: {" "}
161 | {adminStats && (adminStats.average_wait / 60).toFixed(1)} minutes
162 |
163 | Average Claimed Time: {" "}
164 | {adminStats && (adminStats.average_claimed / 60).toFixed(1)} minutes
165 |
166 | Average Rating: {adminStats && adminStats.average_rating.toFixed(2)}
167 |
168 |
169 | )
170 | }
171 | ];
172 | return (
173 |
174 |
Admin Settings Page
175 |
176 |
177 |
178 |
179 |
180 |
181 | Main Settings
182 |
183 |
184 |
185 |
186 | Mentor Login Link:
187 |
188 | e.target.select()}
196 | readOnly
197 | />
198 |
199 |
200 |
206 | Save JSON Settings
207 |
208 | {settingsJSON ? (
209 |
210 | ) : null}
211 |
212 |
213 | queue_status (whether the queue is on or off): true or{" "}
214 | false
215 |
216 |
217 | queue_message (if you want to send a message to everyone):
218 | empty or any string
219 |
220 | mentor_password_key (password mentors use to sign in!)
221 | app_creator (org running the program)
222 | app_name (name of event)
223 | app_contact_email (contact email for question)
224 | github_client_id (client id for github)
225 | github_client_secret (client secret for github)
226 |
227 |
228 | {
230 | if (
231 | window.confirm(
232 | "Are you sure you want to remove all ticket and non-admins?"
233 | )
234 | ) {
235 | const res = await ServerHelper.post(
236 | ServerURL.resetEverything,
237 | {
238 | ...getCredentials()
239 | }
240 | );
241 | if (res.success) {
242 | createAlert(AlertType.Success, "Deleted all tickets");
243 | getData();
244 | } else {
245 | createAlert(AlertType.Error, "Something went wrong");
246 | }
247 | }
248 | }}
249 | className="col-12 my-2"
250 | color="red"
251 | >
252 | Reset all non-admin users and tickets
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | );
267 | };
268 |
269 | export default AdminPage;
270 |
--------------------------------------------------------------------------------
/client/src/components/Alert.tsx:
--------------------------------------------------------------------------------
1 | import Alert from "react-s-alert";
2 |
3 | export enum AlertType {
4 | Success,
5 | Warning,
6 | Error
7 | }
8 |
9 | const createAlert = (type: AlertType, message: string) => {
10 | switch (type) {
11 | case AlertType.Success:
12 | Alert.success(message, ALERT_SETTINGS);
13 | break;
14 | case AlertType.Warning:
15 | Alert.warning(message, ALERT_SETTINGS);
16 | break;
17 | case AlertType.Error:
18 | Alert.error(message, ALERT_SETTINGS);
19 | break;
20 | default:
21 | break;
22 | }
23 | };
24 | const ALERT_SETTINGS = {
25 | position: "top-right",
26 | effect: "slide",
27 | beep: false,
28 | timeout: 3000,
29 | offset: 50
30 | };
31 |
32 | export default createAlert;
33 |
--------------------------------------------------------------------------------
/client/src/components/FAQPage.tsx:
--------------------------------------------------------------------------------
1 | import { settings as cluster_settings } from "cluster";
2 | import React, { useState, useEffect } from "react";
3 | import { Container, Button, Input, Label, Card, Form, Accordion } from "semantic-ui-react";
4 | import useViewer from "../hooks/useViewer";
5 |
6 | const FAQPage = () => {
7 | const { settings } = useViewer();
8 | const panels = [
9 | {
10 | key: 'q1',
11 | title: {
12 | content: 'How do I sign up as a hacker?',
13 | icon: 'star',
14 | },
15 | content: [
16 | 'Hackers have two ways to sign up: the first is through GitHub and the second is through DopeAuth, which only needs an email confirmation.',
17 | ].join(' '),
18 | },
19 | {
20 | key: 'q2',
21 | title: {
22 | content: 'How do I sign up as a mentor?',
23 | icon: 'star',
24 | },
25 | content: [
26 | 'Mentors sign up using a mentor key that is shared prior to the event by an admin user of HelpQ.',
27 | ].join(' '),
28 | },
29 | {
30 | key: 'q3',
31 | title: {
32 | content: 'What constitutes a good description of an issue?',
33 | icon: 'star',
34 | },
35 | content: [
36 | 'The more specific you are with describing your issue, the more prepared mentors will be in guiding you. A good description explains what technologies, tools, or frameworks you\'re using, includes relevant context of the project, and summarizes the fixes that your team has tried to work around the bug.'
37 | ]
38 | }, {
39 | key: 'q4',
40 | title: {
41 | content: 'What to put as the event in the ticket? ',
42 | icon: 'star',
43 | },
44 | content: [
45 | 'Select the track that you plan to submit to.'
46 | ]
47 | }, {
48 | key: 'q5',
49 | title: {
50 | content: 'I have trouble finding my mentor / hacker, where should I go? ',
51 | icon: 'star',
52 | },
53 | content: [
54 | 'Please reach out in the slack to hackers or mentors. If you are still having difficulties please reach out to a team member at blueprint@hackmit.org.'
55 | ]
56 | }, {
57 | key: 'q6',
58 | title: {
59 | content: 'What if I have more questions or need help using helpq? ',
60 | icon: 'star',
61 | },
62 | content: [
63 | 'Reach out to us in the slack or send us an email' +
64 | ((settings == null) ? '!' : ' at ' + settings.app_contact_email + '!')
65 | ]
66 | },
67 | ]
68 | return (
69 |
70 |
71 | Frequently Asked Questions
72 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default FAQPage;
--------------------------------------------------------------------------------
/client/src/components/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Container, Button, Input, Card } from "semantic-ui-react";
3 | import useLogin from "../hooks/useLogin";
4 | import useViewer from "../hooks/useViewer";
5 | import { RouteComponentProps } from "react-router";
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
7 | import { faHeart } from "@fortawesome/free-solid-svg-icons";
8 |
9 | const LandingPage = (props: RouteComponentProps) => {
10 | const { redirectToDopeAuth } = useLogin();
11 | const { settings } = useViewer();
12 | const search = new URLSearchParams(props.location.search);
13 |
14 | const [password, setPassword] = useState(search.get("key") || "");
15 | const [isMentor, setIsMentor] = useState(password.length > 0);
16 |
17 | return (
18 |
19 |
20 | {settings ? settings.app_name : null} Help Queue
21 | Presented by: {settings ? settings.app_creator : null}
22 |
23 | The easy to use help queue system!
24 | {isMentor ? (
25 | setPassword(e.target.value)}
30 | />
31 | ) : null}
32 |
33 | {isMentor ? null : (
34 | setIsMentor(m => !m)}>
35 | Sign up as mentor
36 |
37 | )}
38 | {
40 | const params: [string, string][] | undefined =
41 | password.length > 0 ? [["mentor_key", password]] : undefined;
42 | redirectToDopeAuth(params);
43 | }}
44 | color="blue"
45 | >
46 | Log in with email
47 |
48 | {!isMentor && settings && settings.github_client_id ? (
49 |
57 | Log in with Github
58 |
59 | ) : null}
60 |
61 |
62 |
71 |
72 | );
73 | };
74 |
75 | export default LandingPage;
76 |
--------------------------------------------------------------------------------
/client/src/components/LoginCallback.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { RouteComponentProps } from "react-router";
3 | import useLogin from "../hooks/useLogin";
4 | import useViewer from "../hooks/useViewer";
5 | import { join } from "path";
6 |
7 | enum Status {
8 | start,
9 | loading,
10 | failed,
11 | succeed
12 | }
13 |
14 | const LoginCallback = (props: RouteComponentProps) => {
15 | const { isLoggedIn } = useViewer();
16 | const [loginStatus, setLoginStatus] = React.useState(
17 | isLoggedIn ? Status.succeed : Status.start
18 | );
19 | const search = new URLSearchParams(props.location.search);
20 | const { login } = useLogin();
21 | React.useEffect(() => {
22 | const id = search.get("uid");
23 | const token = search.get("token");
24 | const email = search.get("email");
25 | const mentor_key = search.get("mentor_key");
26 |
27 | if (isLoggedIn) {
28 | window.location.replace("/profile");
29 | }
30 | if (
31 | id != null &&
32 | token != null &&
33 | email != null &&
34 | loginStatus === Status.start
35 | ) {
36 | setLoginStatus(0);
37 | login(id, email, token, {"mentor_key": mentor_key}).then((success: Boolean) => {
38 | setLoginStatus(success ? Status.succeed : Status.failed);
39 | });
40 | } else if (loginStatus === Status.start) {
41 | setLoginStatus(Status.failed);
42 | }
43 | // eslint-disable-next-line
44 | }, [isLoggedIn, loginStatus]);
45 |
46 | if (loginStatus === Status.start || loginStatus === Status.loading) {
47 | return ;
48 | } else if (loginStatus === Status.succeed) {
49 | return ;
50 | } else {
51 | return ;
52 | }
53 | };
54 |
55 | export default LoginCallback;
56 |
--------------------------------------------------------------------------------
/client/src/components/LoginGithub.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { RouteComponentProps } from "react-router";
3 | import useLogin from "../hooks/useLogin";
4 | import useViewer from "../hooks/useViewer";
5 |
6 | enum Status {
7 | start,
8 | loading,
9 | failed,
10 | succeed
11 | }
12 |
13 | const LoginGithub = (props: RouteComponentProps) => {
14 | const { isLoggedIn } = useViewer();
15 | const [loginStatus, setLoginStatus] = React.useState(
16 | isLoggedIn ? Status.succeed : Status.start
17 | );
18 | const search = new URLSearchParams(props.location.search);
19 | const { login } = useLogin();
20 | React.useEffect(() => {
21 | const code = search.get("code");
22 | const mentor_key = search.get("mentor_key");
23 |
24 | if (isLoggedIn) {
25 | window.location.replace("/profile");
26 | }
27 | if (
28 | code != null &&
29 | loginStatus === Status.start
30 | ) {
31 | setLoginStatus(0);
32 | login("GITHUB", "GITHUB", code, {"mentor_key": mentor_key, }).then((success: Boolean) => {
33 | setLoginStatus(success ? Status.succeed : Status.failed);
34 | });
35 | } else if (loginStatus === Status.start) {
36 | setLoginStatus(Status.failed);
37 | }
38 | // eslint-disable-next-line
39 | }, [isLoggedIn, loginStatus]);
40 |
41 | if (loginStatus === Status.start || loginStatus === Status.loading) {
42 | return ;
43 | } else if (loginStatus === Status.succeed) {
44 | return ;
45 | } else {
46 | return ;
47 | }
48 | };
49 |
50 | export default LoginGithub;
51 |
--------------------------------------------------------------------------------
/client/src/components/ProfilePage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Container, Button, Input, Label, Card, Form } from "semantic-ui-react";
3 | import useLogin from "../hooks/useLogin";
4 | import { User } from "./Types";
5 | import ServerHelper, { ServerURL } from "./ServerHelper";
6 | import useViewer from "../hooks/useViewer";
7 | import TagsInput from "react-tagsinput";
8 | import "react-tagsinput/react-tagsinput.css"; // If using WebPack and style-loader.
9 | import { useCookies } from "react-cookie";
10 | import createAlert, { AlertType } from "./Alert";
11 | import { Alert, Badge } from "reactstrap";
12 |
13 | const ProfilePage = () => {
14 | const { getCredentials } = useLogin();
15 | const [_cookies, setCookie] = useCookies();
16 | const { isLoggedIn } = useViewer();
17 | const [user, setUser] = useState(null);
18 | const [name, setName] = useState("");
19 | const [skills, setSkills] = useState([]);
20 | const [permissionsGranted, setPermissionsGranted] = useState(true);
21 |
22 | const getUser = async () => {
23 | const res = await ServerHelper.post(ServerURL.userTicket, getCredentials());
24 | if (res.success) {
25 | setUser(res.user);
26 | setName(res.user.name || "");
27 | } else {
28 | setUser(null);
29 | createAlert(AlertType.Error, "Failed to get user, are you logged in?");
30 | }
31 | };
32 | const saveProfile = async (shouldRedirect: string | null) => {
33 | if (name.length === 0) {
34 | createAlert(AlertType.Error, "Name must be nonempty");
35 | return;
36 | }
37 | const res = await ServerHelper.post(ServerURL.userUpdate, {
38 | ...getCredentials(),
39 | name: name,
40 | affiliation: "", // TODO(kevinfang): add company affiliation
41 | skills: skills.join(";"),
42 | });
43 | if (res.success) {
44 | setUser(res.user);
45 | createAlert(AlertType.Success, "Updated profile");
46 | } else {
47 | setUser(null);
48 | createAlert(AlertType.Error, "Failed to update profile");
49 | }
50 | if (shouldRedirect) {
51 | window.location.href = shouldRedirect;
52 | }
53 | };
54 |
55 | useEffect(() => {
56 | getUser();
57 | if (Notification) {
58 | Notification.requestPermission();
59 | }
60 | }, []);
61 | const permission = Notification && Notification.permission;
62 |
63 | useEffect(() => {
64 | if (Notification && permission !== "granted") {
65 | setPermissionsGranted(false);
66 | }
67 | }, [permission]);
68 |
69 | const tempName = user ? user.name : null;
70 | const tempSkills = user ? user.skills : null;
71 | useEffect(() => {
72 | if (tempName) {
73 | setName(tempName);
74 | setCookie("name", tempName);
75 | }
76 | if (tempSkills) {
77 | setSkills(tempSkills.split(";").filter((e) => e.length > 0));
78 | }
79 | }, [tempName, tempSkills]);
80 |
81 | if (!isLoggedIn) {
82 | window.location.href = "/login";
83 | }
84 |
85 | if (!user) {
86 | return (
87 |
88 |
Loading user...
89 |
90 | );
91 | }
92 |
93 | return (
94 |
95 |
96 | Profile
97 |
99 | setName(e.target.value)}
103 | />
104 |
105 |
106 |
113 |
114 |
115 |
116 | {user.mentor_is ? (
117 | <>
118 |
119 | Technical skills (i.e. javascript, java...):
120 | setSkills(e)} />
121 |
122 | >
123 | ) : null}
124 |
125 | {!permissionsGranted
126 | ? You have not enabled desktop notifications! Consider enabling them! Look at the top left corner
127 | : null}
128 |
129 | saveProfile(null)}>Save Profile
130 | {!user.mentor_is || user.admin_is ? (
131 | {
134 | saveProfile("/");
135 | }}
136 | >
137 | Go to Queue!
138 |
139 | ) : null}
140 | {user.mentor_is || user.admin_is ? (
141 | {
144 | saveProfile("/m");
145 | }}
146 | >
147 | Go to Mentor Queue!
148 |
149 | ) : null}
150 |
151 |
152 |
153 | );
154 | };
155 |
156 | export default ProfilePage;
157 |
--------------------------------------------------------------------------------
/client/src/components/QueueMentor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Container, Button, Card, Select } from "semantic-ui-react";
3 | import useLogin from "../hooks/useLogin";
4 | import ServerHelper, { ServerURL } from "./ServerHelper";
5 | import { Ticket } from "./Types";
6 | import createAlert, { AlertType } from "./Alert";
7 | import { Row, Col, Badge } from "reactstrap";
8 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
9 | import { faStar } from "@fortawesome/free-solid-svg-icons";
10 | import useViewer from "../hooks/useViewer";
11 |
12 | const QueueMentor = () => {
13 | const { getCredentials } = useLogin();
14 | const { settings } = useViewer();
15 | const [tickets, setTickets] = useState(null);
16 | const [rankings, setRankings] = useState([]);
17 | const [ticket, setTicket] = useState(null);
18 | const [queueLength, setQueueLength] = useState(0);
19 | const locationOptions = [
20 | { key: "", value: "no location", text: "No filter" },
21 | ].concat(
22 | ((settings && settings.locations) || "no location")
23 | .split(",")
24 | .map((l) => ({ key: l, value: l, text: l }))
25 | );
26 | const [filteredTickets, setFilteredTickets] = useState([]);
27 | const [searchValue, setSearchValue] = useState("no location");
28 |
29 | const getTickets = async () => {
30 | const res = await ServerHelper.post(
31 | ServerURL.userTickets,
32 | getCredentials()
33 | );
34 | document.title =
35 | `(${res.tickets && res.tickets.length}) ` +
36 | (settings ? settings.app_name : "") +
37 | " Help Queue";
38 | if (res.success) {
39 | setTickets(res.tickets);
40 | setRankings(res.rankings);
41 | setTicket(res.ticket);
42 | setQueueLength(res.queue_length);
43 | if (!res.user.mentor_is) {
44 | createAlert(
45 | AlertType.Error,
46 | "You are not registered as a mentor! Ask your admin to whitelist you"
47 | );
48 | }
49 | } else {
50 | setTickets(null);
51 | }
52 | };
53 |
54 | useEffect(() => {
55 | if (!tickets) return;
56 | if (searchValue === "no location") {
57 | setFilteredTickets(tickets);
58 | return;
59 | }
60 | setFilteredTickets(
61 | tickets.filter((ticket) => ticket.data.location.includes(searchValue))
62 | );
63 | }, [searchValue, tickets]);
64 |
65 | useEffect(() => {
66 | // On load check to see what the status is of the ticket
67 | getTickets();
68 |
69 | const interval = setInterval(getTickets, 5000);
70 | return () => clearInterval(interval);
71 | }, []);
72 |
73 | let queueCard = null;
74 | if (ticket == null || ticket.status != 1) {
75 | // If the mentor version hasn't claimed the ticket
76 | if (tickets == null || queueLength == 0) {
77 | queueCard = There are no tickets :(
;
78 | } else {
79 | queueCard = filteredTickets.map((ticket) => {
80 | return (
81 |
82 |
83 | {ticket.requested_by} asked {ticket.minutes}{" "}
84 | mins ago :
85 |
86 | {ticket.data.question}
87 | {ticket.data.location !== "no location" &&
88 | ticket.data.location !== "default" ? (
89 |
90 | {ticket.data.location}
91 |
92 | ) : null}
93 | {
95 | const res = await ServerHelper.post(ServerURL.claimTicket, {
96 | ...getCredentials(),
97 | ticket_id: ticket.id,
98 | });
99 | if (res.success) {
100 | setTicket(res.ticket);
101 | createAlert(AlertType.Success, "Claimed ticket");
102 | }
103 | }}
104 | className="col-12"
105 | basic
106 | color="green"
107 | >
108 | Claim
109 |
110 |
111 | );
112 | });
113 | }
114 | } else if (ticket != null) {
115 | // Ticket has been claimed
116 | queueCard = (
117 | <>
118 |
119 | You have claimed: {ticket.requested_by}{" "}
120 |
121 |
122 | Question: {ticket.data.question}
123 |
124 | Location: {ticket.data.location}
125 |
126 | Contact: {ticket.data.contact}
127 |
128 |
129 | {settings &&
130 | settings.jitsi_link &&
131 | settings.jitsi_link.includes("://") ? (
132 |
133 | {settings.jitsi_link + "/" + ticket.uid}
134 |
135 | ) : null}
136 |
137 |
138 | {
140 | const res = await ServerHelper.post(ServerURL.closeTicket, {
141 | ...getCredentials(),
142 | ticket_id: ticket.id,
143 | });
144 | if (res.success) {
145 | setTicket(null);
146 | getTickets();
147 | createAlert(AlertType.Success, "Closed ticket");
148 | }
149 | }}
150 | color="green"
151 | >
152 | Close Ticket
153 |
154 | {
157 | const res = await ServerHelper.post(ServerURL.unclaimTicket, {
158 | ...getCredentials(),
159 | ticket_id: ticket.id,
160 | });
161 | if (res.success) {
162 | getTickets();
163 | createAlert(AlertType.Success, "Unclaimed ticket");
164 | }
165 | }}
166 | basic
167 | >
168 | Unclaim
169 |
170 |
171 | >
172 | );
173 | }
174 |
175 | return (
176 |
177 |
178 | 0 ? "8" : "12"}>
179 |
180 | Mentor Queue
181 | Queue length: {queueLength}
182 | setSearchValue("" + data.value || "")}
186 | />
187 | {queueCard}
188 |
189 |
190 | {rankings.length > 0 ? (
191 |
192 |
193 | Mentor Leaderboard
194 |
195 | {rankings.map(
196 | (
197 | r: { name: string; rating: string; tickets: string },
198 | ind
199 | ) => {
200 | return (
201 |
202 | {r.name} - {r.rating}{" "}
203 | (
204 | {r.tickets} {r.tickets == "1" ? "ticket" : "tickets"})
205 |
206 | );
207 | }
208 | )}
209 |
210 |
211 |
212 | ) : null}
213 |
214 |
215 | );
216 | };
217 |
218 | export default QueueMentor;
219 |
--------------------------------------------------------------------------------
/client/src/components/QueueRequest.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from "react";
2 | // @ts-ignore
3 | import Rate from "rc-rate";
4 | import "rc-rate/assets/index.css";
5 | import {
6 | Container,
7 | Button,
8 | Input,
9 | Card,
10 | Form,
11 | Label,
12 | Message,
13 | TextArea,
14 | Header,
15 | MessageHeader,
16 | Select,
17 | } from "semantic-ui-react";
18 | import useLogin from "../hooks/useLogin";
19 | import ServerHelper, { ServerURL } from "./ServerHelper";
20 | import useViewer from "../hooks/useViewer";
21 | import { Ticket, User } from "./Types";
22 | import createAlert, { AlertType } from "./Alert";
23 |
24 | const QueueRequest = () => {
25 | const { getCredentials, logout } = useLogin();
26 | const { settings } = useViewer();
27 | const { isLoggedIn } = useViewer();
28 | const [ticket, setTicket] = useState(null);
29 | const [user, setUser] = useState(null);
30 | const [queueLength, setQueueLength] = useState(0);
31 | const [cTicketQuestion, setCTicketQuestion] = useState("");
32 | const [cTicketContact, setCTicketContact] = useState("");
33 | const [cTicketRating, setCTicketRating] = useState(0);
34 | const locationOptions = ((settings && settings.locations) || "no location")
35 | .split(",")
36 | .map((l) => ({ key: l, value: l, text: l }));
37 | const [cTicketLocation, setCTicketLocation] = useState(
38 | locationOptions[0].value
39 | );
40 | const [canMakeNotification, setCanMakeNotification] = useState(false);
41 |
42 | const getTicket = useCallback(async () => {
43 | const res = await ServerHelper.post(ServerURL.userTicket, getCredentials());
44 | if (res.success) {
45 | const newticket = res.ticket as Ticket | null;
46 | if (newticket && (newticket.status == 0 || newticket.status == 2)) {
47 | setCanMakeNotification(true);
48 | }
49 | if (newticket && newticket.status == 1 && canMakeNotification) {
50 | const notification = new Notification("Your ticket has been claimed", {
51 | body:
52 | settings &&
53 | settings.jitsi_link &&
54 | settings.jitsi_link.includes("://")
55 | ? "Click to open up your zoom call link"
56 | : "Don't keep your mentor waiting!",
57 | });
58 | notification.onclick = () => {
59 | if (
60 | settings &&
61 | settings.jitsi_link &&
62 | settings.jitsi_link.includes("://")
63 | ) {
64 | window.open(settings.jitsi_link + "/" + newticket.uid);
65 | }
66 | };
67 | setCanMakeNotification(false);
68 | }
69 | setTicket(res.ticket);
70 | setUser(res.user);
71 | setQueueLength(res.queue_position + 1);
72 | } else {
73 | setTicket(null);
74 | if (isLoggedIn) {
75 | if (
76 | window.confirm(
77 | "Your credentials appear to be invalid... Do you want to log out and try again?"
78 | )
79 | ) {
80 | logout();
81 | }
82 | }
83 | }
84 | }, [settings, isLoggedIn, canMakeNotification]);
85 | const cancelTicket = async () => {
86 | if (ticket == null) {
87 | return;
88 | }
89 | const res = await ServerHelper.post(ServerURL.cancelTicket, {
90 | ...getCredentials(),
91 | ticket_id: ticket.id,
92 | });
93 | if (res.success) {
94 | setTicket(null);
95 | setCTicketQuestion(ticket.data.question);
96 | createAlert(AlertType.Success, "Canceled ticket");
97 | } else {
98 | createAlert(AlertType.Error, "Could not cancel ticket");
99 | }
100 | };
101 | const rateTicket = async () => {
102 | if (ticket == null) {
103 | return;
104 | }
105 | const res = await ServerHelper.post(ServerURL.rateTicket, {
106 | ...getCredentials(),
107 | ticket_id: ticket.id,
108 | rating: cTicketRating,
109 | });
110 | if (res.success) {
111 | setTicket(null);
112 | createAlert(AlertType.Success, "Successfully rated ticket");
113 | } else {
114 | createAlert(AlertType.Error, "Could not rate ticket");
115 | }
116 | };
117 | useEffect(() => {
118 | // On load check to see what the status is of the ticket
119 | getTicket();
120 |
121 | const interval = setInterval(getTicket, 5000);
122 | return () => clearInterval(interval);
123 | }, []);
124 | if (!isLoggedIn) {
125 | window.location.href = "/login";
126 | return null;
127 | }
128 |
129 | let queueCard = null;
130 | if (ticket == null) {
131 | queueCard = (
132 | <>
133 | Welcome to the HelpQueue!
134 | {settings &&
135 | settings.queue_message &&
136 | settings.queue_message.length > 0 ? (
137 |
138 | Announcement:
139 | {settings.queue_message}
140 |
141 | ) : null}
142 |
143 |
145 | I need help with...
146 |
152 |
153 | What event?
154 | setCTicketLocation("" + data.value || "")}
158 | />
159 |
160 |
161 | Contact Info:
162 | setCTicketContact(e.target.value)}
166 | />
167 |
168 |
169 |
170 |
171 | {
174 | if (cTicketQuestion.length === 0) {
175 | createAlert(AlertType.Warning, "You need to ask a question!");
176 | return;
177 | }
178 | if (cTicketLocation.length === 0) {
179 | createAlert(
180 | AlertType.Warning,
181 | "Please provide a location so a mentor can find you!"
182 | );
183 | return;
184 | }
185 | setCTicketRating(0);
186 | const res = await ServerHelper.post(ServerURL.createTicket, {
187 | ...getCredentials(),
188 | data: JSON.stringify({
189 | question: cTicketQuestion,
190 | location: cTicketLocation,
191 | contact: cTicketContact.length === 0 ? "N/A" : cTicketContact,
192 | }),
193 | });
194 | if (res.success) {
195 | setTicket(res.ticket);
196 | setCTicketQuestion("");
197 | }
198 | }}
199 | color="blue"
200 | className="col-12"
201 | >
202 | Create Ticket
203 |
204 | >
205 | );
206 | } else if (ticket.status == 0 || ticket.status == 2) {
207 | // Unclaimed
208 | queueCard = (
209 | <>
210 | Waiting for Mentor...
211 |
212 | Position in Queue: {queueLength}
213 |
214 | Posted: {" "}
215 | {ticket.minutes < 3
216 | ? "a few minutes ago"
217 | : ticket.minutes + " minutes ago"}
218 |
219 |
220 | Question: {ticket.data.question}
221 |
222 | Location: {ticket.data.location}
223 |
224 | Contact: {ticket.data.contact}
225 |
226 |
227 | {settings &&
228 | settings.jitsi_link &&
229 | settings.jitsi_link.includes("://") ? (
230 |
231 | {settings.jitsi_link + "/" + ticket.uid}
232 |
233 | ) : null}
234 |
235 |
236 | Cancel Ticket
237 |
238 | >
239 | );
240 | } else if (ticket.status == 1) {
241 | // Claimed
242 | queueCard = (
243 | <>
244 | You have been claimed!
245 |
246 | Claimed by: {ticket.claimed_by}
247 |
248 |
249 | {settings &&
250 | settings.jitsi_link &&
251 | settings.jitsi_link.includes("://") ? (
252 |
253 | {settings.jitsi_link + "/" + ticket.uid}
254 |
255 | ) : null}
256 |
257 |
258 | Cancel Ticket
259 |
260 | >
261 | );
262 | } else if (ticket.status == 3) {
263 | // Closed but not yet rated
264 | queueCard = (
265 | <>
266 | The ticket has been closed.
267 |
268 | Please rate your mentor ({ticket.claimed_by} )!
269 |
270 |
276 |
277 | {cTicketRating == 0 ? (
278 | "Close Ticket"
279 | ) : (
280 | <>
281 | Rating {ticket.claimed_by} {cTicketRating} stars
282 | >
283 | )}
284 |
285 | >
286 | );
287 | } else {
288 | queueCard = Something went wrong
;
289 | }
290 | return (
291 |
292 |
293 |
294 | {user && user.admin_is ? (
295 |
296 | Admin Page
297 |
298 | ) : null}
299 | {user && user.mentor_is ? (
300 |
301 | Mentor Queue
302 |
303 | ) : null}
304 |
305 | {settings && settings.queue_status == "true"
306 | ? queueCard
307 | : "The queue is currently closed"}
308 |
309 |
310 |
311 | );
312 | };
313 |
314 | export default QueueRequest;
315 |
--------------------------------------------------------------------------------
/client/src/components/ServerHelper.tsx:
--------------------------------------------------------------------------------
1 | export enum ServerURL {
2 | createTicket = "/api/v1/ticket/create",
3 | claimTicket = "/api/v1/ticket/claim",
4 | unclaimTicket = "/api/v1/ticket/unclaim",
5 | closeTicket = "/api/v1/ticket/close",
6 | cancelTicket = "/api/v1/ticket/cancel",
7 | rateTicket = "/api/v1/ticket/rate",
8 | userTicket = "/api/v1/user/ticket",
9 | userTickets = "/api/v1/user/tickets",
10 | userUpdate = "/api/v1/user/update",
11 | login = "/api/v1/client/login",
12 | settings = "/api/v1/client",
13 | admin = "/api/v1/admin/settings",
14 | updateAdmin = "/api/v1/admin/update",
15 | promoteUser = "/api/v1/admin/promote",
16 | resetEverything = "/api/v1/admin/reset"
17 | }
18 |
19 | const ServerHelper = {
20 | post: async (
21 | url: ServerURL,
22 | data: any,
23 | ): Promise<{ success: boolean; [key: string]: any }> => {
24 | try {
25 | const config = {
26 | method: "POST",
27 | headers: {
28 | Accept: "application/json",
29 | "Content-Type": "application/json"
30 | },
31 | body: JSON.stringify(data)
32 | };
33 | const response = await fetch(url, config);
34 | if (response.ok) {
35 | const json = await response.json();
36 | return json;
37 | }
38 | } catch (error) {}
39 | return { success: false };
40 | }
41 | };
42 |
43 | export default ServerHelper;
44 |
--------------------------------------------------------------------------------
/client/src/components/Types.tsx:
--------------------------------------------------------------------------------
1 | export type Ticket = {
2 | data: {
3 | question: string;
4 | location: string;
5 | contact: string;
6 | };
7 | id: number;
8 | uid: string;
9 | status: number;
10 | requested_by: string;
11 | claimed_by: string | null;
12 | minutes: number;
13 | };
14 |
15 | export type AdminStats = {
16 | average_wait: number;
17 | average_claimed: number;
18 | average_rating: number;
19 | }
20 | export type User = {
21 | id: number;
22 | email: string;
23 | name: string;
24 | skills: string;
25 | admin_is: boolean;
26 | mentor_is: boolean;
27 | };
28 |
29 | // In server_constants.py
30 | export type ClientSettings = {
31 | app_name: string;
32 | app_contact_email: string;
33 | jitsi_link: string;
34 | app_creator: string;
35 | queue_status: string;
36 | queue_message: string;
37 | readonly_master_url: string;
38 | mentor_password_key: string;
39 | github_client_id: string;
40 | locations: string;
41 | }
--------------------------------------------------------------------------------
/client/src/components/info.md:
--------------------------------------------------------------------------------
1 | Follow the convention of
2 |
3 | `[ComponentName].tsx`
4 |
--------------------------------------------------------------------------------
/client/src/hooks/info.md:
--------------------------------------------------------------------------------
1 | Follow the convention of
2 |
3 | `use[hookname].tsx`
4 |
--------------------------------------------------------------------------------
/client/src/hooks/useLogin.tsx:
--------------------------------------------------------------------------------
1 | import { useCookies } from "react-cookie";
2 | import ServerHelper, { ServerURL } from "../components/ServerHelper";
3 | import useViewer from "./useViewer";
4 |
5 | const useLogin = () => {
6 | // eslint-disable-next-line
7 | const [cookies, setCookie, removeCookie] = useCookies([
8 | "token",
9 | "uid",
10 | "email",
11 | "name"
12 | ]);
13 | const {settings} = useViewer();
14 |
15 | const redirectToDopeAuth = (data?: [string, string][]) => {
16 | let otherdata = "";
17 | if (data != null) {
18 | data.forEach(value => {
19 | otherdata += `${value[0]}=${value[1]}&`;
20 | });
21 | }
22 | if (otherdata.length > 0) {
23 | otherdata = `?${otherdata.substring(0, otherdata.length - 1)}`;
24 | }
25 | window.location.href =
26 | "https://dopeauth.com/login/" +
27 | encodeURIComponent(
28 | (settings ? settings.readonly_master_url : "") + "/login/auth"
29 | ) +
30 | otherdata;
31 | };
32 |
33 | const login = async (
34 | uid: string,
35 | email: string,
36 | token: string,
37 | data?: any,
38 | ): Promise => {
39 | if (data == null) {
40 | data = {};
41 | }
42 | try {
43 | // Server side check!
44 | const json = await ServerHelper.post(ServerURL.login, {
45 | email: email,
46 | token: token,
47 | uid: uid,
48 | name: cookies['name'],
49 | ...data
50 | });
51 | if (json["success"]) {
52 | setCookie("token", json["token"], { path: "/" });
53 | setCookie("uid", json["uid"], { path: "/" });
54 | setCookie("email", json["email"], { path: "/" });
55 | return true;
56 | }
57 | } catch (error) {}
58 | return false;
59 | };
60 | const getCredentials = () => {
61 | return {
62 | uid: cookies["uid"],
63 | token: cookies["token"]
64 | };
65 | };
66 | const logout = () => {
67 | removeCookie("name");
68 | removeCookie("token");
69 | removeCookie("uid");
70 | removeCookie("email");
71 | };
72 | return { redirectToDopeAuth, getCredentials, login, logout };
73 | };
74 |
75 | export default useLogin;
76 |
--------------------------------------------------------------------------------
/client/src/hooks/useViewer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useCookies } from "react-cookie";
4 | import { ClientSettings } from "../components/Types";
5 |
6 | export enum Query {
7 | name = "name",
8 | email = "email",
9 | uid = "uid",
10 | settings = "settings"
11 | }
12 |
13 | const useViewer = () => {
14 | const [cookies, setCookies] = useCookies([
15 | Query.name,
16 | Query.email,
17 | Query.uid,
18 | Query.settings
19 | ]);
20 | const viewer = (value: Query) => {
21 | return cookies[value];
22 | };
23 | const isLoggedIn = React.useMemo(() => cookies[Query.uid] != null, [cookies]);
24 |
25 | const settings: ClientSettings | null = React.useMemo(
26 | () => (cookies[Query.settings] ? cookies[Query.settings] : null),
27 | [cookies]
28 | );
29 | return { viewer, isLoggedIn, settings };
30 | };
31 |
32 | export default useViewer;
33 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Lato&display=swap');
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | background-color: #96D0F1;
11 | font-family: 'Lato', sans-serif;
12 | }
13 |
14 | footer p {
15 | color: #333;
16 | font-size: 12px;
17 | }
18 |
19 | footer a {
20 | color: #333;
21 | }
22 |
23 | footer a:hover {
24 | color: #999;
25 | text-decoration: none;
26 | }
27 |
28 | body.white {
29 | background-color: white;
30 | }
31 |
32 | code {
33 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
34 | monospace;
35 | }
36 |
37 | #MentorButton{
38 | background-color: #3AA5B0;
39 | color: white;
40 | }
41 |
42 | #AdminPage{
43 | background-color: #FBCDC2;
44 | color: white;
45 | }
46 |
47 | #CreateTicket{
48 | background-color: #146fc1;
49 | color: white;
50 | }
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import * as serviceWorker from "./serviceWorker";
5 | import "bootstrap/dist/css/bootstrap.min.css";
6 | import "./index.css";
7 |
8 | require("dotenv").config();
9 |
10 | ReactDOM.render( , document.getElementById("root"));
11 |
12 | // If you want your app to work offline and load faster, you can change
13 | // unregister() to register() below. Note this comes with some pitfalls.
14 | // Learn more about service workers: https://bit.ly/CRA-PWA
15 | serviceWorker.unregister();
16 |
--------------------------------------------------------------------------------
/client/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "preserve"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/docs/img/admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techx/helpqueue/f524f1bc6ad9675d5a8f80a24bca89b27814beb1/docs/img/admin.png
--------------------------------------------------------------------------------
/docs/img/mentor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techx/helpqueue/f524f1bc6ad9675d5a8f80a24bca89b27814beb1/docs/img/mentor.png
--------------------------------------------------------------------------------
/docs/img/opening.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/techx/helpqueue/f524f1bc6ad9675d5a8f80a24bca89b27814beb1/docs/img/opening.png
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask_script import Manager
3 | from flask_migrate import Migrate, MigrateCommand
4 |
5 | from server.app import app, db
6 |
7 | # This file is only necessary to be called
8 | # You are using a database
9 |
10 | migrate = Migrate(app, db)
11 | manager = Manager(app)
12 |
13 | manager.add_command('db', MigrateCommand)
14 |
15 | if __name__ == '__main__':
16 | manager.run()
17 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | # this is the Alembic Config object, which provides
12 | # access to the values within the .ini file in use.
13 | config = context.config
14 |
15 | # Interpret the config file for Python logging.
16 | # This line sets up loggers basically.
17 | fileConfig(config.config_file_name)
18 | logger = logging.getLogger('alembic.env')
19 |
20 | # add your model's MetaData object here
21 | # for 'autogenerate' support
22 | # from myapp import mymodel
23 | # target_metadata = mymodel.Base.metadata
24 | from flask import current_app
25 | config.set_main_option('sqlalchemy.url',
26 | current_app.config.get('SQLALCHEMY_DATABASE_URI'))
27 | target_metadata = current_app.extensions['migrate'].db.metadata
28 |
29 | # other values from the config, defined by the needs of env.py,
30 | # can be acquired:
31 | # my_important_option = config.get_main_option("my_important_option")
32 | # ... etc.
33 |
34 |
35 | def run_migrations_offline():
36 | """Run migrations in 'offline' mode.
37 |
38 | This configures the context with just a URL
39 | and not an Engine, though an Engine is acceptable
40 | here as well. By skipping the Engine creation
41 | we don't even need a DBAPI to be available.
42 |
43 | Calls to context.execute() here emit the given string to the
44 | script output.
45 |
46 | """
47 | url = config.get_main_option("sqlalchemy.url")
48 | context.configure(
49 | url=url, target_metadata=target_metadata, literal_binds=True
50 | )
51 |
52 | with context.begin_transaction():
53 | context.run_migrations()
54 |
55 |
56 | def run_migrations_online():
57 | """Run migrations in 'online' mode.
58 |
59 | In this scenario we need to create an Engine
60 | and associate a connection with the context.
61 |
62 | """
63 |
64 | # this callback is used to prevent an auto-migration from being generated
65 | # when there are no changes to the schema
66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
67 | def process_revision_directives(context, revision, directives):
68 | if getattr(config.cmd_opts, 'autogenerate', False):
69 | script = directives[0]
70 | if script.upgrade_ops.is_empty():
71 | directives[:] = []
72 | logger.info('No changes in schema detected.')
73 |
74 | connectable = engine_from_config(
75 | config.get_section(config.config_ini_section),
76 | prefix='sqlalchemy.',
77 | poolclass=pool.NullPool,
78 | )
79 |
80 | with connectable.connect() as connection:
81 | context.configure(
82 | connection=connection,
83 | target_metadata=target_metadata,
84 | process_revision_directives=process_revision_directives,
85 | **current_app.extensions['migrate'].configure_args
86 | )
87 |
88 | with context.begin_transaction():
89 | context.run_migrations()
90 |
91 |
92 | if context.is_offline_mode():
93 | run_migrations_offline()
94 | else:
95 | run_migrations_online()
96 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/migrations/versions/653093fddc44_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 653093fddc44
4 | Revises: 8d950e758485
5 | Create Date: 2020-08-28 18:15:33.543244
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '653093fddc44'
14 | down_revision = '8d950e758485'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('tickets', sa.Column('uid', sa.String(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_column('tickets', 'uid')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/migrations/versions/68d234e0f83e_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 68d234e0f83e
4 | Revises: a9fd5b5f5b0a
5 | Create Date: 2019-12-21 07:11:51.818491
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '68d234e0f83e'
14 | down_revision = 'a9fd5b5f5b0a'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('tickets', sa.Column('rating', sa.Integer(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_column('tickets', 'rating')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/migrations/versions/8a8e177c22c8_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 8a8e177c22c8
4 | Revises: eef69a932db1
5 | Create Date: 2019-12-15 18:02:16.218540
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '8a8e177c22c8'
14 | down_revision = 'eef69a932db1'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('settings',
22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
23 | sa.Column('key', sa.String(), nullable=False),
24 | sa.Column('value', sa.String(), nullable=True),
25 | sa.Column('date_created', sa.DateTime(), nullable=True),
26 | sa.Column('date_updated', sa.DateTime(), nullable=True),
27 | sa.PrimaryKeyConstraint('id', 'key'),
28 | sa.UniqueConstraint('id'),
29 | sa.UniqueConstraint('key')
30 | )
31 | op.create_unique_constraint(None, 'clients', ['id'])
32 | op.create_unique_constraint(None, 'tickets', ['id'])
33 | # ### end Alembic commands ###
34 |
35 |
36 | def downgrade():
37 | # ### commands auto generated by Alembic - please adjust! ###
38 | op.drop_constraint(None, 'tickets', type_='unique')
39 | op.drop_constraint(None, 'clients', type_='unique')
40 | op.drop_table('settings')
41 | # ### end Alembic commands ###
42 |
--------------------------------------------------------------------------------
/migrations/versions/8d950e758485_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 8d950e758485
4 | Revises: de3ab01f4eb0
5 | Create Date: 2020-01-23 12:05:29.835861
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '8d950e758485'
14 | down_revision = 'de3ab01f4eb0'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('users', sa.Column('date_last_signin', sa.DateTime(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_column('users', 'date_last_signin')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/migrations/versions/a9fd5b5f5b0a_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: a9fd5b5f5b0a
4 | Revises: 8a8e177c22c8
5 | Create Date: 2019-12-16 17:05:04.697429
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'a9fd5b5f5b0a'
14 | down_revision = '8a8e177c22c8'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('users', sa.Column('affiliation', sa.String(), nullable=True))
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_column('users', 'affiliation')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/migrations/versions/de3ab01f4eb0_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: de3ab01f4eb0
4 | Revises: 68d234e0f83e
5 | Create Date: 2020-01-09 13:56:48.972103
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'de3ab01f4eb0'
14 | down_revision = '68d234e0f83e'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_unique_constraint(None, 'clients', ['uid'])
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | op.drop_constraint(None, 'clients', type_='unique')
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/migrations/versions/eef69a932db1_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: eef69a932db1
4 | Revises:
5 | Create Date: 2019-12-15 07:59:08.143400
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'eef69a932db1'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('users',
22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
23 | sa.Column('email', sa.String(), nullable=False),
24 | sa.Column('name', sa.String(), nullable=True),
25 | sa.Column('contact_info', sa.String(), nullable=True),
26 | sa.Column('admin_is', sa.Boolean(), nullable=True),
27 | sa.Column('mentor_is', sa.Boolean(), nullable=True),
28 | sa.Column('skills', sa.String(), nullable=True),
29 | sa.Column('date_created', sa.DateTime(), nullable=True),
30 | sa.Column('date_updated', sa.DateTime(), nullable=True),
31 | sa.PrimaryKeyConstraint('id', 'email'),
32 | sa.UniqueConstraint('email'),
33 | sa.UniqueConstraint('id')
34 | )
35 | op.create_table('clients',
36 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
37 | sa.Column('user_id', sa.Integer(), nullable=True),
38 | sa.Column('uid', sa.String(), nullable=True),
39 | sa.Column('token', sa.String(), nullable=True),
40 | sa.Column('date_created', sa.DateTime(), nullable=True),
41 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
42 | sa.PrimaryKeyConstraint('id'),
43 | sa.UniqueConstraint('id')
44 | )
45 | op.create_table('tickets',
46 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
47 | sa.Column('requestor_id', sa.Integer(), nullable=True),
48 | sa.Column('claimant_id', sa.Integer(), nullable=True),
49 | sa.Column('data', sa.String(), nullable=True),
50 | sa.Column('status', sa.Integer(), nullable=True),
51 | sa.Column('total_claimed_seconds', sa.Integer(), nullable=True),
52 | sa.Column('total_unclaimed_seconds', sa.Integer(), nullable=True),
53 | sa.Column('date_created', sa.DateTime(), nullable=True),
54 | sa.Column('date_updated', sa.DateTime(), nullable=True),
55 | sa.ForeignKeyConstraint(['claimant_id'], ['users.id'], ),
56 | sa.ForeignKeyConstraint(['requestor_id'], ['users.id'], ),
57 | sa.PrimaryKeyConstraint('id'),
58 | sa.UniqueConstraint('id')
59 | )
60 | # ### end Alembic commands ###
61 |
62 |
63 | def downgrade():
64 | # ### commands auto generated by Alembic - please adjust! ###
65 | op.drop_table('tickets')
66 | op.drop_table('clients')
67 | op.drop_table('users')
68 | # ### end Alembic commands ###
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "helplifo",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "concurrently": "^4.1.1",
7 | "prettier": "^1.19.1"
8 | },
9 | "scripts": {
10 | "start": "cp .env client/.env; cd client && yarn start",
11 | "prebuild": "python manage.py db upgrade && python prebuild.py",
12 | "build": "cd client && yarn install && yarn build",
13 | "postbuild": "cp -r client/build server/",
14 | "gunicorn": "gunicorn run_server:app",
15 | "dev-server": "gunicorn -b 127.0.0.1:5000 --reload run_dev_server:app",
16 | "prod-server": "gunicorn -b 127.0.0.1:5000 --reload run_server:app",
17 | "dev": "concurrently -r -k \"yarn start >/dev/null 2>/dev/null\" \"npm run dev-server\"",
18 | "prod": "concurrently --kill-others \"yarn start\" \"npm run prod-server\""
19 | },
20 | "devDependencies": {
21 | "ws": "3.3.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/prebuild.py:
--------------------------------------------------------------------------------
1 | import os
2 | import base64
3 | import os.path
4 | from os import path
5 | import os
6 | import base64
7 | print("Post BUILD ENV sync")
8 |
9 | if not path.exists(".env"):
10 | print("******* Post build sync started *********")
11 | f = open(".env", "w")
12 | for key in os.environ:
13 | f.write(f"{key}=\"{os.environ[key]}\"\n")
14 |
15 | if('GSHEETS_AUTH64' in os.environ):
16 | print("Copying over GSHEETS_AUTH64 as well")
17 | f = open("service_account.json", "wb")
18 | f.write(base64.b64decode(os.environ['GSHEETS_AUTH64']))
19 | f.close()
20 | else:
21 | print("******* WARNING *********")
22 | print("******* Heroku Post Build is current disabled *********")
23 |
24 | from server.controllers.settings import *
25 |
26 | for setting in ALLDEFAULTSETTINGS:
27 | if (get_setting(None,setting, True) is None):
28 | set_setting(None, setting, "CHANGE ME", override=True)
29 |
30 | if (get_setting(None, SETTING_OFFICIAL_MESSAGE, True) is None):
31 | set_setting(None, SETTING_OFFICIAL_MESSAGE, "", override=True)
32 | if (get_setting(None, SETTING_QUEUE_ON, True) is None):
33 | set_setting(None, SETTING_QUEUE_ON, True, override=True)
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic==1.0.11
2 | aniso8601==7.0.0
3 | APScheduler==3.6.3
4 | astroid==2.2.5
5 | autopep8==1.4.4
6 | certifi==2019.6.16
7 | chardet==3.0.4
8 | Click==7.0
9 | Flask==1.1.1
10 | Flask-DotEnv==0.1.2
11 | Flask-Migrate==2.5.2
12 | Flask-RESTful==0.3.7
13 | Flask-Script==2.0.6
14 | Flask-SQLAlchemy==2.4.0
15 | gspread==3.1.0
16 | gunicorn==19.9.0
17 | httplib2==0.13.1
18 | idna==2.8
19 | isort==4.3.21
20 | itsdangerous==1.1.0
21 | Jinja2==2.10.1
22 | lazy-object-proxy==1.4.2
23 | Mako==1.1.0
24 | MarkupSafe==1.1.1
25 | mccabe==0.6.1
26 | oauth2client==4.1.3
27 | psycopg2==2.8.3
28 | psycopg2-binary==2.8.3
29 | pyasn1==0.4.6
30 | pyasn1-modules==0.2.6
31 | pycodestyle==2.5.0
32 | pylint==2.3.1
33 | python-dateutil==2.8.0
34 | python-dotenv==0.15.0
35 | python-editor==1.0.4
36 | pytz==2019.2
37 | requests==2.22.0
38 | rsa==4.0
39 | six==1.12.0
40 | SQLAlchemy==1.3.7
41 | typed-ast==1.4.0
42 | tzlocal==2.0.0
43 | urllib3==1.25.3
44 | Werkzeug==0.15.5
45 | wrapt==1.11.2
46 |
--------------------------------------------------------------------------------
/run_dev_server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Runs the backend server
4 | """
5 |
6 | from flask import Flask
7 | from server.app import app
8 |
9 | app.config["APP-DEV"] = True
10 |
11 | if __name__ == '__main__':
12 | app.run(host='0.0.0.0', port=app.config['PORT'])
13 |
--------------------------------------------------------------------------------
/run_server.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Runs the backend server
4 | """
5 |
6 | from flask import Flask
7 | from server.app import app
8 |
9 | if __name__ == '__main__':
10 | app.run(host='0.0.0.0', port=app.config['PORT'])
11 |
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
1 | from dotenv import load_dotenv, find_dotenv
2 | print(find_dotenv())
3 | load_dotenv(dotenv_path=find_dotenv(), verbose=True)
4 | import os
5 | from server.app import app, db
6 | from server.api.v1.api import api
7 | from flask import Flask
8 | from flask_restful import Api
9 | from server.controllers.cron import cron_job
10 | from apscheduler.schedulers.background import BackgroundScheduler
11 | import sys
12 | import os
13 | from server.controllers.settings import *
14 |
15 | app.register_blueprint(api.blueprint, url_prefix='/api/v1')
16 |
17 | print("Initializing Background Scheduler")
18 | sched = BackgroundScheduler()
19 | sched.add_job(cron_job, trigger='interval', days=1)
20 | sched.start()
21 | cron_job()
22 |
23 | try:
24 | if "MASTER_EMAIL" in os.environ:
25 | set_setting(None, SETTING_MASTER_USER, os.environ["MASTER_EMAIL"], override=True)
26 | if os.getenv("MASTER_EMAIL") is not None:
27 | set_setting(None, SETTING_MASTER_USER, os.getenv("MASTER_EMAIL"), override=True)
28 |
29 | if "REACT_APP_SITEURL" in os.environ:
30 | app.config["REACT_APP_SITEURL"] = os.environ["REACT_APP_SITEURL"]
31 | if os.getenv("REACT_APP_SITEURL") is not None:
32 | app.config["REACT_APP_SITEURL"] = os.getenv("REACT_APP_SITEURL")
33 |
34 | set_setting(None, SETTING_URL, app.config["REACT_APP_SITEURL"], override=True)
35 | except:
36 | print("It appears as if the database is not set up")
37 | db.session.rollback()
--------------------------------------------------------------------------------
/server/api/v1/__init__.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from server.controllers.authentication import authenticate
3 |
4 | def add_token(parser):
5 | """
6 | Adds the requirement for the parser to require tokens
7 | """
8 | parser.add_argument('uid',
9 | type=str,
10 | help='UID',
11 | required=True)
12 | parser.add_argument('token',
13 | type=str,
14 | help='Token',
15 | required=True)
16 |
17 | def require_login(parser):
18 | """
19 | Forces token to be valid in order to use a specific API request
20 | """
21 | add_token(parser)
22 |
23 | def wrapper(func):
24 | @wraps(func)
25 | # Do something before
26 | def inner(self):
27 | data = parser.parse_args()
28 | user = verify_token(data)
29 | if (user is None):
30 | return return_failure("could not verify token", error_code=999)
31 | value = func(self, data, user)
32 | return value
33 | return inner
34 | return wrapper
35 |
36 |
37 | def verify_token(data):
38 | """
39 | Returns None or User if the user has been authenticaed
40 | """
41 | if (any([x not in data for x in ["token", "uid"]])):
42 | return None
43 | _, user = authenticate(data['uid'], data['token'])
44 | return user
45 |
46 |
47 | def return_auth_failure():
48 | return return_failure("could not verify token")
49 |
50 |
51 | def return_failure(message, error_code=500):
52 | """
53 | Generates JSON for a failed API request
54 | """
55 | return {"success": False, "error": message, "error_code": error_code}
56 |
57 | def return_success(data=None):
58 | """
59 | Generates JSON for a successful API request
60 | """
61 | if data is None:
62 | return {"success": True}
63 | return {"success": True, **data}
64 |
--------------------------------------------------------------------------------
/server/api/v1/api.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, Response
2 | from flask_restful import Api, Resource, reqparse
3 | from server.app import app
4 | from server.api.v1 import api_tickets
5 | from server.api.v1 import api_admin
6 | from server.api.v1 import api_login
7 | from server.api.v1 import api_user
8 | import json
9 |
10 | # NOTE: all the following resources by default start with '/api/v1' so there's
11 | # no need to specify that
12 |
13 |
14 | class HelloWorld(Resource):
15 | def get(self):
16 | return {'success': False, 'message': "Please use post requests"}
17 |
18 | def post(self):
19 | return {'success': True}
20 |
21 |
22 |
23 | # Blueprint for /api/v1 requests
24 | api = Api(Blueprint('api', __name__))
25 |
26 | # Update in ServerHelper.tsx
27 |
28 | # Endpoint registration
29 | api.add_resource(HelloWorld, '') # This would be the default hostname/api/v1
30 | api.add_resource(api_tickets.TicketCreate, '/ticket/create')
31 | api.add_resource(api_tickets.TicketClaim, '/ticket/claim')
32 | api.add_resource(api_tickets.TicketUnclaim, '/ticket/unclaim')
33 | api.add_resource(api_tickets.TicketClose, '/ticket/close')
34 | api.add_resource(api_tickets.TicketCancel, '/ticket/cancel')
35 | api.add_resource(api_tickets.TicketRate, '/ticket/rate')
36 |
37 | api.add_resource(api_user.UserRetrieveUser, '/user/ticket')
38 | api.add_resource(api_user.UserRetrieveAdmin, '/user/tickets')
39 | api.add_resource(api_user.UserProfileUpdate, '/user/update')
40 |
41 | api.add_resource(api_admin.AdminPromote, '/admin/promote')
42 | api.add_resource(api_admin.AdminUpdate, '/admin/update')
43 | api.add_resource(api_admin.AdminGetSettings, '/admin/settings')
44 | api.add_resource(api_admin.AdminReset, '/admin/reset')
45 |
46 | # Login
47 | api.add_resource(api_login.ClientLogin, '/client/login')
48 | api.add_resource(api_login.ClientSettings, '/client')
--------------------------------------------------------------------------------
/server/api/v1/api_admin.py:
--------------------------------------------------------------------------------
1 | from flask_restful import Resource, reqparse
2 | from server.controllers.settings import *
3 | from server.controllers.users import *
4 | from server.controllers.tickets import ticket_stats
5 | from server.api.v1 import return_failure, return_success, require_login
6 | from typing import cast
7 | import json
8 |
9 | GET_PARSER = reqparse.RequestParser(bundle_errors=True)
10 |
11 | class AdminGetSettings(Resource):
12 | def get(self):
13 | return return_failure("Please use post requests")
14 |
15 | @require_login(GET_PARSER)
16 | def post(self, data, user):
17 | settings = get_all_settings(user)
18 | users = get_all_users(user)
19 | if (settings is None):
20 | return return_failure("no admin privileges")
21 | return return_success({
22 | 'settings': {s.key:s.value for s in settings},
23 | 'users': [u.json() for u in users],
24 | 'ticket_stats': ticket_stats()
25 | })
26 |
27 |
28 | class AdminReset(Resource):
29 | def get(self):
30 | return return_failure("Please use post requests")
31 |
32 | @require_login(GET_PARSER)
33 | def post(self, data, user):
34 | if (not user.admin_is):
35 | return return_failure("no admin privileges")
36 | delete_users_and_tickets(user)
37 | return return_success({})
38 |
39 |
40 | UPDATE_PARSER = reqparse.RequestParser(bundle_errors=True)
41 | UPDATE_PARSER.add_argument('data', help='data required', required=True)
42 |
43 | class AdminUpdate(Resource):
44 | def get(self):
45 | return return_failure("Please use post requests")
46 |
47 | @require_login(UPDATE_PARSER)
48 | def post(self, data, user):
49 | if (not user.admin_is):
50 | return return_failure("no admin privileges")
51 | users = get_all_users(user)
52 | settings = json.loads(data['data'])
53 | for key in settings:
54 | set_setting(user, key, settings[key])
55 | settings = get_all_settings(user)
56 | return return_success({
57 | 'settings': {s.key:s.value for s in settings},
58 | 'users': [u.json() for u in users]
59 | })
60 |
61 | PROMOTE_PARSER = reqparse.RequestParser(bundle_errors=True)
62 | PROMOTE_PARSER.add_argument('user_id', help='user_id required', required=True)
63 | PROMOTE_PARSER.add_argument('type', help='type required', required=True)
64 | PROMOTE_PARSER.add_argument('value', help='value required', required=True)
65 |
66 | class AdminPromote(Resource):
67 | def get(self):
68 | return return_failure("Please use post requests")
69 |
70 | @require_login(PROMOTE_PARSER)
71 | def post(self, data, user):
72 | theuser = User.query.filter_by(id=data['user_id']).first()
73 | if (theuser is None):
74 | return return_failure("something went wrong")
75 | if data['type'] == "mentor":
76 | set_mentor(user, theuser, data['value'] == '1')
77 | elif data['type'] == "admin":
78 | set_admin(user, theuser, data['value'] == '1')
79 |
80 | settings = get_all_settings(user)
81 | users = get_all_users(user)
82 |
83 | return return_success({
84 | 'settings': {s.key:s.value for s in settings},
85 | 'users': [u.json() for u in users]
86 | })
--------------------------------------------------------------------------------
/server/api/v1/api_login.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote
2 | from flask_restful import Resource, reqparse
3 | from server.controllers.authentication import authenticate_firsttime, get_client
4 | from server.controllers.dopeauth import authenticate_with_github
5 | from server.controllers.users import set_name, set_admin, set_mentor
6 | from server.server_constants import *
7 | from server.controllers.settings import get_public_settings, get_setting
8 | from server.api.v1 import return_failure, return_success
9 |
10 | LOGIN_PARSER = reqparse.RequestParser(bundle_errors=True)
11 | LOGIN_PARSER.add_argument('email', help='email', required=True)
12 | LOGIN_PARSER.add_argument('token', help='token', required=True)
13 | LOGIN_PARSER.add_argument('uid', help="uid", required=True)
14 | LOGIN_PARSER.add_argument('name', help="name", required=False)
15 | LOGIN_PARSER.add_argument(
16 | 'mentor_key', help="mentor key optional", required=False)
17 |
18 |
19 | class ClientLogin(Resource):
20 | def post(self):
21 | """
22 | Login into reddlinks with dopeauth
23 | Return success if token and uid are created
24 | """
25 | data = LOGIN_PARSER.parse_args()
26 | email = data['email']
27 | uid = data['uid']
28 | token = data['token']
29 | if email == "GITHUB" and uid == "GITHUB":
30 | email = authenticate_with_github(token, get_setting(None, SETTING_GITHUB_CLIENT_ID, override=True), get_setting(None, SETTING_GITHUB_CLIENT_SECRET, override=True))
31 | if email is None:
32 | return return_failure("login credentials invalid")
33 | client = get_client(email)
34 | else:
35 | client = authenticate_firsttime(email, uid, token)
36 | if (client is None):
37 | # Unauthenticated
38 | return return_failure("login credentials invalid")
39 |
40 | if (not client.user.admin_is and client.user.email == get_setting(None, SETTING_MASTER_USER, override=True)):
41 | set_admin(None, client.user, True, override=True)
42 |
43 | # add mentor
44 | if ('mentor_key' in data and data['mentor_key'] == get_setting(None, SETTING_MENTOR_PASSWORD, override=True)):
45 | set_mentor(None, client.user, True, override=True)
46 |
47 | return return_success({"email": email, "token": client.token, "uid": client.uid})
48 |
49 |
50 | class ClientSettings(Resource):
51 | def post(self):
52 | settings = get_public_settings()
53 | return return_success({
54 | 'settings': {s.key: s.value for s in settings}
55 | })
56 |
--------------------------------------------------------------------------------
/server/api/v1/api_tickets.py:
--------------------------------------------------------------------------------
1 | from flask_restful import Resource, reqparse
2 | from server.controllers.tickets import *
3 | from server.controllers.users import *
4 | from server.api.v1 import return_failure, return_success, require_login
5 | from typing import cast
6 |
7 | CREATE_PARSER = reqparse.RequestParser(bundle_errors=True)
8 | CREATE_PARSER.add_argument('data',
9 | help='Needs data',
10 | required=True)
11 |
12 |
13 | class TicketCreate(Resource):
14 | def get(self):
15 | return return_failure("Please use post requests")
16 |
17 | @require_login(CREATE_PARSER)
18 | def post(self, data, user):
19 | ticket = create_ticket(user, data['data'])
20 | if (ticket is None):
21 | return return_failure("could not create ticket")
22 | return return_success({'ticket': ticket.json()})
23 |
24 |
25 | TICKET_PARSER = reqparse.RequestParser(bundle_errors=True)
26 | TICKET_PARSER.add_argument('ticket_id',
27 | help='Need ticket',
28 | required=True)
29 |
30 |
31 | class TicketClaim(Resource):
32 | def get(self):
33 | return return_failure("Please use post requests")
34 |
35 | @require_login(TICKET_PARSER)
36 | def post(self, data, user):
37 | ticket = get_ticket(data["ticket_id"])
38 | if ticket is None:
39 | return return_failure("ticket not found")
40 | if claim_ticket(user, ticket):
41 | return return_success({'ticket': ticket.json()})
42 | return return_failure("could not claim ticket")
43 |
44 |
45 | class TicketUnclaim(Resource):
46 | def get(self):
47 | return return_failure("Please use post requests")
48 |
49 | @require_login(TICKET_PARSER)
50 | def post(self, data, user):
51 | ticket = get_ticket(data["ticket_id"])
52 | if ticket is None:
53 | return return_failure("ticket not found")
54 | if unclaim_ticket(user, ticket):
55 | return return_success({'ticket': ticket.json()})
56 | return return_failure("could not unclaim ticket")
57 |
58 |
59 | class TicketClose(Resource):
60 | def get(self):
61 | return return_failure("Please use post requests")
62 |
63 | @require_login(TICKET_PARSER)
64 | def post(self, data, user):
65 | ticket = get_ticket(data["ticket_id"])
66 | if ticket is None:
67 | return return_failure("ticket not found")
68 | if close_ticket(user, ticket):
69 | return return_success({'ticket': ticket.json()})
70 | return return_failure("could not close ticket")
71 |
72 |
73 | class TicketCancel(Resource):
74 | def get(self):
75 | return return_failure("Please use post requests")
76 |
77 | @require_login(TICKET_PARSER)
78 | def post(self, data, user):
79 | ticket = get_ticket(data["ticket_id"])
80 | if ticket is None:
81 | return return_failure("ticket not found")
82 | if cancel_ticket(user, ticket):
83 | return return_success({'ticket': ticket.json()})
84 | return return_failure("could not cancel ticket")
85 |
86 |
87 | TICKET_RATE_PARSER = reqparse.RequestParser(bundle_errors=True)
88 | TICKET_RATE_PARSER.add_argument('ticket_id',
89 | help='Need ticket',
90 | required=True)
91 | TICKET_RATE_PARSER.add_argument('rating',
92 | help='Need to assign rating',
93 | required=True)
94 |
95 |
96 | class TicketRate(Resource):
97 | def get(self):
98 | return return_failure("Please use post requests")
99 |
100 | @require_login(TICKET_RATE_PARSER)
101 | def post(self, data, user):
102 | ticket = get_ticket(data["ticket_id"])
103 | if ticket is None:
104 | return return_failure("ticket not found")
105 | if rate_ticket(user, ticket, data["rating"]):
106 | return return_success({'ticket': ticket.json()})
107 | return return_failure("could not cancel ticket")
108 |
--------------------------------------------------------------------------------
/server/api/v1/api_user.py:
--------------------------------------------------------------------------------
1 | from flask_restful import Resource, reqparse
2 | from server.controllers.tickets import *
3 | from server.controllers.users import *
4 | from server.api.v1 import return_failure, return_success, require_login
5 |
6 | USER_PARSER = reqparse.RequestParser(bundle_errors=True)
7 |
8 |
9 | class UserRetrieveUser(Resource):
10 | def get(self):
11 | return return_failure("Please use post requests")
12 |
13 | @require_login(USER_PARSER)
14 | def post(self, data, user):
15 | ticket = user_get_ticket(user)
16 | tickets = get_claimable_tickets(user, override=True)
17 | total_tickets = len(tickets) if tickets is not None else 0
18 | current_position = total_tickets
19 | for i, t in enumerate(tickets):
20 | if t == ticket:
21 | current_position = i
22 | break
23 | return return_success({
24 | 'ticket': ticket.json() if ticket is not None else None,
25 | 'queue_position': current_position,
26 | 'queue_length': total_tickets,
27 | 'rankings': mentor_rankings(),
28 | 'user': user.json()
29 | })
30 |
31 |
32 | class UserRetrieveAdmin(Resource):
33 | def get(self):
34 | return return_failure("Please use post requests")
35 |
36 | @require_login(USER_PARSER)
37 | def post(self, data, user):
38 | ticket = user_get_claim_ticket(user)
39 | tickets = get_claimable_tickets(user)
40 | total_tickets = len(tickets) if tickets is not None else 0
41 | return return_success({
42 | 'ticket': ticket.json() if ticket is not None else None,
43 | 'tickets': [t.json() for t in tickets],
44 | 'queue_length': total_tickets,
45 | 'rankings': mentor_rankings(),
46 | 'user': user.json()
47 | })
48 |
49 |
50 | USER_UPDATE_PARSER = reqparse.RequestParser(bundle_errors=True)
51 | USER_UPDATE_PARSER.add_argument('name',
52 | help='Need name',
53 | required=True)
54 | USER_UPDATE_PARSER.add_argument('affiliation',
55 | help='Needs affiliation',
56 | required=True)
57 | USER_UPDATE_PARSER.add_argument('skills',
58 | help='Need skills',
59 | required=True)
60 |
61 |
62 | class UserProfileUpdate(Resource):
63 | @require_login(USER_UPDATE_PARSER)
64 | def post(self, data, user):
65 | set_name(user, data['name'])
66 | set_affiliation(user, data['affiliation'])
67 | set_skills(user, data['skills'])
68 | return return_success({
69 | 'user': user.json()
70 | })
71 |
--------------------------------------------------------------------------------
/server/api/v1/info.md:
--------------------------------------------------------------------------------
1 | This folder is for all API calls
2 |
3 | No actual database logic should occur here (you should NEVER import db)
4 | Rather, call functions in `controllers`, `helpers` and `models` to solve your problems
5 |
--------------------------------------------------------------------------------
/server/app.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from flask import Flask
3 | import os
4 |
5 | print("Initializing Backend")
6 | app = Flask(__name__, static_folder='build')
7 |
8 | if "SQLALCHEMY_DATABASE_URI" in os.environ:
9 | app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["SQLALCHEMY_DATABASE_URI"]
10 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
11 |
12 | #windows
13 | if os.getenv("SQLALCHEMY_DATABASE_URI") is not None:
14 | app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")
15 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
16 |
17 |
18 | # For heroku launching
19 | if "DATABASE_URL" in os.environ:
20 | app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]
21 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
22 |
23 | if "PORT" in os.environ:
24 | app.config["PORT"] = os.environ["PORT"]
25 |
26 | #windows
27 | if os.getenv("PORT") is not None:
28 | app.config["PORT"] = os.getenv("PORT")
29 |
30 | # Database (uncomment if needed)
31 | db = SQLAlchemy(app)
32 |
33 | if app.config["DEBUG"]:
34 | app.debug = True
35 | else:
36 | app.debug = False
37 |
38 | # Routes for heroku push
39 | @app.route('/')
40 | def root():
41 | return app.send_static_file('index.html')
42 |
43 | @app.route('/')
44 | def static_proxy(path):
45 | """
46 | First we attempt to see if a static file exists, otherwise we let the react
47 | client do the routing.
48 | """
49 | try:
50 | return app.send_static_file(path)
51 | except:
52 | return app.send_static_file('index.html')
53 |
--------------------------------------------------------------------------------
/server/cache.py:
--------------------------------------------------------------------------------
1 | """
2 | Caching!
3 | """
4 |
5 | from werkzeug.contrib.cache import SimpleCache
6 |
7 | CACHE_TIMEOUT = 300 # 6 minute cache
8 | cache = SimpleCache()
9 |
10 |
11 | class should_cache_request(object):
12 |
13 | def __init__(self, timeout=None):
14 | self.timeout = timeout or CACHE_TIMEOUT
15 |
16 | def __call__(self, f):
17 | def decorator(*args, **kwargs):
18 | response = cache.get(request.path)
19 | if response is None:
20 | response = f(*args, **kwargs)
21 | cache.set(request.path, response, self.timeout)
22 | return response
23 | return decorator
24 |
25 |
26 | class should_cache_function(object):
27 |
28 | def __init__(self, name, timeout=None):
29 | self.name = "FUNCTION_CACHE__" + name
30 | self.timeout = timeout or CACHE_TIMEOUT
31 |
32 | def __call__(self, f):
33 | def decorator(*args, **kwargs):
34 | response = cache.get(self.name)
35 | if response is None:
36 | response = f(*args, **kwargs)
37 | cache.set(self.name, response, self.timeout)
38 | return response
39 | return decorator
40 |
--------------------------------------------------------------------------------
/server/controllers/authentication.py:
--------------------------------------------------------------------------------
1 | from server.controllers.dopeauth import authenticate_with_dopeauth
2 | from server.app import db
3 | from server.models.user import User
4 | from server.models.client import Client
5 | from server.models import add_to_db
6 |
7 |
8 | def authenticate_firsttime(email, uid, token):
9 | """
10 | Authenticates the code first time!
11 |
12 | returns client or None
13 | """
14 | # TODO(kevinfang): FALSE authentication should be TRUE unless in debug
15 | if(authenticate_with_dopeauth(email, uid, token, True)):
16 | return get_client(email)
17 | return None
18 |
19 | def get_client(email):
20 | user = User.query.filter_by(email=email).first()
21 | if user is None:
22 | user = User(None, email)
23 | else:
24 | user.sign_in()
25 | client = Client(user)
26 | if (add_to_db(client, others=[user], rollbackfunc=lambda:client.generate_uniques())):
27 | return client
28 | return None
29 |
30 |
31 | def authenticate(uid, token):
32 | """
33 | Authenticates with email and reddlinks token
34 | returns (True or False, user)
35 | """
36 | # TODO(kevinfang): make client uid unique
37 | client = Client.query.filter_by(uid=uid, token=token).first()
38 | if (client is not None):
39 | # Yay client confirmed!
40 | return True, client.user
41 | return False, None
42 |
--------------------------------------------------------------------------------
/server/controllers/cron.py:
--------------------------------------------------------------------------------
1 | from server.models.client import Client
2 | import datetime
3 | from server.models import remove_from_db
4 |
5 | def cron_job():
6 | try:
7 | clean_old_clients()
8 | except:
9 | pass
10 |
11 |
12 | def clean_old_clients():
13 | current_time = datetime.datetime.utcnow()
14 | one_month_ago = current_time - datetime.timedelta(weeks=4)
15 | remove_from_db(Client.query.filter(Client.date_created < one_month_ago).all())
16 | print("Cleaned old clients")
--------------------------------------------------------------------------------
/server/controllers/dopeauth.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from server.app import app
3 | import datetime
4 |
5 | DOPEAUTH_CACHE_STRICT = {}
6 | DOPEAUTH_CACHE = {}
7 |
8 |
9 | def authenticate_with_dopeauth(email, uid, token, strictAuth=True):
10 | """
11 | StrictAuth also checks callback url for another layer of security.
12 | Caches keys so we don't call the server so many times
13 |
14 | Returns true or false!
15 | """
16 | global DOPEAUTH_CACHE
17 | global DOPEAUTH_CACHE_STRICT
18 |
19 | if (not strictAuth and uid + "___" + token in DOPEAUTH_CACHE):
20 | return email == DOPEAUTH_CACHE[uid + "___" + token]
21 | if (strictAuth and uid + "___" + token in DOPEAUTH_CACHE_STRICT):
22 | return email == DOPEAUTH_CACHE_STRICT[uid + "___" + token]
23 |
24 | if(strictAuth):
25 | params = {
26 | "email": email,
27 | "uid": uid,
28 | "token": token,
29 | "callback": app.config["REACT_APP_SITEURL"] + "/login/auth"
30 | }
31 | else:
32 | params = {
33 | "email": email,
34 | "uid": uid,
35 | "token": token
36 | }
37 |
38 | r = requests.post(
39 | url="https://dopeauth.com/api/v1/site/verify", params=params)
40 | data = r.json()
41 | success = "success" in data and data["success"]
42 | if (success):
43 | if(strictAuth):
44 | DOPEAUTH_CACHE_STRICT[uid + "___" + token] = email
45 | DOPEAUTH_CACHE[uid + "___" + token] = email
46 | return success
47 |
48 | def authenticate_with_github(code, client_id, secret):
49 | """
50 | Returns the email or None
51 | """
52 |
53 | params = {
54 | "client_id": client_id,
55 | "client_secret": secret,
56 | "code": code
57 | }
58 | headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
59 | try:
60 | r = requests.post(url="https://github.com/login/oauth/access_token", params=params, headers=headers)
61 | data = r.json()
62 | if ("access_token" in data):
63 | headers = {'Content-type': 'application/json', 'Accept': 'application/json', 'Authorization': 'token ' + data['access_token']}
64 | r = requests.get(url="https://api.github.com/user/emails", headers=headers)
65 | data = r.json()
66 | for d in data:
67 | if d['primary']:
68 | return d['email']
69 | except:
70 | return None
71 |
72 | return None
--------------------------------------------------------------------------------
/server/controllers/info.md:
--------------------------------------------------------------------------------
1 | The controllers file is for holding the main logic of the application and should
2 | do all the database execution and such.
3 |
4 | `from server.app import app`
5 |
6 | Will help import the application and
7 |
8 | `from server.app import db`
9 |
10 | will import the database
11 |
--------------------------------------------------------------------------------
/server/controllers/settings.py:
--------------------------------------------------------------------------------
1 | from server.models.setting import Setting
2 | from server.app import db
3 | from server.server_constants import *
4 | import datetime
5 |
6 |
7 | def get_setting(user, key, override=False):
8 | if not override and not user.admin_is:
9 | return None
10 | setting = Setting.query.filter_by(key=key).first()
11 | if (setting is None):
12 | return None
13 | else:
14 | return setting.value
15 |
16 | def get_public_settings():
17 | """
18 | Returns all key:value pairings for settings that are free to use!
19 | """
20 | res = []
21 | for key in SETTINGS_PUBLIC:
22 | setting = Setting.query.filter_by(key=key).first()
23 | if (setting is not None):
24 | res.append(setting)
25 | return res
26 |
27 | def get_all_settings(user, override=False):
28 | if not override and not user.admin_is:
29 | return None
30 | return Setting.query.all()
31 |
32 | def set_setting(user, key, value, override=False):
33 | if not override and user and not user.admin_is:
34 | return False
35 | if (not override and key in SETTINGS_ENV_PERMENANT):
36 | return False
37 | setting = Setting.query.filter_by(key=key).first()
38 | if (setting is not None):
39 | setting.value = value
40 | setting.date_updated = datetime.datetime.now()
41 | db.session.commit()
42 | else:
43 | setting = Setting(key, value)
44 | db.session.add(setting)
45 | db.session.commit()
46 | return True
47 |
--------------------------------------------------------------------------------
/server/controllers/tickets.py:
--------------------------------------------------------------------------------
1 | from server.models.ticket import Ticket
2 | from server.models.user import User
3 | from server.app import db
4 | from typing import cast
5 | import datetime
6 | from sqlalchemy import or_, and_
7 | from server.cache import should_cache_function
8 |
9 |
10 | # Mentor rankings update every 60 seconds
11 | @should_cache_function("ticket_stats", 60)
12 | def ticket_stats():
13 | tickets = Ticket.query.filter(
14 | or_(Ticket.status == 3, Ticket.status == 5)).all()
15 | if len(tickets) == 0:
16 | return {
17 | 'average_wait': 0,
18 | 'average_claimed': 0,
19 | 'average_rating': 0
20 | }
21 |
22 | wait_total = 0
23 | claimed_total = 0
24 | rating_total = 0
25 | for ticket in tickets:
26 | wait_total += ticket.total_unclaimed_seconds
27 | claimed_total += ticket.total_claimed_seconds
28 | rating_total += ticket.rating
29 | return {
30 | 'average_wait': wait_total / len(tickets),
31 | 'average_claimed': claimed_total / len(tickets),
32 | 'average_rating': rating_total / len(tickets)
33 | }
34 |
35 | def get_claimable_tickets(user, override=False):
36 | if not user.mentor_is and not override:
37 | return []
38 | tickets = Ticket.query.filter(
39 | or_(Ticket.status == 0, Ticket.status == 2)).order_by(Ticket.id).all()
40 | return tickets
41 |
42 |
43 | def get_ticket(ticket_id):
44 | ticket = Ticket.query.filter_by(
45 | id=ticket_id).first()
46 | return ticket
47 |
48 |
49 | def create_ticket(user, data):
50 | """
51 | Creates ticket with a data dictionary field
52 | Returns ticket or None if failed
53 | """
54 | if (Ticket.query.filter(and_(Ticket.requestor == user, Ticket.status < 3)).count() > 0):
55 | return None
56 | ticket = Ticket(user, data)
57 | db.session.add(ticket)
58 | db.session.commit()
59 | return ticket
60 |
61 |
62 | def claim_ticket(user, ticket):
63 | """
64 | returns true if successful
65 | """
66 | # only mentors / admins can claim
67 | if (not user.mentor_is and not user.admin_is):
68 | return False
69 |
70 | now = datetime.datetime.now()
71 | if (ticket.status == 0 or ticket.status == 2):
72 | # ticket is currently able to be claimed
73 | ticket.total_unclaimed_seconds += (now -
74 | ticket.date_updated).total_seconds()
75 | ticket.claimant = user
76 | ticket.date_updated = now
77 | ticket.status = 1
78 | db.session.commit()
79 | return True
80 | return False
81 |
82 | def unclaim_ticket(user, ticket):
83 | now = datetime.datetime.now()
84 |
85 | # Claimant has to be same user and actually claimed (and not closed)
86 | if ticket.claimant != user or ticket.status != 1:
87 | return False
88 |
89 | ticket.total_claimed_seconds += (now-ticket.date_updated).total_seconds()
90 | ticket.claimant = None
91 | ticket.date_updated = now
92 | ticket.status = 2
93 | db.session.commit()
94 | return True
95 |
96 |
97 | def cancel_ticket(user, ticket):
98 | now = datetime.datetime.now()
99 |
100 | # since ticket was never claimed then we don't do anything
101 | # You can only cancel ticket if the ticket is not already dead
102 | if ticket.status < 3:
103 | # Only the requester (or admin) can close
104 | if (ticket.requestor != user and not user.admin_is):
105 | return False
106 | ticket.status = 4
107 | ticket.date_updated = now
108 | db.session.commit()
109 | return True
110 | return False
111 |
112 |
113 | def close_ticket(user, ticket):
114 | now = datetime.datetime.now()
115 | # only mentors can close
116 | if (not user.mentor_is and not user.admin_is):
117 | return False
118 | if (ticket.claimant == user):
119 | ticket.total_claimed_seconds += (now -
120 | ticket.date_updated).total_seconds()
121 | ticket.status = 3
122 | ticket.date_updated = now
123 | db.session.commit()
124 | return True
125 | return False
126 |
127 | def rate_ticket(user, ticket, rating):
128 | now = datetime.datetime.now()
129 |
130 | # Requestor has to be same user and actually closed
131 | if ticket.requestor != user or ticket.status != 3:
132 | return False
133 |
134 | ticket.rating = rating
135 | # Completely closed and rated now!
136 | ticket.status = 5
137 | ticket.date_updated = now
138 | db.session.commit()
139 | return True
--------------------------------------------------------------------------------
/server/controllers/users.py:
--------------------------------------------------------------------------------
1 | from server.models.ticket import Ticket
2 | from server.models.user import User
3 | from server.app import db
4 | from server.server_constants import *
5 | from server.controllers.settings import *
6 | from typing import cast
7 | import datetime
8 | from sqlalchemy import or_, and_
9 | from server.cache import should_cache_function
10 |
11 | def laplaceSmooth(totalRating, totalRatings):
12 | alpha = 6 # was 6
13 | beta = 2
14 | return ((totalRating + alpha)/(totalRatings + beta))
15 |
16 | # Mentor rankings update every 60 seconds
17 | @should_cache_function("mentor_rankings", 60)
18 | def mentor_rankings():
19 | users = User.query.filter_by(mentor_is=True)
20 | ret = []
21 | len_leaderboard = 10
22 | for user in users:
23 | tickets = Ticket.query.filter_by(claimant=user, status=5)
24 | totalRatings = 0
25 | totalRating = 0
26 | totalUnrated = 0
27 | for ticket in tickets:
28 | if (ticket.rating > 0):
29 | totalRating += ticket.rating
30 | totalRatings += 1
31 | else:
32 | totalUnrated += 1
33 | unrated_tickets = Ticket.query.filter_by(claimant=user, status=3).all()
34 | totalUnrated += len(unrated_tickets)
35 |
36 | if (totalRatings > 0):
37 | ret.append({"name": user.name, "rating": float("{:.1f}".format(
38 | totalRating/totalRatings)), "tickets": totalRatings + totalUnrated, "smooth_rating": laplaceSmooth(totalRating, totalRatings)})
39 |
40 | # return sorted(ret, key=(lambda x: -x["smooth_rating"]))
41 | # sort first by rating, then by number of tickets
42 | return sorted(ret, key=(lambda x: (-x["rating"], -x["tickets"])))[:len_leaderboard]
43 |
44 | def get_all_users(user, override=False):
45 | """
46 | Gets all users
47 | """
48 | if not override and not user.admin_is:
49 | return []
50 | return User.query.all()
51 |
52 |
53 | def set_mentor(admin_user, user, value, override=False):
54 | # You have to be admin
55 | if not override and not admin_user.admin_is:
56 | return False
57 | user.mentor_is = value
58 | db.session.commit()
59 | return True
60 |
61 |
62 | def set_admin(admin_user, user, value, override=False):
63 | # You have to be admin
64 | if not override and not admin_user.admin_is:
65 | return False
66 | # Master admin cannot be unadmined
67 | if value is False and get_setting(admin_user, SETTING_MASTER_USER) == user.email:
68 | return False
69 | user.admin_is = value
70 | db.session.commit()
71 | return True
72 |
73 |
74 | def set_name(user, name):
75 | user.name = name
76 | db.session.commit()
77 |
78 |
79 | def set_affiliation(user, affiliation):
80 | user.affiliation = affiliation
81 | db.session.commit()
82 |
83 |
84 | def set_skills(user, skills):
85 | user.skills = skills
86 | db.session.commit()
87 |
88 |
89 | def delete_users_and_tickets(user):
90 | """Deletes all non-admin users and tickets
91 |
92 | Arguments:
93 | user {User} -- admin user
94 |
95 | Returns:
96 | True if successful
97 | """
98 | if (not user.admin_is):
99 | return False
100 |
101 | for ticket in Ticket.query.all():
102 | db.session.delete(ticket)
103 | for user in User.query.all():
104 | if not user.admin_is:
105 | db.session.delete(user)
106 | db.session.commit()
107 | return True
108 |
109 |
110 | def user_get_ticket(user):
111 | """Gets the first ticket a user has requested and has not been successfully closed
112 | (either canceled or closed and rated)
113 |
114 | Arguments:
115 | user {User}
116 |
117 | Returns:
118 | Ticket or None
119 | """
120 |
121 | # Getting current ticket
122 | query = Ticket.query.filter(
123 | and_(Ticket.requestor == user, Ticket.status < 4))
124 | if (query.count() > 0):
125 | return query.first()
126 | # No current ticket
127 | return None
128 |
129 |
130 | def user_get_claim_ticket(user):
131 | """Gets the first ticket that a user has already claimed
132 |
133 | Arguments:
134 | user {User}
135 |
136 | Returns:
137 | Ticket or None
138 | """
139 | # Getting current ticket
140 | query = Ticket.query.filter(
141 | and_(Ticket.claimant == user, Ticket.status < 3))
142 | if (query.count() > 0):
143 | return query.first()
144 | # No current ticket
145 | return None
146 |
--------------------------------------------------------------------------------
/server/helpers.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 |
5 | def random_id_string(stringLength=6):
6 | """Generate a random string of letters and digits """
7 | available_characters = string.ascii_lowercase + string.digits
8 | return ''.join(random.choice(available_characters) for i in range(stringLength))
9 |
10 |
11 | def random_number_string(stringLength=6):
12 | """Generate a random string of letters and digits """
13 | available_characters = string.digits
14 | return ''.join(random.choice(available_characters) for i in range(stringLength))
15 |
--------------------------------------------------------------------------------
/server/models/__init__.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Table, Integer, Column, ForeignKey
2 | from sqlalchemy.ext.declarative import declarative_base
3 | from server.app import db
4 |
5 | Base = db.Model
6 |
7 | def remove_from_db(objs):
8 | """Removes objects from database
9 |
10 | Arguments:
11 | objs {List} -- of models
12 | """
13 | for obj in objs:
14 | db.session.delete(obj)
15 | db.session.commit()
16 |
17 | def update_db():
18 | try:
19 | db.session.commit()
20 | except:
21 | db.session.rollback()
22 |
23 | def add_to_db(obj, others=None,rollbackfunc=None):
24 | """Adds objects to database
25 |
26 | Arguments:
27 | obj {Model} -- Object wanting to add
28 |
29 | Keyword Arguments:
30 | others {List} -- List of other model objects (default: {None})
31 | rollbackfunc {Func} -- Function that should be called on rollback (default: {None})
32 |
33 | Returns:
34 | Boolean - Success or not successful
35 | """
36 | retry = 10
37 | committed = False
38 | while (not committed and retry > 0):
39 | try:
40 | db.session.add(obj)
41 | if (others):
42 | for o in others:
43 | db.session.add(o)
44 | db.session.commit()
45 | except exc.IntegrityError:
46 | db.session.rollback()
47 | if (rollbackfunc):
48 | rollbackfunc()
49 | else:
50 | retry = 0
51 | retry -= 1
52 | else:
53 | committed = True
54 | return committed
--------------------------------------------------------------------------------
/server/models/client.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime
2 | from sqlalchemy.orm import relationship
3 | from server.models import Base
4 | import datetime
5 | import secrets
6 |
7 |
8 | class Client(Base):
9 | __tablename__ = "clients"
10 | id = Column(Integer, primary_key=True,
11 | unique=True, autoincrement=True)
12 |
13 | user_id = Column(Integer, ForeignKey("users.id"))
14 | user = relationship("User", back_populates="clients")
15 |
16 | uid = Column(String, unique=True)
17 | token = Column(String)
18 |
19 | date_created = Column(DateTime, default=datetime.datetime.now)
20 |
21 | def __init__(self, user):
22 | self.user = user
23 | self.generate_uniques()
24 | self.token = secrets.token_hex(32)
25 |
26 | def generate_uniques(self):
27 | self.uid = secrets.token_hex(32)
28 |
--------------------------------------------------------------------------------
/server/models/setting.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime
2 | from sqlalchemy.orm import relationship
3 | from server.models import Base
4 | import datetime
5 | import secrets
6 |
7 |
8 | class Setting(Base):
9 | __tablename__ = "settings"
10 | id = Column(Integer, primary_key=True,
11 | unique=True, autoincrement=True)
12 |
13 | key = Column(String, unique=True, primary_key=True)
14 | value = Column(String)
15 |
16 | date_created = Column(DateTime, default=datetime.datetime.now)
17 | date_updated = Column(DateTime, default=datetime.datetime.now)
18 |
19 | def __init__(self, key, value):
20 | self.key = key
21 | self.value = value
--------------------------------------------------------------------------------
/server/models/ticket.py:
--------------------------------------------------------------------------------
1 | from server.helpers import random_id_string
2 | from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime
3 | from sqlalchemy.orm import relationship
4 | from server.models import Base
5 | import datetime
6 | import secrets
7 | import json
8 |
9 |
10 | class Ticket(Base):
11 | __tablename__ = "tickets"
12 | id = Column(Integer, primary_key=True,
13 | unique=True, autoincrement=True)
14 |
15 | # Person requesting the ticket
16 | requestor_id = Column(Integer, ForeignKey('users.id'))
17 | requestor = relationship("User", foreign_keys=[requestor_id])
18 |
19 | # Person claiming the ticket (must be mentor)
20 | claimant_id = Column(Integer, ForeignKey('users.id'))
21 | claimant = relationship("User", foreign_keys=[claimant_id])
22 |
23 | # The data in the request
24 | data = Column(String, default="\{\}")
25 |
26 | # Random unique link
27 | uid = Column(String, default="no location")
28 |
29 | # 0 = created, 1 = claimed, 2 = unclaimed, 3 = closed, 4 = canceled, 5 = closed AND rated
30 | status = Column(Integer, default=0)
31 |
32 | # The total amount of seconds time the ticket was claimed for
33 | total_claimed_seconds = Column(Integer, default=0)
34 |
35 | # The total amount of seconds time the ticket was unclaimed for
36 | total_unclaimed_seconds = Column(Integer, default=0)
37 |
38 | # Rating given by claimant after ticket is closed
39 | rating = Column(Integer, default=0)
40 |
41 | date_created = Column(DateTime, default=datetime.datetime.now)
42 | date_updated = Column(DateTime, default=datetime.datetime.now)
43 |
44 | def __init__(self, user, data):
45 | """Initializes a ticket object
46 |
47 | Arguments:
48 | user {User} -- the user requesting the ticket
49 | data {string} -- the data of the ticket as a string
50 | """
51 | self.requestor = user
52 | self.data = data
53 | self.uid = random_id_string(stringLength=12)
54 |
55 | def json(self):
56 | """Returns JSON of the ticket object
57 |
58 | Returns:
59 | JSON -- a dictionary of string: string pairings
60 | """
61 | now = datetime.datetime.now()
62 | return {
63 | "id": self.id,
64 | "data": json.loads(self.data),
65 | "uid": self.uid,
66 | "status": self.status,
67 | "requested_by": self.requestor.name,
68 | "claimed_by": self.claimant.name if self.claimant else "",
69 | "minutes": (now-self.date_created).total_seconds()//60
70 | }
71 |
--------------------------------------------------------------------------------
/server/models/user.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime
2 | from sqlalchemy.orm import relationship
3 | from server.models import Base, update_db
4 | import datetime
5 | import secrets
6 |
7 |
8 | class User(Base):
9 | __tablename__ = "users"
10 | id = Column(Integer, primary_key=True,
11 | unique=True, autoincrement=True)
12 |
13 | email = Column(String, unique=True, primary_key=True)
14 | name = Column(String)
15 |
16 | affiliation = Column(String)
17 |
18 | contact_info = Column(String)
19 |
20 | admin_is = Column(Boolean, default=False)
21 | mentor_is = Column(Boolean, default=False)
22 |
23 | skills = Column(String, default=";")
24 |
25 | clients = relationship("Client", back_populates="user")
26 |
27 | date_created = Column(DateTime, default=datetime.datetime.now)
28 | date_updated = Column(DateTime, default=datetime.datetime.now)
29 | date_last_signin = Column(DateTime, default=datetime.datetime.now)
30 |
31 | def __init__(self, name, email, org=None):
32 | self.name = name
33 | self.email = email
34 |
35 | def sign_in(self):
36 | self.date_last_signin = datetime.datetime.now()
37 | update_db()
38 |
39 | def json(self):
40 | return {
41 | 'id': self.id,
42 | 'email': self.email,
43 | 'name': self.name,
44 | 'admin_is': self.admin_is,
45 | 'mentor_is': self.mentor_is,
46 | 'skills': self.skills
47 | }
48 |
--------------------------------------------------------------------------------
/server/server_constants.py:
--------------------------------------------------------------------------------
1 | # In Types.tsx
2 | SETTING_APP_NAME = "app_name"
3 | SETTING_CONTACT_EMAIL = "app_contact_email"
4 | SETTING_CREATOR = "app_creator"
5 | SETTING_MASTER_USER = "readonly_master_user_email"
6 | SETTING_URL = "readonly_master_url"
7 | SETTING_MENTOR_PASSWORD = "mentor_password_key"
8 | SETTING_GITHUB_CLIENT_ID = "github_client_id"
9 | SETTING_GITHUB_CLIENT_SECRET = "github_client_secret"
10 | SETTING_QUEUE_ON = "queue_status"
11 | SETTING_OFFICIAL_MESSAGE = "queue_message"
12 | SETTING_JITSI_LINK = "jitsi_link"
13 | SETTING_LOCATIONS = "locations"
14 |
15 | SETTINGS_ENV_PERMENANT = [SETTING_MASTER_USER, SETTING_URL]
16 | SETTINGS_PUBLIC = [
17 | SETTING_APP_NAME,
18 | SETTING_CONTACT_EMAIL,
19 | SETTING_CREATOR,
20 | SETTING_QUEUE_ON,
21 | SETTING_OFFICIAL_MESSAGE,
22 | SETTING_URL,
23 | SETTING_JITSI_LINK,
24 | SETTING_GITHUB_CLIENT_ID,
25 | SETTING_LOCATIONS
26 | ]
27 |
28 | ALLDEFAULTSETTINGS = [
29 | SETTING_APP_NAME,
30 | SETTING_CONTACT_EMAIL,
31 | SETTING_JITSI_LINK,
32 | SETTING_CREATOR,
33 | SETTING_MENTOR_PASSWORD,
34 | SETTING_LOCATIONS,
35 | SETTING_GITHUB_CLIENT_SECRET
36 | ]
37 |
--------------------------------------------------------------------------------
/update_and_deploy.sh:
--------------------------------------------------------------------------------
1 | # We first build everything
2 | # Make sure you are in the correct folder!
3 | git pull
4 | yarn install
5 | cp .env client/.env
6 | (cd client && yarn install)
7 | (cd client && yarn build)
8 | rm -rf build
9 | mv client/build build
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | ansi-regex@^2.0.0:
6 | version "2.1.1"
7 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
8 | integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
9 |
10 | ansi-regex@^3.0.0:
11 | version "3.0.0"
12 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
13 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
14 |
15 | ansi-styles@^3.2.1:
16 | version "3.2.1"
17 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
18 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
19 | dependencies:
20 | color-convert "^1.9.0"
21 |
22 | async-limiter@~1.0.0:
23 | version "1.0.1"
24 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
25 | integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
26 |
27 | camelcase@^5.0.0:
28 | version "5.3.1"
29 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
30 | integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
31 |
32 | chalk@^2.4.2:
33 | version "2.4.2"
34 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
35 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
36 | dependencies:
37 | ansi-styles "^3.2.1"
38 | escape-string-regexp "^1.0.5"
39 | supports-color "^5.3.0"
40 |
41 | cliui@^4.0.0:
42 | version "4.1.0"
43 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
44 | integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
45 | dependencies:
46 | string-width "^2.1.1"
47 | strip-ansi "^4.0.0"
48 | wrap-ansi "^2.0.0"
49 |
50 | code-point-at@^1.0.0:
51 | version "1.1.0"
52 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
53 | integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
54 |
55 | color-convert@^1.9.0:
56 | version "1.9.3"
57 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
58 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
59 | dependencies:
60 | color-name "1.1.3"
61 |
62 | color-name@1.1.3:
63 | version "1.1.3"
64 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
65 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
66 |
67 | concurrently@^4.1.1:
68 | version "4.1.2"
69 | resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-4.1.2.tgz#1a683b2b5c41e9ed324c9002b9f6e4c6e1f3b6d7"
70 | integrity sha512-Kim9SFrNr2jd8/0yNYqDTFALzUX1tvimmwFWxmp/D4mRI+kbqIIwE2RkBDrxS2ic25O1UgQMI5AtBqdtX3ynYg==
71 | dependencies:
72 | chalk "^2.4.2"
73 | date-fns "^1.30.1"
74 | lodash "^4.17.15"
75 | read-pkg "^4.0.1"
76 | rxjs "^6.5.2"
77 | spawn-command "^0.0.2-1"
78 | supports-color "^4.5.0"
79 | tree-kill "^1.2.1"
80 | yargs "^12.0.5"
81 |
82 | cross-spawn@^6.0.0:
83 | version "6.0.5"
84 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
85 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
86 | dependencies:
87 | nice-try "^1.0.4"
88 | path-key "^2.0.1"
89 | semver "^5.5.0"
90 | shebang-command "^1.2.0"
91 | which "^1.2.9"
92 |
93 | date-fns@^1.30.1:
94 | version "1.30.1"
95 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
96 | integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
97 |
98 | decamelize@^1.2.0:
99 | version "1.2.0"
100 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
101 | integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
102 |
103 | end-of-stream@^1.1.0:
104 | version "1.4.4"
105 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
106 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
107 | dependencies:
108 | once "^1.4.0"
109 |
110 | error-ex@^1.3.1:
111 | version "1.3.2"
112 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
113 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
114 | dependencies:
115 | is-arrayish "^0.2.1"
116 |
117 | escape-string-regexp@^1.0.5:
118 | version "1.0.5"
119 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
120 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
121 |
122 | execa@^1.0.0:
123 | version "1.0.0"
124 | resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
125 | integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
126 | dependencies:
127 | cross-spawn "^6.0.0"
128 | get-stream "^4.0.0"
129 | is-stream "^1.1.0"
130 | npm-run-path "^2.0.0"
131 | p-finally "^1.0.0"
132 | signal-exit "^3.0.0"
133 | strip-eof "^1.0.0"
134 |
135 | find-up@^3.0.0:
136 | version "3.0.0"
137 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
138 | integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
139 | dependencies:
140 | locate-path "^3.0.0"
141 |
142 | function-bind@^1.1.1:
143 | version "1.1.1"
144 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
145 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
146 |
147 | get-caller-file@^1.0.1:
148 | version "1.0.3"
149 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
150 | integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
151 |
152 | get-stream@^4.0.0:
153 | version "4.1.0"
154 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
155 | integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
156 | dependencies:
157 | pump "^3.0.0"
158 |
159 | has-flag@^2.0.0:
160 | version "2.0.0"
161 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
162 | integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=
163 |
164 | has-flag@^3.0.0:
165 | version "3.0.0"
166 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
167 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
168 |
169 | has@^1.0.3:
170 | version "1.0.3"
171 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
172 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
173 | dependencies:
174 | function-bind "^1.1.1"
175 |
176 | hosted-git-info@^2.1.4:
177 | version "2.8.8"
178 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
179 | integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
180 |
181 | invert-kv@^2.0.0:
182 | version "2.0.0"
183 | resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
184 | integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
185 |
186 | is-arrayish@^0.2.1:
187 | version "0.2.1"
188 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
189 | integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
190 |
191 | is-core-module@^2.1.0:
192 | version "2.2.0"
193 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a"
194 | integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
195 | dependencies:
196 | has "^1.0.3"
197 |
198 | is-fullwidth-code-point@^1.0.0:
199 | version "1.0.0"
200 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
201 | integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
202 | dependencies:
203 | number-is-nan "^1.0.0"
204 |
205 | is-fullwidth-code-point@^2.0.0:
206 | version "2.0.0"
207 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
208 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
209 |
210 | is-stream@^1.1.0:
211 | version "1.1.0"
212 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
213 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
214 |
215 | isexe@^2.0.0:
216 | version "2.0.0"
217 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
218 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
219 |
220 | json-parse-better-errors@^1.0.1:
221 | version "1.0.2"
222 | resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
223 | integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
224 |
225 | lcid@^2.0.0:
226 | version "2.0.0"
227 | resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
228 | integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
229 | dependencies:
230 | invert-kv "^2.0.0"
231 |
232 | locate-path@^3.0.0:
233 | version "3.0.0"
234 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
235 | integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
236 | dependencies:
237 | p-locate "^3.0.0"
238 | path-exists "^3.0.0"
239 |
240 | lodash@^4.17.15:
241 | version "4.17.20"
242 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
243 | integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
244 |
245 | map-age-cleaner@^0.1.1:
246 | version "0.1.3"
247 | resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
248 | integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
249 | dependencies:
250 | p-defer "^1.0.0"
251 |
252 | mem@^4.0.0:
253 | version "4.3.0"
254 | resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178"
255 | integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==
256 | dependencies:
257 | map-age-cleaner "^0.1.1"
258 | mimic-fn "^2.0.0"
259 | p-is-promise "^2.0.0"
260 |
261 | mimic-fn@^2.0.0:
262 | version "2.1.0"
263 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
264 | integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
265 |
266 | nice-try@^1.0.4:
267 | version "1.0.5"
268 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
269 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
270 |
271 | normalize-package-data@^2.3.2:
272 | version "2.5.0"
273 | resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
274 | integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
275 | dependencies:
276 | hosted-git-info "^2.1.4"
277 | resolve "^1.10.0"
278 | semver "2 || 3 || 4 || 5"
279 | validate-npm-package-license "^3.0.1"
280 |
281 | npm-run-path@^2.0.0:
282 | version "2.0.2"
283 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
284 | integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
285 | dependencies:
286 | path-key "^2.0.0"
287 |
288 | number-is-nan@^1.0.0:
289 | version "1.0.1"
290 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
291 | integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
292 |
293 | once@^1.3.1, once@^1.4.0:
294 | version "1.4.0"
295 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
296 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
297 | dependencies:
298 | wrappy "1"
299 |
300 | os-locale@^3.0.0:
301 | version "3.1.0"
302 | resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
303 | integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
304 | dependencies:
305 | execa "^1.0.0"
306 | lcid "^2.0.0"
307 | mem "^4.0.0"
308 |
309 | p-defer@^1.0.0:
310 | version "1.0.0"
311 | resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
312 | integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
313 |
314 | p-finally@^1.0.0:
315 | version "1.0.0"
316 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
317 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
318 |
319 | p-is-promise@^2.0.0:
320 | version "2.1.0"
321 | resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
322 | integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==
323 |
324 | p-limit@^2.0.0:
325 | version "2.3.0"
326 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
327 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
328 | dependencies:
329 | p-try "^2.0.0"
330 |
331 | p-locate@^3.0.0:
332 | version "3.0.0"
333 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
334 | integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
335 | dependencies:
336 | p-limit "^2.0.0"
337 |
338 | p-try@^2.0.0:
339 | version "2.2.0"
340 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
341 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
342 |
343 | parse-json@^4.0.0:
344 | version "4.0.0"
345 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
346 | integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
347 | dependencies:
348 | error-ex "^1.3.1"
349 | json-parse-better-errors "^1.0.1"
350 |
351 | path-exists@^3.0.0:
352 | version "3.0.0"
353 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
354 | integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
355 |
356 | path-key@^2.0.0, path-key@^2.0.1:
357 | version "2.0.1"
358 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
359 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
360 |
361 | path-parse@^1.0.6:
362 | version "1.0.6"
363 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
364 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
365 |
366 | pify@^3.0.0:
367 | version "3.0.0"
368 | resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
369 | integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
370 |
371 | prettier@^1.19.1:
372 | version "1.19.1"
373 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
374 | integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
375 |
376 | pump@^3.0.0:
377 | version "3.0.0"
378 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
379 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
380 | dependencies:
381 | end-of-stream "^1.1.0"
382 | once "^1.3.1"
383 |
384 | read-pkg@^4.0.1:
385 | version "4.0.1"
386 | resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237"
387 | integrity sha1-ljYlN48+HE1IyFhytabsfV0JMjc=
388 | dependencies:
389 | normalize-package-data "^2.3.2"
390 | parse-json "^4.0.0"
391 | pify "^3.0.0"
392 |
393 | require-directory@^2.1.1:
394 | version "2.1.1"
395 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
396 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
397 |
398 | require-main-filename@^1.0.1:
399 | version "1.0.1"
400 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
401 | integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
402 |
403 | resolve@^1.10.0:
404 | version "1.19.0"
405 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
406 | integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
407 | dependencies:
408 | is-core-module "^2.1.0"
409 | path-parse "^1.0.6"
410 |
411 | rxjs@^6.5.2:
412 | version "6.6.3"
413 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
414 | integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
415 | dependencies:
416 | tslib "^1.9.0"
417 |
418 | safe-buffer@~5.1.0:
419 | version "5.1.2"
420 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
421 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
422 |
423 | "semver@2 || 3 || 4 || 5", semver@^5.5.0:
424 | version "5.7.1"
425 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
426 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
427 |
428 | set-blocking@^2.0.0:
429 | version "2.0.0"
430 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
431 | integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
432 |
433 | shebang-command@^1.2.0:
434 | version "1.2.0"
435 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
436 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
437 | dependencies:
438 | shebang-regex "^1.0.0"
439 |
440 | shebang-regex@^1.0.0:
441 | version "1.0.0"
442 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
443 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
444 |
445 | signal-exit@^3.0.0:
446 | version "3.0.3"
447 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
448 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
449 |
450 | spawn-command@^0.0.2-1:
451 | version "0.0.2-1"
452 | resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
453 | integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
454 |
455 | spdx-correct@^3.0.0:
456 | version "3.1.1"
457 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
458 | integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
459 | dependencies:
460 | spdx-expression-parse "^3.0.0"
461 | spdx-license-ids "^3.0.0"
462 |
463 | spdx-exceptions@^2.1.0:
464 | version "2.3.0"
465 | resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
466 | integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
467 |
468 | spdx-expression-parse@^3.0.0:
469 | version "3.0.1"
470 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
471 | integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
472 | dependencies:
473 | spdx-exceptions "^2.1.0"
474 | spdx-license-ids "^3.0.0"
475 |
476 | spdx-license-ids@^3.0.0:
477 | version "3.0.7"
478 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65"
479 | integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
480 |
481 | string-width@^1.0.1:
482 | version "1.0.2"
483 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
484 | integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
485 | dependencies:
486 | code-point-at "^1.0.0"
487 | is-fullwidth-code-point "^1.0.0"
488 | strip-ansi "^3.0.0"
489 |
490 | string-width@^2.0.0, string-width@^2.1.1:
491 | version "2.1.1"
492 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
493 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
494 | dependencies:
495 | is-fullwidth-code-point "^2.0.0"
496 | strip-ansi "^4.0.0"
497 |
498 | strip-ansi@^3.0.0, strip-ansi@^3.0.1:
499 | version "3.0.1"
500 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
501 | integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
502 | dependencies:
503 | ansi-regex "^2.0.0"
504 |
505 | strip-ansi@^4.0.0:
506 | version "4.0.0"
507 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
508 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
509 | dependencies:
510 | ansi-regex "^3.0.0"
511 |
512 | strip-eof@^1.0.0:
513 | version "1.0.0"
514 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
515 | integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
516 |
517 | supports-color@^4.5.0:
518 | version "4.5.0"
519 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
520 | integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=
521 | dependencies:
522 | has-flag "^2.0.0"
523 |
524 | supports-color@^5.3.0:
525 | version "5.5.0"
526 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
527 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
528 | dependencies:
529 | has-flag "^3.0.0"
530 |
531 | tree-kill@^1.2.1:
532 | version "1.2.2"
533 | resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
534 | integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
535 |
536 | tslib@^1.9.0:
537 | version "1.14.1"
538 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
539 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
540 |
541 | ultron@~1.1.0:
542 | version "1.1.1"
543 | resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
544 | integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
545 |
546 | validate-npm-package-license@^3.0.1:
547 | version "3.0.4"
548 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
549 | integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
550 | dependencies:
551 | spdx-correct "^3.0.0"
552 | spdx-expression-parse "^3.0.0"
553 |
554 | which-module@^2.0.0:
555 | version "2.0.0"
556 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
557 | integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
558 |
559 | which@^1.2.9:
560 | version "1.3.1"
561 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
562 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
563 | dependencies:
564 | isexe "^2.0.0"
565 |
566 | wrap-ansi@^2.0.0:
567 | version "2.1.0"
568 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
569 | integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
570 | dependencies:
571 | string-width "^1.0.1"
572 | strip-ansi "^3.0.1"
573 |
574 | wrappy@1:
575 | version "1.0.2"
576 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
577 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
578 |
579 | ws@3.3.2:
580 | version "3.3.2"
581 | resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.2.tgz#96c1d08b3fefda1d5c1e33700d3bfaa9be2d5608"
582 | integrity sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==
583 | dependencies:
584 | async-limiter "~1.0.0"
585 | safe-buffer "~5.1.0"
586 | ultron "~1.1.0"
587 |
588 | "y18n@^3.2.1 || ^4.0.0":
589 | version "4.0.1"
590 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
591 | integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
592 |
593 | yargs-parser@^11.1.1:
594 | version "11.1.1"
595 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
596 | integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
597 | dependencies:
598 | camelcase "^5.0.0"
599 | decamelize "^1.2.0"
600 |
601 | yargs@^12.0.5:
602 | version "12.0.5"
603 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
604 | integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
605 | dependencies:
606 | cliui "^4.0.0"
607 | decamelize "^1.2.0"
608 | find-up "^3.0.0"
609 | get-caller-file "^1.0.1"
610 | os-locale "^3.0.0"
611 | require-directory "^2.1.1"
612 | require-main-filename "^1.0.1"
613 | set-blocking "^2.0.0"
614 | string-width "^2.0.0"
615 | which-module "^2.0.0"
616 | y18n "^3.2.1 || ^4.0.0"
617 | yargs-parser "^11.1.1"
618 |
--------------------------------------------------------------------------------