├── src ├── constants │ ├── roles.js │ └── routes.js ├── components │ ├── Messages │ │ ├── index.js │ │ ├── MessageList.js │ │ ├── MessageItem.js │ │ └── Messages.js │ ├── Users │ │ ├── index.js │ │ ├── UserItem.js │ │ └── UserList.js │ ├── Session │ │ ├── context.js │ │ ├── index.js │ │ ├── withAuthorization.js │ │ ├── withAuthentication.js │ │ └── withEmailVerification.js │ ├── Landing │ │ └── index.js │ ├── Firebase │ │ ├── index.js │ │ ├── context.js │ │ └── firebase.js │ ├── SignOut │ │ └── index.js │ ├── Home │ │ └── index.js │ ├── Admin │ │ └── index.js │ ├── App │ │ └── index.js │ ├── Navigation │ │ └── index.js │ ├── PasswordChange │ │ └── index.js │ ├── PasswordForget │ │ └── index.js │ ├── SignUp │ │ └── index.js │ ├── SignIn │ │ └── index.js │ └── Account │ │ └── index.js ├── index.css ├── index.js └── serviceWorker.js ├── .prettierrc ├── .travis.yml ├── public ├── favicon.ico ├── manifest.json └── index.html ├── firebase.json ├── .gitignore ├── .github └── FUNDING.yml ├── package.json └── README.md /src/constants/roles.js: -------------------------------------------------------------------------------- 1 | export const ADMIN = 'ADMIN'; 2 | -------------------------------------------------------------------------------- /src/components/Messages/index.js: -------------------------------------------------------------------------------- 1 | import Messages from './Messages'; 2 | 3 | export default Messages; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 70 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '10' 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm test 11 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-road-to-react-with-firebase/react-semantic-ui-firebase-authentication/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/Users/index.js: -------------------------------------------------------------------------------- 1 | import UserList from './UserList'; 2 | import UserItem from './UserItem'; 3 | 4 | export { UserList, UserItem }; 5 | -------------------------------------------------------------------------------- /src/components/Session/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AuthUserContext = React.createContext(null); 4 | 5 | export default AuthUserContext; 6 | -------------------------------------------------------------------------------- /src/components/Landing/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Landing = () => ( 4 |
5 |

Landing

6 |
7 | ); 8 | 9 | export default Landing; 10 | -------------------------------------------------------------------------------- /src/components/Firebase/index.js: -------------------------------------------------------------------------------- 1 | import FirebaseContext, { withFirebase } from './context'; 2 | import Firebase from './firebase'; 3 | 4 | export default Firebase; 5 | 6 | export { FirebaseContext, withFirebase }; 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/constants/routes.js: -------------------------------------------------------------------------------- 1 | export const LANDING = '/'; 2 | export const SIGN_UP = '/signup'; 3 | export const SIGN_IN = '/signin'; 4 | export const HOME = '/home'; 5 | export const ACCOUNT = '/account'; 6 | export const PASSWORD_FORGET = '/pw-forget'; 7 | export const ADMIN = '/admin'; 8 | export const ADMIN_DETAILS = '/admin/:id'; 9 | -------------------------------------------------------------------------------- /src/components/Firebase/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FirebaseContext = React.createContext(null); 4 | 5 | export const withFirebase = Component => props => ( 6 | 7 | {firebase => } 8 | 9 | ); 10 | 11 | export default FirebaseContext; 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Session/index.js: -------------------------------------------------------------------------------- 1 | import AuthUserContext from './context'; 2 | import withAuthentication from './withAuthentication'; 3 | import withAuthorization from './withAuthorization'; 4 | import withEmailVerification from './withEmailVerification'; 5 | 6 | export { 7 | AuthUserContext, 8 | withAuthentication, 9 | withAuthorization, 10 | withEmailVerification, 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/SignOut/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { withFirebase } from '../Firebase'; 4 | 5 | import { Menu } from 'semantic-ui-react'; 6 | 7 | const SignOutButton = ({ firebase }) => ( 8 | 9 | 10 | 11 | ); 12 | 13 | export default withFirebase(SignOutButton); 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 5 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 6 | 'Helvetica Neue', sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | 16 | .inline { 17 | display: inline-block; 18 | } 19 | -------------------------------------------------------------------------------- /.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 | .DS_Store 14 | 15 | # env 16 | .env 17 | .env.development 18 | .env.production 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | # log 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # firebase 30 | 31 | .firebase 32 | .firebaserc 33 | .idea 34 | -------------------------------------------------------------------------------- /src/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { withAuthorization, withEmailVerification } from '../Session'; 5 | import Messages from '../Messages'; 6 | 7 | const HomePage = () => ( 8 |
9 |

Home Page

10 |

The Home Page is accessible by every signed in user.

11 | 12 | 13 |
14 | ); 15 | 16 | const condition = authUser => !!authUser; 17 | 18 | export default compose( 19 | withEmailVerification, 20 | withAuthorization(condition), 21 | )(HomePage); 22 | -------------------------------------------------------------------------------- /src/components/Messages/MessageList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import MessageItem from './MessageItem'; 4 | 5 | import { Feed } from 'semantic-ui-react'; 6 | 7 | const MessageList = ({ 8 | authUser, 9 | messages, 10 | onEditMessage, 11 | onRemoveMessage, 12 | }) => ( 13 | 14 | {messages.map(message => ( 15 | 22 | ))} 23 | 24 | ); 25 | 26 | export default MessageList; 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: rwieruch 4 | patreon: # rwieruch 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'semantic-ui-css/semantic.min.css'; 4 | 5 | import './index.css'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | import App from './components/App'; 9 | import Firebase, { FirebaseContext } from './components/Firebase'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root'), 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: http://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-authentication", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "date-fns": "^1.30.1", 7 | "firebase": "^5.6.0", 8 | "react": "^16.6.3", 9 | "react-dom": "^16.6.3", 10 | "react-router-dom": "^4.3.1", 11 | "react-scripts": "3.1.0", 12 | "recompose": "^0.30.0", 13 | "semantic-ui-css": "^2.4.1", 14 | "semantic-ui-react": "^0.87.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom --passWithNoTests", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Admin/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import { compose } from 'recompose'; 4 | 5 | import { withAuthorization, withEmailVerification } from '../Session'; 6 | import { UserList, UserItem } from '../Users'; 7 | import * as ROLES from '../../constants/roles'; 8 | import * as ROUTES from '../../constants/routes'; 9 | 10 | import { Header } from 'semantic-ui-react'; 11 | 12 | const AdminPage = () => ( 13 |
14 |
Admin
15 |

