├── .gitignore ├── LICENSE ├── README.md ├── client ├── .env.example ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── setup.js ├── src │ ├── components │ │ ├── About.js │ │ ├── Dashboard.css │ │ ├── Dashboard.js │ │ ├── Login.js │ │ ├── SocialButtonList.css │ │ ├── SocialButtonList.js │ │ ├── SocialProfileList.css │ │ └── SocialProfileList.js │ ├── containers │ │ ├── App.css │ │ ├── App.js │ │ ├── Layout.css │ │ ├── Layout.js │ │ └── withAuthentication.js │ ├── firebase │ │ ├── auth.js │ │ ├── firebase.js │ │ └── index.js │ ├── index.css │ ├── index.js │ ├── initialButtonList.js │ └── registerServiceWorker.js └── yarn.lock ├── package.json ├── server.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | /misc 14 | /Tutorial 15 | Notes.md 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Esau Silva 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth Authentication with Firebase 2 | 3 | > React app to showcase OAuth Authentication with GitHub, Twitter and Facebook using Firebase 4 | 5 | This app has been bootstrapped with Create React App so you will need `create-react-app` installed. 6 | 7 | To install 8 | 9 | ``` 10 | $ npm install -g create-react-app 11 | ``` 12 | 13 | ## Usage 14 | 15 | **Install dependencies** 16 | 17 | ``` 18 | $ yarn 19 | $ cd client 20 | $ yarn 21 | $ cd .. 22 | ``` 23 | 24 | **Run development server** 25 | 26 | - From root directory 27 | 28 | ``` 29 | $ yarn dev:client 30 | ``` 31 | 32 | - From client directory 33 | 34 | ``` 35 | $ yarn start 36 | ``` 37 | 38 | **Production** 39 | 40 | ``` 41 | $ dev:server 42 | ``` 43 | 44 | This will create the production build and Node will serve the app, then you can visit http://localhost:5000/ 45 | 46 | ## Setup 47 | 48 | First create the required `.env` file, run 49 | 50 | ``` 51 | $ cd client && yarn setup 52 | ``` 53 | 54 | Before we begin working on the React app, we will need to create a project in Firebase, then setup new apps on GitHub, Twitter and Facebook. 55 | 56 | Follow the tutorial for detailed step-by-step guide. 57 | 58 | [https://esausilva.com/2018/08/13/react-oauth-authentication-with-firebase-tutorial/](https://esausilva.com/2018/08/13/react-oauth-authentication-with-firebase-tutorial/) 59 | 60 | ## Preview 61 | 62 | ![GIF](https://i.imgur.com/XqebRL8.gif) 63 | 64 | ## Giving Back 65 | 66 | If you would like to support my work and the time I put into making tutorials, consider getting me a coffee by clicking on the image below. I would really appreciate it! 67 | 68 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/black_img.png)](https://www.buymeacoffee.com/esausilva) 69 | 70 | -Esau 71 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_FIREBASE_API_KEY=AI...Qo 2 | REACT_APP_FIREBASE_AUTH_DOMAIN=app-name.firebaseapp.com 3 | REACT_APP_FIREBASE_DATABASE_URL=https://app-name.firebaseio.com 4 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-oauth", 3 | "version": "0.1.0", 4 | "author": "Esau Silva <@_esausilva>", 5 | "license": "MIT", 6 | "scripts": { 7 | "setup": "node setup.js", 8 | "start": "react-scripts start", 9 | "build": "react-scripts build", 10 | "test": "react-scripts test --env=jsdom", 11 | "eject": "react-scripts eject" 12 | }, 13 | "dependencies": { 14 | "firebase": "^7.14.0", 15 | "prop-types": "^15.7.2", 16 | "react": "^16.13.1", 17 | "react-delay": "^0.1.0", 18 | "react-dom": "^16.13.1", 19 | "react-router-dom": "^5.1.2", 20 | "react-scripts": "3.4.1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/esausilva/react-firebase-oauth.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/esausilva/react-firebase-oauth/issues" 28 | }, 29 | "keywords": [ 30 | "react", 31 | "firebase", 32 | "github", 33 | "twitter", 34 | "facebook", 35 | "authentication", 36 | "oauth" 37 | ], 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esausilva/react-firebase-oauth/74eb84d05b917aab2a6e2203b804d765a9b38c3c/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /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": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Copies the contents of .env.example to .env 4 | const fs = require('fs'); 5 | fs.createReadStream('.env.example').pipe(fs.createWriteStream('.env')); 6 | -------------------------------------------------------------------------------- /client/src/components/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Layout from '../containers/Layout'; 4 | 5 | const About = () => { 6 | return ( 7 | 8 |

