├── 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 |
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
54 | Send confirmation E-Mail
55 |
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 |
66 | Send Password Reset
67 |
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 |
53 |
54 | Old Password
55 |
62 |
63 |
64 | New Password
65 |
72 |
73 |
74 |
75 | Reset My Password
76 |
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 |
71 | Email
72 |
79 |
80 |
81 | Reset My Password
82 |
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 |
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 |
74 | Details
75 |
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 | {distanceInWordsToNow(time)} ago
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 |
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 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ) : (
83 |
84 |
85 |
86 |
87 | onRemoveMessage(message.uid)}
90 | >
91 |
92 |
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 |
117 | Load Older Messages
118 |
119 | )}
120 |
121 | {messages && (
122 |
128 | )}
129 |
130 | {!loading && !messages && (
131 |
132 | There are no messages ...
133 |
134 | )}
135 |
136 | {!loading && (
137 |
147 |
148 | Send
149 |
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 |
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 |
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 | [](https://travis-ci.org/the-road-to-react-with-firebase/react-semantic-ui-firebase-authentication) [](https://slack-the-road-to-learn-react.wieruch.com/) [](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 | 
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 |
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 |
92 | Email
93 |
100 |
101 |
102 | Password
103 |
110 |
111 |
112 | Submit
113 |
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 |
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 |
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 |
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 | onUnlink(signInMethod.id)}
198 | disabled={onlyOneLeft}
199 | >
200 | Deactivate {signInMethod.id}
201 |
202 | ) : (
203 | onLink(signInMethod.provider)}
215 | >
216 | Link {signInMethod.id}
217 |
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 | onUnlink(signInMethod.id)}
256 | disabled={onlyOneLeft}
257 | >
258 | Deactivate {signInMethod.id}
259 |
260 |
261 |
262 | ) : (
263 |
265 |
266 | New Password
267 |
274 |
275 |
276 | Confirm Password
277 |
284 |
285 |
286 |
287 | Link {signInMethod.id}
288 |
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 |
--------------------------------------------------------------------------------