The Admin Page is accessible by every signed in admin user.

16 | 17 | 18 | 19 | 20 | 21 |
22 | ); 23 | 24 | const condition = authUser => 25 | authUser && !!authUser.roles[ROLES.ADMIN]; 26 | 27 | export default compose( 28 | withEmailVerification, 29 | withAuthorization(condition), 30 | )(AdminPage); 31 | -------------------------------------------------------------------------------- /src/components/Session/withAuthorization.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { compose } from 'recompose'; 4 | 5 | import AuthUserContext from './context'; 6 | import { withFirebase } from '../Firebase'; 7 | import * as ROUTES from '../../constants/routes'; 8 | 9 | const withAuthorization = condition => Component => { 10 | class WithAuthorization extends React.Component { 11 | componentDidMount() { 12 | this.listener = this.props.firebase.onAuthUserListener( 13 | authUser => { 14 | if (!condition(authUser)) { 15 | this.props.history.push(ROUTES.SIGN_IN); 16 | } 17 | }, 18 | () => this.props.history.push(ROUTES.SIGN_IN), 19 | ); 20 | } 21 | 22 | componentWillUnmount() { 23 | this.listener(); 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | {authUser => 30 | condition(authUser) ? : null 31 | } 32 | 33 | ); 34 | } 35 | } 36 | 37 | return compose( 38 | withRouter, 39 | withFirebase, 40 | )(WithAuthorization); 41 | }; 42 | 43 | export default withAuthorization; 44 | -------------------------------------------------------------------------------- /src/components/Session/withAuthentication.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AuthUserContext from './context'; 4 | import { withFirebase } from '../Firebase'; 5 | 6 | const withAuthentication = Component => { 7 | class WithAuthentication extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | authUser: JSON.parse(localStorage.getItem('authUser')), 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | this.listener = this.props.firebase.onAuthUserListener( 18 | authUser => { 19 | localStorage.setItem('authUser', JSON.stringify(authUser)); 20 | this.setState({ authUser }); 21 | }, 22 | () => { 23 | localStorage.removeItem('authUser'); 24 | this.setState({ authUser: null }); 25 | }, 26 | ); 27 | } 28 | 29 | componentWillUnmount() { 30 | this.listener(); 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | return withFirebase(WithAuthentication); 43 | }; 44 | 45 | export default withAuthentication; 46 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | 4 | import Navigation from '../Navigation'; 5 | import LandingPage from '../Landing'; 6 | import SignUpPage from '../SignUp'; 7 | import SignInPage from '../SignIn'; 8 | import PasswordForgetPage from '../PasswordForget'; 9 | import HomePage from '../Home'; 10 | import AccountPage from '../Account'; 11 | import AdminPage from '../Admin'; 12 | 13 | import * as ROUTES from '../../constants/routes'; 14 | import { withAuthentication } from '../Session'; 15 | 16 | import { Container } from 'semantic-ui-react'; 17 | 18 | const App = () => ( 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | ); 37 | 38 | export default withAuthentication(App); 39 | -------------------------------------------------------------------------------- /src/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { AuthUserContext } from '../Session'; 5 | import SignOutButton from '../SignOut'; 6 | import * as ROUTES from '../../constants/routes'; 7 | import * as ROLES from '../../constants/roles'; 8 | 9 | import { Container, Menu } from 'semantic-ui-react'; 10 | 11 | const Navigation = () => ( 12 | 13 | {authUser => 14 | authUser ? ( 15 | 16 | ) : ( 17 | 18 | ) 19 | } 20 | 21 | ); 22 | 23 | const NavigationAuth = ({ authUser }) => ( 24 | 25 | 26 | 27 | 28 | 29 | {!!authUser.roles[ROLES.ADMIN] && ( 30 | 31 | )} 32 | 33 | 34 | 35 | ); 36 | 37 | const NavigationNonAuth = () => ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | 48 | export default Navigation; 49 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/Session/withEmailVerification.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import AuthUserContext from './context'; 4 | import { withFirebase } from '../Firebase'; 5 | 6 | import { Button } from 'semantic-ui-react' 7 | 8 | const needsEmailVerification = authUser => 9 | authUser && 10 | !authUser.emailVerified && 11 | authUser.providerData 12 | .map(provider => provider.providerId) 13 | .includes('password'); 14 | 15 | const withEmailVerification = Component => { 16 | class WithEmailVerification extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { isSent: false }; 21 | } 22 | 23 | onSendEmailVerification = () => { 24 | this.props.firebase 25 | .doSendEmailVerification() 26 | .then(() => this.setState({ isSent: true })); 27 | }; 28 | 29 | render() { 30 | return ( 31 | 32 | {authUser => 33 | needsEmailVerification(authUser) ? ( 34 |
35 | {this.state.isSent ? ( 36 |

37 | E-Mail confirmation sent: Check your E-Mails (Spam 38 | folder included) for a confirmation E-Mail. 39 | Refresh this page once you have confirmed your E-Mail. 40 |

41 | ) : ( 42 |

43 | Verify your E-Mail: Check your E-Mails (Spam folder 44 | included) for a confirmation E-Mail or send 45 | another confirmation E-Mail. 46 |

47 | )} 48 | 49 | 56 |
57 | ) : ( 58 | 59 | ) 60 | } 61 |
62 | ); 63 | } 64 | } 65 | 66 | return withFirebase(WithEmailVerification); 67 | }; 68 | 69 | export default withEmailVerification; 70 | -------------------------------------------------------------------------------- /src/components/Users/UserItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { withFirebase } from '../Firebase'; 4 | import { Card, Loader, Button } from 'semantic-ui-react'; 5 | 6 | class UserItem extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | loading: false, 12 | user: null, 13 | ...props.location.state, 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | if (this.state.user) { 19 | return; 20 | } 21 | 22 | this.setState({ loading: true }); 23 | 24 | this.props.firebase 25 | .user(this.props.match.params.id) 26 | .on('value', snapshot => { 27 | this.setState({ 28 | user: snapshot.val(), 29 | loading: false, 30 | }); 31 | }); 32 | } 33 | 34 | componentWillUnmount() { 35 | this.props.firebase.user(this.props.match.params.id).off(); 36 | } 37 | 38 | onSendPasswordResetEmail = () => { 39 | this.props.firebase.doPasswordReset(this.state.user.email); 40 | }; 41 | 42 | render() { 43 | const { user, loading } = this.state; 44 | 45 | return ( 46 | 47 | {loading ? ( 48 | 49 | ) : ( 50 | 51 | User: {user.uid} 52 | 53 | {user && ( 54 |
55 | 56 | 57 | Username: {user.username} 58 | 59 | {user.email} 60 |
61 | 68 |
69 |
70 | )} 71 |
72 |
73 | )} 74 |
75 | ); 76 | } 77 | } 78 | 79 | export default withFirebase(UserItem); 80 | -------------------------------------------------------------------------------- /src/components/PasswordChange/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { withFirebase } from '../Firebase'; 4 | 5 | import { Form, Message, Button } from 'semantic-ui-react'; 6 | 7 | const INITIAL_STATE = { 8 | passwordOne: '', 9 | passwordTwo: '', 10 | error: null, 11 | }; 12 | 13 | class PasswordChangeForm extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { ...INITIAL_STATE }; 18 | } 19 | 20 | onSubmit = event => { 21 | const { passwordOne } = this.state; 22 | 23 | this.props.firebase 24 | .doPasswordUpdate(passwordOne) 25 | .then(() => { 26 | this.setState({ ...INITIAL_STATE }); 27 | }) 28 | .catch(error => { 29 | this.setState({ error }); 30 | }); 31 | 32 | event.preventDefault(); 33 | }; 34 | 35 | onChange = event => { 36 | this.setState({ [event.target.name]: event.target.value }); 37 | }; 38 | 39 | render() { 40 | const { passwordOne, passwordTwo, error } = this.state; 41 | 42 | const isInvalid = 43 | passwordOne !== passwordTwo || passwordOne === ''; 44 | 45 | return ( 46 |
47 | {error && ( 48 | 49 |

{error.message}

50 |
51 | )} 52 | 53 | 54 | 55 | 62 | 63 | 64 | 65 | 72 | 73 | 74 | 77 |
78 | ); 79 | } 80 | } 81 | 82 | export default withFirebase(PasswordChangeForm); 83 | -------------------------------------------------------------------------------- /src/components/PasswordForget/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { withFirebase } from '../Firebase'; 5 | import * as ROUTES from '../../constants/routes'; 6 | 7 | import { 8 | Grid, 9 | Form, 10 | Header, 11 | Button, 12 | Message, 13 | } from 'semantic-ui-react'; 14 | 15 | const PasswordForgetPage = () => ( 16 | 17 | 18 |
19 | Password Forget 20 |
21 | 22 |
23 |
24 | ); 25 | 26 | const INITIAL_STATE = { 27 | email: '', 28 | error: null, 29 | }; 30 | 31 | class PasswordForgetFormBase extends Component { 32 | constructor(props) { 33 | super(props); 34 | 35 | this.state = { ...INITIAL_STATE }; 36 | } 37 | 38 | onSubmit = event => { 39 | const { email } = this.state; 40 | 41 | this.props.firebase 42 | .doPasswordReset(email) 43 | .then(() => { 44 | this.setState({ ...INITIAL_STATE }); 45 | }) 46 | .catch(error => { 47 | this.setState({ error }); 48 | }); 49 | 50 | event.preventDefault(); 51 | }; 52 | 53 | onChange = event => { 54 | this.setState({ [event.target.name]: event.target.value }); 55 | }; 56 | 57 | render() { 58 | const { email, error } = this.state; 59 | 60 | const isInvalid = email === ''; 61 | 62 | return ( 63 |
64 | {error && ( 65 | 66 |

{error.message}

67 |
68 | )} 69 |
70 | 71 | 72 | 79 | 80 | 83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | const PasswordForgetLink = () => ( 90 | Forgot Password? 91 | ); 92 | 93 | export default PasswordForgetPage; 94 | 95 | const PasswordForgetForm = withFirebase(PasswordForgetFormBase); 96 | 97 | export { PasswordForgetForm, PasswordForgetLink }; 98 | -------------------------------------------------------------------------------- /src/components/Users/UserList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { withFirebase } from '../Firebase'; 5 | import * as ROUTES from '../../constants/routes'; 6 | 7 | import { Header, Loader, Table, Button } from 'semantic-ui-react'; 8 | 9 | class UserList extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | loading: false, 15 | users: [], 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | this.setState({ loading: true }); 21 | 22 | this.props.firebase.users().on('value', snapshot => { 23 | const usersObject = snapshot.val(); 24 | 25 | const usersList = Object.keys(usersObject).map(key => ({ 26 | ...usersObject[key], 27 | uid: key, 28 | })); 29 | 30 | this.setState({ 31 | users: usersList, 32 | loading: false, 33 | }); 34 | }); 35 | } 36 | 37 | componentWillUnmount() { 38 | this.props.firebase.users().off(); 39 | } 40 | 41 | render() { 42 | const { users, loading } = this.state; 43 | 44 | return ( 45 |
46 |
Users
47 | {loading ? ( 48 | 49 | ) : ( 50 | 51 | 52 | 53 | ID 54 | Username 55 | Email Address 56 | Actions 57 | 58 | 59 | 60 | {users.map((user, i) => ( 61 | 62 | {user.uid} 63 | {user.username} 64 | {user.email} 65 | 66 | 76 | 77 | 78 | ))} 79 | 80 |
81 | )} 82 |
83 | ); 84 | } 85 | } 86 | 87 | export default withFirebase(UserList); 88 | -------------------------------------------------------------------------------- /src/components/Firebase/firebase.js: -------------------------------------------------------------------------------- 1 | import app from 'firebase/app'; 2 | import 'firebase/auth'; 3 | import 'firebase/database'; 4 | 5 | const config = { 6 | apiKey: process.env.REACT_APP_API_KEY, 7 | authDomain: process.env.REACT_APP_AUTH_DOMAIN, 8 | databaseURL: process.env.REACT_APP_DATABASE_URL, 9 | projectId: process.env.REACT_APP_PROJECT_ID, 10 | storageBucket: process.env.REACT_APP_STORAGE_BUCKET, 11 | messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, 12 | }; 13 | 14 | class Firebase { 15 | constructor() { 16 | app.initializeApp(config); 17 | 18 | /* Helper */ 19 | 20 | this.serverValue = app.database.ServerValue; 21 | this.emailAuthProvider = app.auth.EmailAuthProvider; 22 | 23 | /* Firebase APIs */ 24 | 25 | this.auth = app.auth(); 26 | this.db = app.database(); 27 | 28 | /* Social Sign In Method Provider */ 29 | 30 | this.googleProvider = new app.auth.GoogleAuthProvider(); 31 | this.facebookProvider = new app.auth.FacebookAuthProvider(); 32 | this.twitterProvider = new app.auth.TwitterAuthProvider(); 33 | } 34 | 35 | // *** Auth API *** 36 | 37 | doCreateUserWithEmailAndPassword = (email, password) => 38 | this.auth.createUserWithEmailAndPassword(email, password); 39 | 40 | doSignInWithEmailAndPassword = (email, password) => 41 | this.auth.signInWithEmailAndPassword(email, password); 42 | 43 | doSignInWithGoogle = () => 44 | this.auth.signInWithPopup(this.googleProvider); 45 | 46 | doSignInWithFacebook = () => 47 | this.auth.signInWithPopup(this.facebookProvider); 48 | 49 | doSignInWithTwitter = () => 50 | this.auth.signInWithPopup(this.twitterProvider); 51 | 52 | doSignOut = () => this.auth.signOut(); 53 | 54 | doPasswordReset = email => this.auth.sendPasswordResetEmail(email); 55 | 56 | doSendEmailVerification = () => 57 | this.auth.currentUser.sendEmailVerification({ 58 | url: process.env.REACT_APP_CONFIRMATION_EMAIL_REDIRECT, 59 | }); 60 | 61 | doPasswordUpdate = password => 62 | this.auth.currentUser.updatePassword(password); 63 | 64 | // *** Merge Auth and DB User API *** // 65 | 66 | onAuthUserListener = (next, fallback) => 67 | this.auth.onAuthStateChanged(authUser => { 68 | if (authUser) { 69 | this.user(authUser.uid) 70 | .once('value') 71 | .then(snapshot => { 72 | const dbUser = snapshot.val(); 73 | 74 | // default empty roles 75 | if (!dbUser.roles) { 76 | dbUser.roles = {}; 77 | } 78 | 79 | // merge auth and db user 80 | authUser = { 81 | uid: authUser.uid, 82 | email: authUser.email, 83 | emailVerified: authUser.emailVerified, 84 | providerData: authUser.providerData, 85 | ...dbUser, 86 | }; 87 | 88 | next(authUser); 89 | }); 90 | } else { 91 | fallback(); 92 | } 93 | }); 94 | 95 | // *** User API *** 96 | 97 | user = uid => this.db.ref(`users/${uid}`); 98 | 99 | users = () => this.db.ref('users'); 100 | 101 | // *** Message API *** 102 | 103 | message = uid => this.db.ref(`messages/${uid}`); 104 | 105 | messages = () => this.db.ref('messages'); 106 | } 107 | 108 | export default Firebase; 109 | -------------------------------------------------------------------------------- /src/components/Messages/MessageItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { distanceInWordsToNow } from 'date-fns'; 3 | import { Link } from 'react-router-dom'; 4 | import { Feed, Icon, Form, Button } from 'semantic-ui-react'; 5 | 6 | export const TimeAgo = ({ time }) => ( 7 | 8 | ); 9 | 10 | class MessageItem extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | editMode: false, 16 | editText: this.props.message.text, 17 | }; 18 | } 19 | 20 | onToggleEditMode = () => { 21 | this.setState(state => ({ 22 | editMode: !state.editMode, 23 | editText: this.props.message.text, 24 | })); 25 | }; 26 | 27 | onChangeEditText = event => { 28 | this.setState({ editText: event.target.value }); 29 | }; 30 | 31 | onSaveEditText = () => { 32 | this.props.onEditMessage(this.props.message, this.state.editText); 33 | 34 | this.setState({ editMode: false }); 35 | }; 36 | 37 | render() { 38 | const { authUser, message, onRemoveMessage } = this.props; 39 | const { editMode, editText } = this.state; 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | {message.userId} 47 | 48 | 49 | 50 | 51 | 52 | 53 | {editMode ? ( 54 |
55 | 56 | 61 | 62 |
63 | ) : ( 64 | 65 | {message.text}{' '} 66 | {message.editedAt && (Edited)} 67 | 68 | )} 69 |
70 | 71 | {authUser.uid === message.userId && ( 72 | 73 | {editMode ? ( 74 | 75 | 78 | 81 | 82 | ) : ( 83 | 84 | 87 | 93 | 94 | )} 95 | 96 | )} 97 | 98 |
99 |
100 | ); 101 | } 102 | } 103 | 104 | export default MessageItem; 105 | -------------------------------------------------------------------------------- /src/components/Messages/Messages.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { AuthUserContext } from '../Session'; 4 | import { withFirebase } from '../Firebase'; 5 | import MessageList from './MessageList'; 6 | 7 | import { 8 | Card, 9 | Message, 10 | Button, 11 | Loader, 12 | Form, 13 | Icon, 14 | } from 'semantic-ui-react'; 15 | 16 | class Messages extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | text: '', 22 | loading: false, 23 | messages: [], 24 | limit: 5, 25 | }; 26 | } 27 | 28 | componentDidMount() { 29 | this.onListenForMessages(); 30 | } 31 | 32 | onListenForMessages = () => { 33 | this.setState({ loading: true }); 34 | 35 | this.props.firebase 36 | .messages() 37 | .orderByChild('createdAt') 38 | .limitToLast(this.state.limit) 39 | .on('value', snapshot => { 40 | const messageObject = snapshot.val(); 41 | 42 | if (messageObject) { 43 | const messageList = Object.keys(messageObject).map(key => ({ 44 | ...messageObject[key], 45 | uid: key, 46 | })); 47 | 48 | this.setState({ 49 | messages: messageList, 50 | loading: false, 51 | }); 52 | } else { 53 | this.setState({ messages: null, loading: false }); 54 | } 55 | }); 56 | }; 57 | 58 | componentWillUnmount() { 59 | this.props.firebase.messages().off(); 60 | } 61 | 62 | onChangeText = event => { 63 | this.setState({ text: event.target.value }); 64 | }; 65 | 66 | onCreateMessage = (event, authUser) => { 67 | this.props.firebase.messages().push({ 68 | text: this.state.text, 69 | userId: authUser.uid, 70 | createdAt: this.props.firebase.serverValue.TIMESTAMP, 71 | }); 72 | 73 | this.setState({ text: '' }); 74 | 75 | event.preventDefault(); 76 | }; 77 | 78 | onEditMessage = (message, text) => { 79 | const { uid, ...messageSnapshot } = message; 80 | 81 | this.props.firebase.message(message.uid).set({ 82 | ...messageSnapshot, 83 | text, 84 | editedAt: this.props.firebase.serverValue.TIMESTAMP, 85 | }); 86 | }; 87 | 88 | onRemoveMessage = uid => { 89 | this.props.firebase.message(uid).remove(); 90 | }; 91 | 92 | onNextPage = () => { 93 | this.setState( 94 | state => ({ limit: state.limit + 5 }), 95 | this.onListenForMessages, 96 | ); 97 | }; 98 | 99 | render() { 100 | const { text, messages, loading } = this.state; 101 | 102 | return ( 103 | 104 | {authUser => ( 105 | 106 | 107 | 108 | {loading && } 109 | 110 | {!loading && messages && ( 111 | 119 | )} 120 | 121 | {messages && ( 122 | 128 | )} 129 | 130 | {!loading && !messages && ( 131 | 132 |

There are no messages ...

133 |
134 | )} 135 | 136 | {!loading && ( 137 |
139 | this.onCreateMessage(event, authUser) 140 | } 141 | > 142 | 147 | 150 | 151 | )} 152 |
153 |
154 |
155 | )} 156 |
157 | ); 158 | } 159 | } 160 | 161 | export default withFirebase(Messages); 162 | -------------------------------------------------------------------------------- /src/components/SignUp/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | 4 | import { withFirebase } from '../Firebase'; 5 | import * as ROUTES from '../../constants/routes'; 6 | import * as ROLES from '../../constants/roles'; 7 | import { 8 | Form, 9 | Button, 10 | Grid, 11 | Header, 12 | Message, 13 | Checkbox, 14 | } from 'semantic-ui-react'; 15 | 16 | const SignUpPage = () => ( 17 | 18 | 19 |
20 | Sign Up 21 |
22 | 23 |
24 |
25 | ); 26 | 27 | const INITIAL_STATE = { 28 | username: '', 29 | email: '', 30 | passwordOne: '', 31 | passwordTwo: '', 32 | isAdmin: false, 33 | error: null, 34 | }; 35 | 36 | const ERROR_CODE_ACCOUNT_EXISTS = 'auth/email-already-in-use'; 37 | 38 | const ERROR_MSG_ACCOUNT_EXISTS = ` 39 | An account with this E-Mail address already exists. 40 | Try to login with this account instead. If you think the 41 | account is already used from one of the social logins, try 42 | to sign in with one of them. Afterward, associate your accounts 43 | on your personal account page. 44 | `; 45 | 46 | class SignUpFormBase extends Component { 47 | constructor(props) { 48 | super(props); 49 | 50 | this.state = { ...INITIAL_STATE }; 51 | } 52 | 53 | onSubmit = event => { 54 | const { username, email, passwordOne, isAdmin } = this.state; 55 | const roles = {}; 56 | 57 | if (isAdmin) { 58 | roles[ROLES.ADMIN] = ROLES.ADMIN; 59 | } 60 | 61 | this.props.firebase 62 | .doCreateUserWithEmailAndPassword(email, passwordOne) 63 | .then(authUser => { 64 | // Create a user in your Firebase realtime database 65 | return this.props.firebase.user(authUser.user.uid).set({ 66 | username, 67 | email, 68 | roles, 69 | }); 70 | }) 71 | .then(() => { 72 | return this.props.firebase.doSendEmailVerification(); 73 | }) 74 | .then(() => { 75 | this.setState({ ...INITIAL_STATE }); 76 | this.props.history.push(ROUTES.HOME); 77 | }) 78 | .catch(error => { 79 | if (error.code === ERROR_CODE_ACCOUNT_EXISTS) { 80 | error.message = ERROR_MSG_ACCOUNT_EXISTS; 81 | } 82 | 83 | this.setState({ error }); 84 | }); 85 | 86 | event.preventDefault(); 87 | }; 88 | 89 | onChange = event => { 90 | this.setState({ [event.target.name]: event.target.value }); 91 | }; 92 | 93 | onChangeCheckbox = () => { 94 | this.setState({ isAdmin: !this.state.isAdmin }); 95 | }; 96 | 97 | render() { 98 | const { 99 | username, 100 | email, 101 | passwordOne, 102 | passwordTwo, 103 | isAdmin, 104 | error, 105 | } = this.state; 106 | 107 | const isInvalid = 108 | passwordOne !== passwordTwo || 109 | passwordOne === '' || 110 | email === '' || 111 | username === ''; 112 | 113 | return ( 114 |
115 | {error && ( 116 | 117 |

{error.message}

118 |
119 | )} 120 |
121 | 122 | 123 | 130 | 131 | 132 | 133 | 140 | 141 | 142 | 143 | 144 | 151 | 152 | 153 | 154 | 161 | 162 | 163 | 164 | 170 | 171 | 174 |
175 |
176 | ); 177 | } 178 | } 179 | 180 | const SignUpLink = () => ( 181 |

182 | Don't have an account? Sign Up 183 |

184 | ); 185 | 186 | const SignUpForm = withRouter(withFirebase(SignUpFormBase)); 187 | 188 | export default SignUpPage; 189 | 190 | export { SignUpForm, SignUpLink }; 191 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 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 http://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 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-semantic-ui-firebase-authentication 2 | 3 | [![Build Status](https://travis-ci.org/the-road-to-react-with-firebase/react-semantic-ui-firebase-authentication.svg?branch=master)](https://travis-ci.org/the-road-to-react-with-firebase/react-semantic-ui-firebase-authentication) [![Slack](https://slack-the-road-to-learn-react.wieruch.com/badge.svg)](https://slack-the-road-to-learn-react.wieruch.com/) [![Greenkeeper badge](https://badges.greenkeeper.io/the-road-to-react-with-firebase/react-semantic-ui-firebase-authentication.svg)](https://greenkeeper.io/) 4 | 5 | * [Tutorial](https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/) 6 | 7 | Related: 8 | 9 | * [Semantic UI with React Tutorial](https://www.robinwieruch.de/react-semantic-ui-tutorial/) 10 | 11 | ## Variations 12 | 13 | * [Only React Version](https://github.com/the-road-to-react-with-firebase/react-firebase-authentication) 14 | * [Redux Version](https://github.com/the-road-to-react-with-firebase/react-redux-firebase-authentication) 15 | * [MobX Version](https://github.com/the-road-to-react-with-firebase/react-mobx-firebase-authentication) 16 | * [Gatsby Version](https://github.com/the-road-to-react-with-firebase/react-gatsby-firebase-authentication) 17 | * [Firestore Version](https://github.com/the-road-to-react-with-firebase/react-firestore-authentication) 18 | 19 | ## Features 20 | 21 | * uses: 22 | * only React (create-react-app) 23 | * firebase 24 | * react-router 25 | * **semantic UI** 26 | * features: 27 | * Sign In 28 | * Sign Up 29 | * Sign Out 30 | * Password Forget 31 | * Password Change 32 | * Verification Email 33 | * Protected Routes with Authorization 34 | * Roles-based Authorization 35 | * Social Logins with Google, Facebook and Twitter 36 | * Linking of Social Logins on Account dashboard 37 | * Auth Persistence with Local Storage 38 | * Database with Users and Messages 39 | 40 | ## License 41 | 42 | ### Commercial license 43 | 44 | If you want to use this starter project to develop commercial sites, themes, projects, and applications, the Commercial license is the appropriate license. With this option, your source code is kept proprietary. Purchase an commercial license for different team sizes: 45 | 46 | * [1 Developer](https://gum.co/react-with-firebase-starter-pack-developer) 47 | * [Team of up to 8 Developers](https://gum.co/react-with-firebase-starter-pack-team) 48 | * [Unlimited Developers of an Organization](https://gum.co/react-with-firebase-starter-pack-organization) 49 | 50 | It grants you also access to the other starter projects in this GitHub organization. 51 | 52 | ### Open source license 53 | 54 | If you are creating an open source application under a license compatible with the [GNU GPL license v3](https://www.gnu.org/licenses/gpl-3.0.html), you may use this starter project under the terms of the GPLv3. 55 | 56 | ## Contributors 57 | 58 | * [John Muteti (iamuteti)](https://github.com/iamuteti) 59 | 60 | ## Installation 61 | 62 | * `git clone git@github.com:the-road-to-react-with-firebase/react-semantic-ui-firebase-authentication.git` 63 | * `cd react-semantic-ui-firebase-authentication` 64 | * `npm install` 65 | * `npm start` 66 | * visit http://localhost:3000 67 | 68 | Get an overview of Firebase, how to create a project, what kind of features Firebase offers, and how to navigate through the Firebase project dashboard in this [visual tutorial for Firebase](https://www.robinwieruch.de/firebase-tutorial/). 69 | 70 | ### Firebase Configuration 71 | 72 | * copy/paste your configuration from your Firebase project's dashboard into one of these files 73 | * *src/components/Firebase/firebase.js* file 74 | * *.env* file 75 | * *.env.development* and *.env.production* files 76 | 77 | The *.env* or *.env.development* and *.env.production* files could look like the following then: 78 | 79 | ``` 80 | REACT_APP_API_KEY=AIzaSyBtxZ3phPeXcsZsRTySIXa7n33NtQ 81 | REACT_APP_AUTH_DOMAIN=react-firebase-s2233d64f8.firebaseapp.com 82 | REACT_APP_DATABASE_URL=https://react-firebase-s2233d64f8.firebaseio.com 83 | REACT_APP_PROJECT_ID=react-firebase-s2233d64f8 84 | REACT_APP_STORAGE_BUCKET=react-firebase-s2233d64f8.appspot.com 85 | REACT_APP_MESSAGING_SENDER_ID=701928454501 86 | ``` 87 | 88 | ### Activate Sign-In Methods 89 | 90 | ![firebase-enable-google-social-login_640](https://user-images.githubusercontent.com/2479967/49687774-e0a31e80-fb42-11e8-9d8a-4b4c794134e6.jpg) 91 | 92 | * Email/Password 93 | * [Google](https://www.robinwieruch.de/react-firebase-social-login/) 94 | * [Facebook](https://www.robinwieruch.de/firebase-facebook-login/) 95 | * [Twitter](https://www.robinwieruch.de/firebase-twitter-login/) 96 | * [Troubleshoot](https://www.robinwieruch.de/react-firebase-social-login/) 97 | 98 | ### Activate Verification E-Mail 99 | 100 | * add a redirect URL for redirecting a user after an email verification into one of these files 101 | * *src/components/Firebase/firebase.js* file 102 | * *.env* file 103 | * *.env.development* and *.env.production* files 104 | 105 | The *.env* or *.env.development* and *.env.production* files could look like the following then (excl. the Firebase configuration). 106 | 107 | **Development:** 108 | 109 | ``` 110 | REACT_APP_CONFIRMATION_EMAIL_REDIRECT=http://localhost:3000 111 | ``` 112 | 113 | **Production:** 114 | 115 | ``` 116 | REACT_APP_CONFIRMATION_EMAIL_REDIRECT=https://mydomain.com 117 | ``` 118 | 119 | ### Security Rules 120 | 121 | ``` 122 | { 123 | "rules": { 124 | ".read": false, 125 | ".write": false, 126 | "users": { 127 | "$uid": { 128 | ".read": "$uid === auth.uid || root.child('users/'+auth.uid).child('roles').hasChildren(['ADMIN'])", 129 | ".write": "$uid === auth.uid || root.child('users/'+auth.uid).child('roles').hasChildren(['ADMIN'])" 130 | }, 131 | ".read": "root.child('users/'+auth.uid).child('roles').hasChildren(['ADMIN'])", 132 | ".write": "root.child('users/'+auth.uid).child('roles').hasChildren(['ADMIN'])" 133 | }, 134 | "messages": { 135 | ".indexOn": ["createdAt"], 136 | "$uid": { 137 | ".write": "data.exists() ? data.child('userId').val() === auth.uid : newData.child('userId').val() === auth.uid" 138 | }, 139 | ".read": "auth != null", 140 | ".write": "auth != null", 141 | }, 142 | } 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /src/components/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { compose } from 'recompose'; 4 | 5 | import { SignUpLink } from '../SignUp'; 6 | import { PasswordForgetLink } from '../PasswordForget'; 7 | import { withFirebase } from '../Firebase'; 8 | import * as ROUTES from '../../constants/routes'; 9 | 10 | import { 11 | Grid, 12 | Form, 13 | Button, 14 | Header, 15 | Icon, 16 | Message, 17 | Divider, 18 | } from 'semantic-ui-react'; 19 | 20 | const SignInPage = () => ( 21 | 22 | 23 |
24 | Sign In 25 |
26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 | ); 34 | 35 | const INITIAL_STATE = { 36 | email: '', 37 | password: '', 38 | error: null, 39 | }; 40 | 41 | const ERROR_CODE_ACCOUNT_EXISTS = 42 | 'auth/account-exists-with-different-credential'; 43 | 44 | const ERROR_MSG_ACCOUNT_EXISTS = ` 45 | An account with an E-Mail address to 46 | this social account already exists. Try to login from 47 | this account instead and associate your social accounts on 48 | your personal account page. 49 | `; 50 | 51 | class SignInFormBase extends Component { 52 | constructor(props) { 53 | super(props); 54 | 55 | this.state = { ...INITIAL_STATE }; 56 | } 57 | 58 | onSubmit = event => { 59 | const { email, password } = this.state; 60 | 61 | this.props.firebase 62 | .doSignInWithEmailAndPassword(email, password) 63 | .then(() => { 64 | this.setState({ ...INITIAL_STATE }); 65 | this.props.history.push(ROUTES.HOME); 66 | }) 67 | .catch(error => { 68 | this.setState({ error }); 69 | }); 70 | 71 | event.preventDefault(); 72 | }; 73 | 74 | onChange = event => { 75 | this.setState({ [event.target.name]: event.target.value }); 76 | }; 77 | 78 | render() { 79 | const { email, password, error } = this.state; 80 | 81 | const isInvalid = password === '' || email === ''; 82 | 83 | return ( 84 |
85 | {error && ( 86 | 87 |

{error.message}

88 |
89 | )} 90 |
91 | 92 | 93 | 100 | 101 | 102 | 103 | 110 | 111 | 114 | 115 | Or sign in with 116 | 117 |
118 | ); 119 | } 120 | } 121 | 122 | class SignInGoogleBase extends Component { 123 | constructor(props) { 124 | super(props); 125 | 126 | this.state = { error: null }; 127 | } 128 | 129 | onSubmit = event => { 130 | this.props.firebase 131 | .doSignInWithGoogle() 132 | .then(socialAuthUser => { 133 | // Create a user in your Firebase Realtime Database too 134 | return this.props.firebase.user(socialAuthUser.user.uid).set({ 135 | username: socialAuthUser.user.displayName, 136 | email: socialAuthUser.user.email, 137 | roles: {}, 138 | }); 139 | }) 140 | .then(() => { 141 | this.setState({ error: null }); 142 | this.props.history.push(ROUTES.HOME); 143 | }) 144 | .catch(error => { 145 | if (error.code === ERROR_CODE_ACCOUNT_EXISTS) { 146 | error.message = ERROR_MSG_ACCOUNT_EXISTS; 147 | } 148 | 149 | this.setState({ error }); 150 | }); 151 | 152 | event.preventDefault(); 153 | }; 154 | 155 | render() { 156 | const { error } = this.state; 157 | 158 | return ( 159 |
160 | 163 | 164 | {error && ( 165 | 166 |

{error.message}

167 |
168 | )} 169 |
170 | ); 171 | } 172 | } 173 | 174 | class SignInFacebookBase extends Component { 175 | constructor(props) { 176 | super(props); 177 | 178 | this.state = { error: null }; 179 | } 180 | 181 | onSubmit = event => { 182 | this.props.firebase 183 | .doSignInWithFacebook() 184 | .then(socialAuthUser => { 185 | // Create a user in your Firebase Realtime Database too 186 | return this.props.firebase.user(socialAuthUser.user.uid).set({ 187 | username: socialAuthUser.additionalUserInfo.profile.name, 188 | email: socialAuthUser.additionalUserInfo.profile.email, 189 | roles: {}, 190 | }); 191 | }) 192 | .then(() => { 193 | this.setState({ error: null }); 194 | this.props.history.push(ROUTES.HOME); 195 | }) 196 | .catch(error => { 197 | if (error.code === ERROR_CODE_ACCOUNT_EXISTS) { 198 | error.message = ERROR_MSG_ACCOUNT_EXISTS; 199 | } 200 | 201 | this.setState({ error }); 202 | }); 203 | 204 | event.preventDefault(); 205 | }; 206 | 207 | render() { 208 | const { error } = this.state; 209 | 210 | return ( 211 |
212 | 215 | 216 | {error && ( 217 | 218 |

{error.message}

219 |
220 | )} 221 |
222 | ); 223 | } 224 | } 225 | 226 | class SignInTwitterBase extends Component { 227 | constructor(props) { 228 | super(props); 229 | 230 | this.state = { error: null }; 231 | } 232 | 233 | onSubmit = event => { 234 | this.props.firebase 235 | .doSignInWithTwitter() 236 | .then(socialAuthUser => { 237 | // Create a user in your Firebase Realtime Database too 238 | return this.props.firebase.user(socialAuthUser.user.uid).set({ 239 | username: socialAuthUser.additionalUserInfo.profile.name, 240 | email: socialAuthUser.additionalUserInfo.profile.email, 241 | roles: {}, 242 | }); 243 | }) 244 | .then(() => { 245 | this.setState({ error: null }); 246 | this.props.history.push(ROUTES.HOME); 247 | }) 248 | .catch(error => { 249 | if (error.code === ERROR_CODE_ACCOUNT_EXISTS) { 250 | error.message = ERROR_MSG_ACCOUNT_EXISTS; 251 | } 252 | 253 | this.setState({ error }); 254 | }); 255 | 256 | event.preventDefault(); 257 | }; 258 | 259 | render() { 260 | const { error } = this.state; 261 | 262 | return ( 263 |
264 | 267 | 268 | {error && ( 269 | 270 |

{error.message}

271 |
272 | )} 273 |
274 | ); 275 | } 276 | } 277 | 278 | const SignInForm = compose( 279 | withRouter, 280 | withFirebase, 281 | )(SignInFormBase); 282 | 283 | const SignInGoogle = compose( 284 | withRouter, 285 | withFirebase, 286 | )(SignInGoogleBase); 287 | 288 | const SignInFacebook = compose( 289 | withRouter, 290 | withFirebase, 291 | )(SignInFacebookBase); 292 | 293 | const SignInTwitter = compose( 294 | withRouter, 295 | withFirebase, 296 | )(SignInTwitterBase); 297 | 298 | export default SignInPage; 299 | 300 | export { SignInForm, SignInGoogle, SignInFacebook, SignInTwitter }; 301 | -------------------------------------------------------------------------------- /src/components/Account/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'recompose'; 3 | 4 | import { 5 | AuthUserContext, 6 | withAuthorization, 7 | withEmailVerification, 8 | } from '../Session'; 9 | import { withFirebase } from '../Firebase'; 10 | import { PasswordForgetForm } from '../PasswordForget'; 11 | import PasswordChangeForm from '../PasswordChange'; 12 | 13 | import { 14 | Grid, 15 | Card, 16 | Header, 17 | Message, 18 | Form, 19 | Button, 20 | } from 'semantic-ui-react'; 21 | 22 | const SIGN_IN_METHODS = [ 23 | { 24 | id: 'password', 25 | provider: null, 26 | }, 27 | { 28 | id: 'google.com', 29 | provider: 'googleProvider', 30 | }, 31 | { 32 | id: 'facebook.com', 33 | provider: 'facebookProvider', 34 | }, 35 | { 36 | id: 'twitter.com', 37 | provider: 'twitterProvider', 38 | }, 39 | ]; 40 | 41 | const AccountPage = () => ( 42 | 43 | {authUser => ( 44 |
45 |
Account: {authUser.email}
46 | 47 | 48 | 49 | 50 | Reset Password 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | New Password 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | )} 71 |
72 | ); 73 | 74 | class LoginManagementBase extends Component { 75 | constructor(props) { 76 | super(props); 77 | 78 | this.state = { 79 | activeSignInMethods: [], 80 | error: null, 81 | }; 82 | } 83 | 84 | componentDidMount() { 85 | this.fetchSignInMethods(); 86 | } 87 | 88 | fetchSignInMethods = () => { 89 | this.props.firebase.auth 90 | .fetchSignInMethodsForEmail(this.props.authUser.email) 91 | .then(activeSignInMethods => 92 | this.setState({ activeSignInMethods, error: null }), 93 | ) 94 | .catch(error => this.setState({ error })); 95 | }; 96 | 97 | onSocialLoginLink = provider => { 98 | this.props.firebase.auth.currentUser 99 | .linkWithPopup(this.props.firebase[provider]) 100 | .then(this.fetchSignInMethods) 101 | .catch(error => this.setState({ error })); 102 | }; 103 | 104 | onDefaultLoginLink = password => { 105 | const credential = this.props.firebase.emailAuthProvider.credential( 106 | this.props.authUser.email, 107 | password, 108 | ); 109 | 110 | this.props.firebase.auth.currentUser 111 | .linkAndRetrieveDataWithCredential(credential) 112 | .then(this.fetchSignInMethods) 113 | .catch(error => this.setState({ error })); 114 | }; 115 | 116 | onUnlink = providerId => { 117 | this.props.firebase.auth.currentUser 118 | .unlink(providerId) 119 | .then(this.fetchSignInMethods) 120 | .catch(error => this.setState({ error })); 121 | }; 122 | 123 | render() { 124 | const { activeSignInMethods, error } = this.state; 125 | 126 | return ( 127 | 128 | 129 | Sign In Methods 130 | 131 | {error && ( 132 | 133 |

{error.message}

134 |
135 | )} 136 |
137 | {SIGN_IN_METHODS.map(signInMethod => { 138 | const onlyOneLeft = activeSignInMethods.length === 1; 139 | const isEnabled = activeSignInMethods.includes( 140 | signInMethod.id, 141 | ); 142 | 143 | return ( 144 | 145 | {signInMethod.id === 'password' ? ( 146 | 147 | 148 | 155 |
156 |
157 |
158 | ) : ( 159 | 166 | )} 167 |
168 | ); 169 | })} 170 |
171 |
172 |
173 |
174 | ); 175 | } 176 | } 177 | 178 | const SocialLoginToggle = ({ 179 | onlyOneLeft, 180 | isEnabled, 181 | signInMethod, 182 | onLink, 183 | onUnlink, 184 | }) => 185 | isEnabled ? ( 186 | 202 | ) : ( 203 | 218 | ); 219 | 220 | class DefaultLoginToggle extends Component { 221 | constructor(props) { 222 | super(props); 223 | 224 | this.state = { passwordOne: '', passwordTwo: '' }; 225 | } 226 | 227 | onSubmit = event => { 228 | event.preventDefault(); 229 | 230 | this.props.onLink(this.state.passwordOne); 231 | this.setState({ passwordOne: '', passwordTwo: '' }); 232 | }; 233 | 234 | onChange = event => { 235 | this.setState({ [event.target.name]: event.target.value }); 236 | }; 237 | 238 | render() { 239 | const { 240 | onlyOneLeft, 241 | isEnabled, 242 | signInMethod, 243 | onUnlink, 244 | } = this.props; 245 | 246 | const { passwordOne, passwordTwo } = this.state; 247 | 248 | const isInvalid = 249 | passwordOne !== passwordTwo || passwordOne === ''; 250 | 251 | return isEnabled ? ( 252 | 253 | 260 |
261 |
262 | ) : ( 263 |
264 | 265 | 266 | 267 | 274 | 275 | 276 | 277 | 284 | 285 | 286 | 289 |
290 | ); 291 | } 292 | } 293 | 294 | const LoginManagement = withFirebase(LoginManagementBase); 295 | 296 | const condition = authUser => !!authUser; 297 | 298 | export default compose( 299 | withEmailVerification, 300 | withAuthorization(condition), 301 | )(AccountPage); 302 | --------------------------------------------------------------------------------