About

9 |

10 | Bacon ipsum dolor amet tail landjaeger corned beef chuck hamburger, 11 | salami strip steak. Pancetta kielbasa ham hock andouille. Tail cupim 12 | burgdoggen salami bacon jerky shankle strip steak turkey. Drumstick 13 | shoulder pork loin, filet mignon cupim alcatra tongue jowl. Cupim 14 | tenderloin rump t-bone. Picanha turducken short loin jowl, landjaeger 15 | shoulder t-bone buffalo spare ribs salami pastrami tri-tip ground round 16 | alcatra. 17 |

18 |
19 | ); 20 | }; 21 | 22 | export default About; 23 | -------------------------------------------------------------------------------- /client/src/components/Dashboard.css: -------------------------------------------------------------------------------- 1 | .btn__logout { 2 | width: 70px; 3 | height: 33px; 4 | font-size: 0.7rem; 5 | background-color: rgb(172, 43, 43); 6 | } 7 | 8 | .btn__logout:hover { 9 | background-color: rgb(197, 63, 63); 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import buttonList from '../initialButtonList'; 5 | import Layout from '../containers/Layout'; 6 | import SocialButtonList from './SocialButtonList'; 7 | import SocialProfileList from './SocialProfileList'; 8 | import { auth } from '../firebase'; 9 | 10 | import './Dashboard.css'; 11 | 12 | class Dashboard extends Component { 13 | static propTypes = { 14 | providerData: PropTypes.arrayOf(PropTypes.object).isRequired 15 | }; 16 | 17 | // static defaultProps = { 18 | // providerData: [] 19 | // }; 20 | 21 | state = { 22 | buttonList: buttonList, 23 | providerData: this.props.providerData 24 | }; 25 | 26 | componentDidMount() { 27 | this.updateProviders(this.state.providerData); 28 | } 29 | 30 | handleCurrentProviders = providerData => { 31 | this.updateProviders(providerData); 32 | }; 33 | 34 | /** 35 | * Updates visibility of the Social Buttons and Social Profiles 36 | */ 37 | updateProviders = providerData => { 38 | let buttonList = { ...this.state.buttonList }; 39 | 40 | providerData.forEach(provider => { 41 | const providerName = provider.providerId.split('.')[0]; 42 | buttonList = this.updateButtonList(buttonList, providerName, false); 43 | }); 44 | 45 | this.setState({ buttonList, providerData }); 46 | }; 47 | 48 | /** 49 | * Unlinks a provider and if there are no more providers associated 50 | * with the account, delete the user account from Firebase 51 | */ 52 | handleUnlinkedProvider = (providerName, providerData) => { 53 | if (providerData.length < 1) { 54 | auth 55 | .getAuth() 56 | .currentUser.delete() 57 | .then(() => console.log('User Deleted')) 58 | .catch(() => console.error('Error deleting user')); 59 | } 60 | 61 | let buttonList = { ...this.state.buttonList }; 62 | buttonList = this.updateButtonList(buttonList, providerName, true); 63 | 64 | this.setState({ buttonList, providerData }); 65 | }; 66 | 67 | updateButtonList = (buttonList, providerName, visible) => ({ 68 | ...buttonList, 69 | [providerName]: { 70 | ...buttonList[providerName], 71 | visible 72 | } 73 | }); 74 | 75 | render() { 76 | return ( 77 | 78 |

Secure Area

79 | 84 |

85 | Connect Other Social Accounts 86 |

87 | 92 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | export default Dashboard; 104 | -------------------------------------------------------------------------------- /client/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import buttonList from '../initialButtonList'; 5 | import Layout from '../containers/Layout'; 6 | import SocialButtonList from './SocialButtonList'; 7 | import { auth } from '../firebase'; 8 | 9 | class Login extends Component { 10 | /** 11 | * Send the user to 'Dashboard' if is authenticated 12 | */ 13 | componentDidMount() { 14 | auth.getAuth().onAuthStateChanged(user => { 15 | if (user) { 16 | this.props.history.push('/dashboard'); 17 | } 18 | }); 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 |

Connect With

25 | 26 | About 27 |
28 | ); 29 | } 30 | } 31 | 32 | export default Login; 33 | -------------------------------------------------------------------------------- /client/src/components/SocialButtonList.css: -------------------------------------------------------------------------------- 1 | .btn__social--list { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | } 6 | 7 | .btn__social { 8 | width: 150px; 9 | } 10 | 11 | .btn--github { 12 | background-color: #1b1c1d; 13 | } 14 | 15 | .btn--github:hover { 16 | background-color: #232425; 17 | } 18 | 19 | .btn--twitter { 20 | background-color: #55acee; 21 | } 22 | 23 | .btn--twitter:hover { 24 | background-color: rgb(67, 158, 228); 25 | } 26 | 27 | .btn--facebook { 28 | background-color: #3b5998; 29 | } 30 | 31 | .btn--facebook:hover { 32 | background-color: rgb(45, 77, 146); 33 | } 34 | -------------------------------------------------------------------------------- /client/src/components/SocialButtonList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | import './SocialButtonList.css'; 6 | 7 | const propTypes = { 8 | buttonList: PropTypes.shape({ 9 | github: PropTypes.shape({ 10 | visible: PropTypes.bool.isRequired, 11 | provider: PropTypes.func.isRequired 12 | }), 13 | twitter: PropTypes.shape({ 14 | visible: PropTypes.bool.isRequired, 15 | provider: PropTypes.func.isRequired 16 | }), 17 | facebook: PropTypes.shape({ 18 | visible: PropTypes.bool.isRequired, 19 | provider: PropTypes.func.isRequired 20 | }) 21 | }).isRequired, 22 | auth: PropTypes.func.isRequired, 23 | currentProviders: PropTypes.func 24 | }; 25 | 26 | const defaultProps = { 27 | currentProviders: null 28 | }; 29 | 30 | const SocialButtonList = ({ history, buttonList, auth, currentProviders }) => { 31 | /** 32 | * Handles successfull authentication. 33 | * 'currentProviders' is null when the user is in Login page 34 | */ 35 | const authHandler = authData => { 36 | if (authData) { 37 | if (currentProviders === null) { 38 | history.push('/dashboard'); 39 | } else { 40 | currentProviders(authData.user.providerData); 41 | } 42 | } else { 43 | console.error('Error authenticating'); 44 | } 45 | }; 46 | 47 | /** 48 | * Authenticates the user with a social media provider. 49 | * Either creates a new user account in Firebase or links 50 | * a different provider to the same user account 51 | */ 52 | const authenticate = (e, provider) => { 53 | const providerOAuth = buttonList[provider].provider(); 54 | 55 | if (!auth().currentUser) { 56 | auth() 57 | .signInWithPopup(providerOAuth) 58 | .then(authHandler) 59 | .catch(err => console.error(err)); 60 | } else { 61 | auth() 62 | .currentUser.linkWithPopup(providerOAuth) 63 | .then(authHandler) 64 | .catch(err => console.error(err)); 65 | } 66 | }; 67 | 68 | const renderButtonList = provder => { 69 | const visible = buttonList[provder].visible; 70 | 71 | return ( 72 | 79 | ); 80 | }; 81 | 82 | return ( 83 |
84 | {Object.keys(buttonList).map(renderButtonList)} 85 |
86 | ); 87 | }; 88 | 89 | SocialButtonList.propTypes = propTypes; 90 | SocialButtonList.defaultProps = defaultProps; 91 | 92 | export default withRouter(SocialButtonList); 93 | -------------------------------------------------------------------------------- /client/src/components/SocialProfileList.css: -------------------------------------------------------------------------------- 1 | .btn__profiles--list { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | } 6 | 7 | .container__profile { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | margin: 10px; 12 | } 13 | 14 | .container__profile--photo { 15 | border-radius: 50%; 16 | width: 100px; 17 | height: 100px; 18 | } 19 | 20 | .container__profile--btn { 21 | font-size: 0.65rem; 22 | width: 80px; 23 | height: 28px; 24 | background-color: transparent; 25 | border: 1px solid red; 26 | color: red; 27 | } 28 | 29 | .container__profile--btn:hover { 30 | background-color: red; 31 | color: white; 32 | } 33 | -------------------------------------------------------------------------------- /client/src/components/SocialProfileList.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './SocialProfileList.css'; 5 | 6 | class SocialProfileList extends PureComponent { 7 | static propTypes = { 8 | auth: PropTypes.func.isRequired, 9 | providerData: PropTypes.arrayOf(PropTypes.object).isRequired, 10 | unlinkedProvider: PropTypes.func.isRequired 11 | }; 12 | 13 | /** 14 | * Unlinks a provider from the current user account 15 | */ 16 | handleProviderUnlink = async (e, provider) => { 17 | const { auth, unlinkedProvider } = this.props; 18 | 19 | if (window.confirm(`Do you really want to unlink ${provider}?`)) { 20 | const providers = await auth() 21 | .currentUser.unlink(`${provider}.com`) 22 | .catch(err => console.error(err)); 23 | 24 | unlinkedProvider(provider, providers.providerData); 25 | } 26 | }; 27 | 28 | renderProfileList = ({ providerId, photoURL }) => { 29 | const providerName = providerId.split('.')[0]; 30 | 31 | return ( 32 |
33 | {providerName} 38 |

{providerName}

39 | 45 |
46 | ); 47 | }; 48 | 49 | render() { 50 | return ( 51 | 52 |

53 | Connected Social Accounts 54 |

55 |
56 | {this.props.providerData.map(this.renderProfileList)} 57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | export default SocialProfileList; 64 | -------------------------------------------------------------------------------- /client/src/containers/App.css: -------------------------------------------------------------------------------- 1 | button { 2 | border: none; 3 | border-radius: 4px; 4 | height: 45px; 5 | background-color: #aaaaaa; 6 | color: white; 7 | font-weight: bold; 8 | font-size: 1rem; 9 | cursor: pointer; 10 | letter-spacing: 0.07rem; 11 | padding: 3px; 12 | margin: 5px; 13 | } 14 | 15 | button:hover { 16 | background-color: #929292; 17 | } 18 | 19 | .hidden { 20 | display: none; 21 | } 22 | 23 | .text--center { 24 | text-align: center; 25 | } 26 | -------------------------------------------------------------------------------- /client/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | 4 | import Login from '../components/Login'; 5 | import Dashboard from '../components/Dashboard'; 6 | import About from '../components/About'; 7 | import withAuthentication from '../containers/withAuthentication'; 8 | 9 | import './App.css'; 10 | 11 | class App extends Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /client/src/containers/Layout.css: -------------------------------------------------------------------------------- 1 | section { 2 | display: grid; 3 | grid-template-columns: 1fr minmax(360px, 991px) 1fr; 4 | grid-template-areas: '. h .' '. c .' '. f .'; 5 | grid-gap: 5px; 6 | margin-top: 20px; 7 | } 8 | 9 | header { 10 | grid-area: h; 11 | text-align: center; 12 | } 13 | 14 | main { 15 | grid-area: c; 16 | } 17 | 18 | main.content-center { 19 | display: grid; 20 | justify-items: center; 21 | } 22 | 23 | footer { 24 | grid-area: f; 25 | display: grid; 26 | justify-items: end; 27 | border-top: 1px solid rgba(34, 36, 38, 0.15); 28 | margin-top: 20px; 29 | } 30 | -------------------------------------------------------------------------------- /client/src/containers/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './Layout.css'; 5 | 6 | const propTypes = { 7 | children: PropTypes.node.isRequired, 8 | contentCenter: PropTypes.bool 9 | }; 10 | 11 | const defaultProps = { 12 | contentCenter: false 13 | }; 14 | 15 | const Layout = ({ children, contentCenter }) => { 16 | return ( 17 |
18 |
19 |

OAuth Authentication with Firebase

20 |
21 |
{children}
22 | 31 |
32 | ); 33 | }; 34 | 35 | Layout.propTypes = propTypes; 36 | Layout.defaultProps = defaultProps; 37 | 38 | export default Layout; 39 | -------------------------------------------------------------------------------- /client/src/containers/withAuthentication.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Delay from 'react-delay'; 3 | 4 | import { auth } from '../firebase'; 5 | 6 | /** 7 | * HOC that verifies user is authenticated before returning the 8 | * protected component 9 | */ 10 | export default WrappedComponent => { 11 | class WithAuthentication extends Component { 12 | state = { 13 | providerData: [] 14 | }; 15 | 16 | componentDidMount() { 17 | auth.getAuth().onAuthStateChanged(user => { 18 | if (user) { 19 | this.setState({ providerData: user.providerData }); 20 | } else { 21 | console.info('Must be authenticated'); 22 | this.props.history.push('/'); 23 | } 24 | }); 25 | } 26 | 27 | render() { 28 | return this.state.providerData.length > 0 ? ( 29 | 33 | ) : ( 34 | 35 |

Loading...

36 |
37 | ); 38 | } 39 | } 40 | 41 | return WithAuthentication; 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/firebase/auth.js: -------------------------------------------------------------------------------- 1 | import firebase from './firebase'; 2 | 3 | /** 4 | * Returns the Firebase Auth service 5 | */ 6 | export const getAuth = () => { 7 | return firebase.auth(); 8 | }; 9 | 10 | /** 11 | * Returns a new instance of GitHub auth provider. 12 | */ 13 | export const githubOAuth = () => { 14 | return new firebase.firebase_.auth.GithubAuthProvider(); 15 | }; 16 | 17 | /** 18 | * Returns a new instance of Twitter auth provider. 19 | */ 20 | export const twitterOAuth = () => { 21 | return new firebase.firebase_.auth.TwitterAuthProvider(); 22 | }; 23 | 24 | /** 25 | * Returns a new instance of Facebook auth provider. 26 | */ 27 | export const facebookOAuth = () => { 28 | return new firebase.firebase_.auth.FacebookAuthProvider(); 29 | }; 30 | -------------------------------------------------------------------------------- /client/src/firebase/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/auth'; 3 | // import 'firebase/database'; 4 | 5 | /** 6 | * Creates and initializes a Firebase app. 7 | */ 8 | const app = firebase.initializeApp({ 9 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 10 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN 11 | // databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL 12 | }); 13 | 14 | export default app; 15 | -------------------------------------------------------------------------------- /client/src/firebase/index.js: -------------------------------------------------------------------------------- 1 | import firebase from './firebase'; 2 | import * as auth from './auth'; 3 | 4 | export { firebase, auth }; 5 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | box-sizing: border-box; 4 | font-size: 100%; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | padding: 0; 16 | overflow-x: hidden; 17 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 18 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 19 | } 20 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import './index.css'; 5 | import App from './containers/App'; 6 | import registerServiceWorker from './registerServiceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | registerServiceWorker(); 10 | -------------------------------------------------------------------------------- /client/src/initialButtonList.js: -------------------------------------------------------------------------------- 1 | import { auth } from './firebase'; 2 | 3 | /** 4 | * Initial state of the Social Buttons 5 | */ 6 | export default { 7 | github: { 8 | visible: true, 9 | provider: () => { 10 | const provider = auth.githubOAuth(); 11 | provider.addScope('user'); 12 | return provider; 13 | } 14 | }, 15 | twitter: { 16 | visible: true, 17 | provider: () => auth.twitterOAuth() 18 | }, 19 | facebook: { 20 | visible: true, 21 | provider: () => auth.facebookOAuth() 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-oauth", 3 | "version": "1.0.0", 4 | "author": "Esau Silva <@_esausilva>", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev:client": "cd client && yarn start", 8 | "dev:server": "cd client && yarn build && cd .. && yarn start", 9 | "start": "node server.js", 10 | "heroku-postbuild": "cd client && npm install && npm install --only=dev --no-shrinkwrap && npm run build" 11 | }, 12 | "dependencies": { 13 | "express": "^4.17.1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/esausilva/react-firebase-oauth.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/esausilva/react-firebase-oauth/issues" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "firebase", 25 | "github", 26 | "twitter", 27 | "facebook", 28 | "authentication", 29 | "oauth" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | 4 | const app = express(); 5 | 6 | const PORT = process.env.PORT || 5000; 7 | 8 | // Serve any static files 9 | app.use(express.static(path.join(__dirname, 'client/build'))); 10 | 11 | // Handle React routing, return all requests to React app 12 | app.get('*', function(req, res) { 13 | res.sendFile(path.join(__dirname, 'client/build', 'index.html')); 14 | }); 15 | 16 | app.listen(PORT, () => console.log(`Listening on port ${PORT}`)); 17 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.7: 6 | version "1.3.7" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 8 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== 9 | dependencies: 10 | mime-types "~2.1.24" 11 | negotiator "0.6.2" 12 | 13 | array-flatten@1.1.1: 14 | version "1.1.1" 15 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 16 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 17 | 18 | body-parser@1.19.0: 19 | version "1.19.0" 20 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 21 | integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== 22 | dependencies: 23 | bytes "3.1.0" 24 | content-type "~1.0.4" 25 | debug "2.6.9" 26 | depd "~1.1.2" 27 | http-errors "1.7.2" 28 | iconv-lite "0.4.24" 29 | on-finished "~2.3.0" 30 | qs "6.7.0" 31 | raw-body "2.4.0" 32 | type-is "~1.6.17" 33 | 34 | bytes@3.1.0: 35 | version "3.1.0" 36 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 37 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== 38 | 39 | content-disposition@0.5.3: 40 | version "0.5.3" 41 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 42 | integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== 43 | dependencies: 44 | safe-buffer "5.1.2" 45 | 46 | content-type@~1.0.4: 47 | version "1.0.4" 48 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 49 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 50 | 51 | cookie-signature@1.0.6: 52 | version "1.0.6" 53 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 54 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 55 | 56 | cookie@0.4.0: 57 | version "0.4.0" 58 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" 59 | integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== 60 | 61 | debug@2.6.9: 62 | version "2.6.9" 63 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 64 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 65 | dependencies: 66 | ms "2.0.0" 67 | 68 | depd@~1.1.2: 69 | version "1.1.2" 70 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 71 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 72 | 73 | destroy@~1.0.4: 74 | version "1.0.4" 75 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 76 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 77 | 78 | ee-first@1.1.1: 79 | version "1.1.1" 80 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 81 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 82 | 83 | encodeurl@~1.0.2: 84 | version "1.0.2" 85 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 86 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 87 | 88 | escape-html@~1.0.3: 89 | version "1.0.3" 90 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 91 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 92 | 93 | etag@~1.8.1: 94 | version "1.8.1" 95 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 96 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 97 | 98 | express@^4.17.1: 99 | version "4.17.1" 100 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 101 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== 102 | dependencies: 103 | accepts "~1.3.7" 104 | array-flatten "1.1.1" 105 | body-parser "1.19.0" 106 | content-disposition "0.5.3" 107 | content-type "~1.0.4" 108 | cookie "0.4.0" 109 | cookie-signature "1.0.6" 110 | debug "2.6.9" 111 | depd "~1.1.2" 112 | encodeurl "~1.0.2" 113 | escape-html "~1.0.3" 114 | etag "~1.8.1" 115 | finalhandler "~1.1.2" 116 | fresh "0.5.2" 117 | merge-descriptors "1.0.1" 118 | methods "~1.1.2" 119 | on-finished "~2.3.0" 120 | parseurl "~1.3.3" 121 | path-to-regexp "0.1.7" 122 | proxy-addr "~2.0.5" 123 | qs "6.7.0" 124 | range-parser "~1.2.1" 125 | safe-buffer "5.1.2" 126 | send "0.17.1" 127 | serve-static "1.14.1" 128 | setprototypeof "1.1.1" 129 | statuses "~1.5.0" 130 | type-is "~1.6.18" 131 | utils-merge "1.0.1" 132 | vary "~1.1.2" 133 | 134 | finalhandler@~1.1.2: 135 | version "1.1.2" 136 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 137 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== 138 | dependencies: 139 | debug "2.6.9" 140 | encodeurl "~1.0.2" 141 | escape-html "~1.0.3" 142 | on-finished "~2.3.0" 143 | parseurl "~1.3.3" 144 | statuses "~1.5.0" 145 | unpipe "~1.0.0" 146 | 147 | forwarded@~0.1.2: 148 | version "0.1.2" 149 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 150 | integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= 151 | 152 | fresh@0.5.2: 153 | version "0.5.2" 154 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 155 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 156 | 157 | http-errors@1.7.2: 158 | version "1.7.2" 159 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" 160 | integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== 161 | dependencies: 162 | depd "~1.1.2" 163 | inherits "2.0.3" 164 | setprototypeof "1.1.1" 165 | statuses ">= 1.5.0 < 2" 166 | toidentifier "1.0.0" 167 | 168 | http-errors@~1.7.2: 169 | version "1.7.3" 170 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 171 | integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== 172 | dependencies: 173 | depd "~1.1.2" 174 | inherits "2.0.4" 175 | setprototypeof "1.1.1" 176 | statuses ">= 1.5.0 < 2" 177 | toidentifier "1.0.0" 178 | 179 | iconv-lite@0.4.24: 180 | version "0.4.24" 181 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 182 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 183 | dependencies: 184 | safer-buffer ">= 2.1.2 < 3" 185 | 186 | inherits@2.0.3: 187 | version "2.0.3" 188 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 189 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 190 | 191 | inherits@2.0.4: 192 | version "2.0.4" 193 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 194 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 195 | 196 | ipaddr.js@1.9.1: 197 | version "1.9.1" 198 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 199 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 200 | 201 | media-typer@0.3.0: 202 | version "0.3.0" 203 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 204 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 205 | 206 | merge-descriptors@1.0.1: 207 | version "1.0.1" 208 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 209 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 210 | 211 | methods@~1.1.2: 212 | version "1.1.2" 213 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 214 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 215 | 216 | mime-db@1.43.0: 217 | version "1.43.0" 218 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" 219 | integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== 220 | 221 | mime-types@~2.1.24: 222 | version "2.1.26" 223 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" 224 | integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== 225 | dependencies: 226 | mime-db "1.43.0" 227 | 228 | mime@1.6.0: 229 | version "1.6.0" 230 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 231 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 232 | 233 | ms@2.0.0: 234 | version "2.0.0" 235 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 236 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 237 | 238 | ms@2.1.1: 239 | version "2.1.1" 240 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 241 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 242 | 243 | negotiator@0.6.2: 244 | version "0.6.2" 245 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 246 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== 247 | 248 | on-finished@~2.3.0: 249 | version "2.3.0" 250 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 251 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 252 | dependencies: 253 | ee-first "1.1.1" 254 | 255 | parseurl@~1.3.3: 256 | version "1.3.3" 257 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 258 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 259 | 260 | path-to-regexp@0.1.7: 261 | version "0.1.7" 262 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 263 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 264 | 265 | proxy-addr@~2.0.5: 266 | version "2.0.6" 267 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" 268 | integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== 269 | dependencies: 270 | forwarded "~0.1.2" 271 | ipaddr.js "1.9.1" 272 | 273 | qs@6.7.0: 274 | version "6.7.0" 275 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 276 | integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== 277 | 278 | range-parser@~1.2.1: 279 | version "1.2.1" 280 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 281 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 282 | 283 | raw-body@2.4.0: 284 | version "2.4.0" 285 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" 286 | integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== 287 | dependencies: 288 | bytes "3.1.0" 289 | http-errors "1.7.2" 290 | iconv-lite "0.4.24" 291 | unpipe "1.0.0" 292 | 293 | safe-buffer@5.1.2: 294 | version "5.1.2" 295 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 296 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 297 | 298 | "safer-buffer@>= 2.1.2 < 3": 299 | version "2.1.2" 300 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 301 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 302 | 303 | send@0.17.1: 304 | version "0.17.1" 305 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 306 | integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== 307 | dependencies: 308 | debug "2.6.9" 309 | depd "~1.1.2" 310 | destroy "~1.0.4" 311 | encodeurl "~1.0.2" 312 | escape-html "~1.0.3" 313 | etag "~1.8.1" 314 | fresh "0.5.2" 315 | http-errors "~1.7.2" 316 | mime "1.6.0" 317 | ms "2.1.1" 318 | on-finished "~2.3.0" 319 | range-parser "~1.2.1" 320 | statuses "~1.5.0" 321 | 322 | serve-static@1.14.1: 323 | version "1.14.1" 324 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" 325 | integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== 326 | dependencies: 327 | encodeurl "~1.0.2" 328 | escape-html "~1.0.3" 329 | parseurl "~1.3.3" 330 | send "0.17.1" 331 | 332 | setprototypeof@1.1.1: 333 | version "1.1.1" 334 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 335 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== 336 | 337 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 338 | version "1.5.0" 339 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 340 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 341 | 342 | toidentifier@1.0.0: 343 | version "1.0.0" 344 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 345 | integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 346 | 347 | type-is@~1.6.17, type-is@~1.6.18: 348 | version "1.6.18" 349 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 350 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 351 | dependencies: 352 | media-typer "0.3.0" 353 | mime-types "~2.1.24" 354 | 355 | unpipe@1.0.0, unpipe@~1.0.0: 356 | version "1.0.0" 357 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 358 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 359 | 360 | utils-merge@1.0.1: 361 | version "1.0.1" 362 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 363 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 364 | 365 | vary@~1.1.2: 366 | version "1.1.2" 367 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 368 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 369 | --------------------------------------------------------------------------------