Series: Building a Social Network with Flask, React & Stream – Part 14

10 min read
Spencer P.
Spencer P.
Published April 28, 2020

This article is the third installment of a tutorial series focused on how to create a full-stack application using Flask, React/Redux and Stream. In this article, we are going to finish implementing our authentication flow as well as integrating a basic profile component into our app. Be sure to check out the repo to follow along!

Getting Started

Last week we started working on the authentication flow by creating a registration component as well as working with React-Redux in managing state within our application. This week, we are going to create a login portal and 'settings' page to edit a user's information, such as their username, real name, and bio. Once we finish that, we set up a user profile page, as well as authenticated and unauthenticated routing using a concept known as Higher-Order Components. Finally, we create a custom 404 page to render if a user navigates to a page that isn't found on our site.

In and Out

Similar to our registration component, we start with constructing the server request for a user to sign in and out (in app/static/js/agent.jsx).

#...
const Auth = {
  #...
  login: (email, password) =>
        agent.post('/auth/login', { email, password }),
  logout: () =>
        agent.get('/auth/logout')
  #...

We wait to integrate the logout functionality to the settings component because that difference is more aesthetic than functional, so it makes more conceptual sense to complete these two together.

Reduced

Next, we need to update our Auth and Common reducers to handle dispatched events to change state. We will be adding our actions to the Auth reducer first (in app/static/js/reducers/auth.jsx).

export default (state = {}, action) => {
    switch (action.type) {
        case 'LOGIN':
        case 'REGISTER':
            return {
                ...state,
                inProgress: false,
                errors: action.error ? action.payload.errors : null
            };
        case 'LOGIN_PAGE_UNLOADED':
        case 'REGISTER_PAGE_UNLOADED':
            return {};
        case 'ASYNC_START':
            if (action.subtype ==='LOGIN' || action.subtype === 'REGISTER') {
                return { ...state, inProgress: true };

As you might expect, the login event is handled at the same time as the event for registration as they fulfill very similar functionality in the client.

After, we update our Common reducer to update the currentUser property for our app as well as handling any redirections (in app/static/js/reducers/common.jsx).

const defaultState = {
    appName: 'Offbrand'
};

export default (state = defaultState, action) => {
    switch(action.type) {
        case 'APP_LOAD':
            return {
                ...state,
                appLoaded: true,
                currentUser: action.payload ? action.payload.user : null
            };
        case 'REDIRECT':
            return {...state, redirectTo: null};
        case 'LOGOUT':

You'll notice the logout action is similar to that for login, but in contrast, the action erases any currentUser information from the client-side of the application.

Middle-Out

Now that our reducers are taken care of, we need to adjust our middleware to handle the local storage of the currentUser object in response to the login and logout actions (in app/static/js/middleware.jsx).

const promiseMiddleware = store => next => action => {
    if (isPromise(action.payload)) {
        store.dispatch({type: 'ASYNC_START', subtype: action.type});
        action.payload.then(
            res => {
                action.payload = res;
                store.dispatch(action);
            },
            error => {
                action.error = true;
                action.payload = error.response.body;
                store.dispatch(action);
            }
        );

In Component

With the completion of the "plumbing" of this page regarding requests and state, we can start creating the component itself. Once again, the page bears a striking resemblance to our registration page, handling a form and submitting it with the onSubmit function. We want to link our registration form to allow new users the opportunity to switch between the two components quickly. Additionally, as the state is updated to hold the input from our form, we want to clear those fields when a user navigates away from the page. The onUnload function clears that state when the user navigates from the page. We also include theListErrors component to return any issues that occur during server-side validation (in app/static/js/components/auth/Login.jsx).

import React, { Component } from 'react';
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import agent from '../../agent'

import ListErrors from '../ListErrors';

const mapStateToProps = state => ({ ...state.auth });

const mapDispatchToProps = dispatch => ({
    onChangeEmail: value =>
        dispatch({ type: 'UPDATE_FIELD_AUTH', key: 'email', value}),
    onChangePassword: value =>
        dispatch({ type: 'UPDATE_FIELD_AUTH', key: 'password', value}),
    onSubmit: (email, password) =>

Routes on Routes

Once we have the page created, we need to create a route in our router for it to be accessed. As with all our routes, this is done in the AppRouter component (in app/static/js/AppRouter.jsx).

#...
import Login from './components/auth/Login.jsx'

#...
class AppRouter extends Component {
  #...
  render() {
    #...
      <Switch>
          <Route exact path="/" component={() => <Home currentUser={this.props.currentUser} />} />
          <Route path="/register" component={Register} />
          <Route path="/login" component={Login} />
      </Switch>
    #...

Form Time

Now, our client-side is finished in regards to handling login and logout. We can now head to the server-side code to provide the endpoints and forms for the request. We'll start by defining the form (in app/auth/forms.py).

#...

class LoginForm(FlaskForm):
    class Meta:
        csrf = False

    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    password = PasswordField('Password', validators=[DataRequired()])

Authentic Views

Next, we need to build the views to process the request. After importing the form we created to the views file, we can use it in the login endpoint and validate the request information. This form returns a JSON list of each error to display in the ListErrors component. If everything is copacetic, the user is logged in with flask-login, which creates an authentication cookie, and a user JSON object is returned to the client. I've also included a logout view to destroy that cookie and finalize the erasure of any remnants of the authenticated user session when requested (in app/auth/views.py).

#...
from flask_login import login_user, login_required, \
    current_user, logout_user
from .forms import RegistrationForm, LoginForm
from .errors import forbidden
#...


@auth.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data.lower()).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user, False)

After that, our login portion is completed, and we can move onto the settings.

Next Steps

Again, we start from the agent to perform our requests. This creates a form object to send with our request including the username, name and about_me fields (in app/static/js/agents.jsx).

#...
const Auth = {
  #...
  save: (username, name, about_me) =>
        agent.post('/edit-profile', { username, name, about_me })
  #...
  

Settings The Settings

Now we need to create a new reducer to handle actions regarding state for our settings component (in app/static/js/reducers/settings.jsx).

export default (state = {}, action) => {
    switch (action.type) {
        case 'SETTINGS_SAVED':
            return {
                ...state,
                inProgress: false,
                errors: action.error ? action.payload.errors: null
            };
        case 'ASYNC_START':
            return {
                ...state,
                inProgress: true
            };
        default:
            return state;
    }
}

Update Store

As we have created a new reducer, we need to include it in our combineReducer function within the store (in app/static/js/store.jsx).

#...
import settings from './reducers/settings';
#...

const reducer = combineReducers({
    auth,
    common,
    settings,
    router: routerReducer
});

#...

One Component At A Time

Our settings component deals with state differently than in our previous components. Since any of the variables we change will invariably (get it?) change the state of the currentUser object, we need to handle these changes carefully. Updating state directly by overwriting it is a big no-no, so we create a brand new object with the new state given on submit, with unchanged values given from the pre-existing state. While this may seem confusing, we are simply creating a brand new state object based on the new information with fallback values for the old ones if it hasn't changed. This new state object is then set for the component and application (in app/static/js/components/auth/Settings.jsx).

import React, { Component } from 'react'
import agent from '../../agent'
import { connect } from 'react-redux'

import ListErrors from '../ListErrors'


const mapStateToProps = state => ({
    ...state.settings,
    currentUser: state.common.currentUser
});

const mapDispatchToProps = dispatch => ({
    onClickLogout: () => dispatch({ type: 'LOGOUT', payload: agent.Auth.logout() }),
    onSubmitForm: user =>

App Router

Now that our settings component is finished, we once again have to integrate it with our application router before we head back to complete the server code (in app/static/js/AppRouter.jsx)

#...
import Settings from './components/auth/Settings'
#...


class AppRouter extends Component {
  #...
  
  render() {
    return (
      #...
        <Switch>
            <Route exact path="/" component={() => <Home currentUser={this.props.currentUser} />} />
            <Route path="/register" component={Register} />
            <Route path="/login" component={Login} />
            <Route path="/settings" component={Settings} />
        </Switch>
      #...

Back To The Server

As we have a new form-based request, we need to create another form class to handle the validation of the fields being passed (in app/main/forms.py).

from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField
from wtforms.validators import Length, Regexp
from wtforms import ValidationError
from ..models import User


class EditProfileForm(FlaskForm):
    class Meta:
        csrf = False

    name = StringField('name', validators=[Length(0, 64)])
    username = StringField('username', validators=[Length(1, 64),
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,

As we are potentially updating the username field, which is a unique entity in our database, we also need to perform a quick check to ensure it hasn't already been taken.

Main Views

Next, we create our endpoint to handle the settings request (in app/main/views.py)

#...
from .forms import EditProfileForm
from .. import db

#...

@main.route('/edit-profile', methods=['POST'])
@login_required
def edit_profile():
    form = EditProfileForm()
    if form.validate_on_submit():
        if form.name.data:
            current_user.name = form.name.data
        if form.username.data:
            current_user.username = form.username.data

As some users only update select fields at any given time, we can't give an absolute "DataRequired" validation to our form; therefore, we need to handle it conditionally within the route itself. Also, after we are finished, we need to retrieve the newly updated user information so that it can be returned back to the client.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Main Errors

Since we are also starting to build out our endpoints in the main section of our app, it would also be prudent to build out a similar error folder for these requests (in app/main/errors.py).

from flask import jsonify
from . import main


@main.app_errorhandler(403)
def forbidden(e):
    response = jsonify({'errors': {'Forbidden:': [e]}})
    response.status_code = 403
    return response


@main.app_errorhandler(404)
def page_not_found(e):
    response = jsonify({'errors': {'Page Not Found': [e]}})
    response.status_code = 404

Some of you may have noticed that I placed the settings updates within the 'main' folder on the server while in the 'auth' folder in the client. To avoid a lengthy diatribe on naming conventions, it was because I prefer to keep routes that specifically handle authentication together on the server, while keeping the the client-side for anything that handles client identity.

Sanity Check #1

This concludes creating authentication components; therefore, now is a good time to stop and check that everything is running properly. When running the React app with npm run watch and the Flask server with flask run, you should see something similar when you navigate to the settings page after registering.

A Quick Profile

At this point, we have finished our authentication routes for our client. However, we haven't done anything with this information other than displaying a name/username on the home page and a gravatar image in the navbar. As with our last version, we want to have user profile pages for other users to see and, eventually, follow.

So Many Agents

We start by creating a request to be sent to the server with the username of the user that we are looking for. We have already created the skeleton of what the request on the client will look like within the authenticated header section, with a username preceded by an '@' symbol. We will create this new request in our agent file (in app/static/js/agent.jsx).

#...
const Profile = {
    get: username =>
        agent.get(`/user/${username}`),
};

export default {
    Auth,
    Profile
}

Profile Reducer

Next up is to create a reducer to handle dispatched events. This step takes the payload of the request that we have made and return the 'profile' of that user as a state (in app/static/js/reducers/profile.jsx).

export default (state = {}, action) => {
    switch (action.type) {
        case 'PROFILE_PAGE_LOADED':
            return {
                ...action.payload.profile
            };
        case 'PROFILE_PAGE_UNLOADED':
            return {};
        default:
            return state;
    }
};

Update store

As we have created a new reducer, we will once again need to integrate it into our store (in app/static/js/store.jsx).

#...
import profile from './reducers/profile';
#...

const reducer = combineReducers({
    auth,
    common,
    profile,
    settings,
    router: routerReducer
});

#...

Profile Component

Now we can create our user profile component. This component is slightly more elaborate than the previous ones that we have made, but we keep the content as bare-bones as possible to let us quickly walk through the process (in app/static/js/components/Profile.jsx).

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import agent from '../agent';
import { connect } from 'react-redux';


const mapStateToProps = state => ({
    currentUser: state.common.currentUser,
    profile: state.profile
});

const mapDispatchToProps = dispatch => ({
    onLoad: payload => dispatch({ type: 'PROFILE_PAGE_LOADED', payload }),
    onUnload: () => dispatch({ type: 'PROFILE_PAGE_UNLOADED'})
});

The first element that we need to create is the ComponentWillMount function. It uses our agent.Profile.get() function that we recently defined fetches the user profile of the page we are currently on. It uses the match parameters property for the username that is passed through the route to define which user we are looking for. We define this property through the route page later. After that, the isUser function checks to see whether or not the user that we are requesting is identical to the currentUser, which conditionally renders an Edit Profile Settings button that links to our settings route.

Routing

Next, we import the profile component and set up its path. We define the parameter for username after the colon within the Route (in app/static/js/AppRouter.jsx).

#...
import Profile from './components/User';

#...
class AppRouter extends Component {
  #...
  render() {
    return (
      #...
      <Switch>
        #...
        <Route path="/@:username" component={Profile}
      </Switch>
      #...

Main Views

Our next step is to create the view to return our user profile. As our to_json() class method returns sensitive information like an email address and a user's Stream access token, we want to remove those before we return an entry (in app/main/views.py).

#...
from .errors import page_not_found
from ..models import User
#...

@main.route('/user/<username>', methods=['GET'])
@login_required
def get_user(username):
    user = User.query.filter_by(username=username).first()
    if not user:
        return page_not_found
    user = user.to_json()
    user.pop('stream_token')
    user.pop('email')
    return {'profile': user}

#...

We can also use the custom errors we created in the errors.py file to return a more accurate error message to a requestor.

Conditional Routes

Now that we have a user profile, authentication methods for registration, login, and editing a user's settings, we still have a bit of a problem. First and foremost, we have no way of routing users based on their authentication state. For example, if an unauthenticated user goes to a profile page, they will break the isUser function. We can correct this issue with Higher-Order Components (HOC).

Authenticated Routes

HOCs wrap a given function or class with another function or class, creating the ability to provide conditional routing and rendering for similarly structured components that wish to accomplish the same things. This helps to avoid code repetition (DRY!). We create a HOC to wrap routes that are specifically for users that have already authenticated (in `app/static/js/AuthedRoute.jsx).

import React, { Component } from 'react';
import { Route, Redirect, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';

const mapStateToProps = (state) => ({
    currentUser: state.common.currentUser
});

class AuthedRoute extends Component {
    render() {

        const { component, ...rest } = this.props;
        const Component = component;

        return (

Unauthenticated Routes

Next, we do the same thing for components that should be only viewed by users that are unauthenticated, like login and registration (in `app/static/js/UnauthedRoute.jsx).

import { Redirect, Route, withRouter } from 'react-router-dom';
import React from 'react';


const UnauthedRoute = ({ component: Component, ...rest}) => {
    return(
        <Route
            {...rest}
            render={(props) => {
                if (!window.localStorage.getItem('currentUser')) {
                    return <Component {...props} />;
                } else if (rest.redirect) {
                    return <Redirect to='/' />;
                }
            }}

Tying It All Together

Finally, we replace our routes in the AppRouter to use these Authed and Unauthed routes instead of the lower-order components (in app/static/js/AppRouter.jsx').

#...
import AuthedRoute from './AuthedRoute';
import UnauthedRoute from './UnauthedRoute';
#...

class AppRouter extends Component {
  #...
  
  render() {
    return (
      #...
          <Switch>
              <Route exact path="/" component={() => <Home currentUser={this.props.currentUser} />} />
              <UnauthedRoute path="/register" component={Register} />
              <UnauthedRoute path="/login" component={Login} />
              <AuthedRoute path="/settings"  component={Settings} />
              <AuthedRoute path="/@:username" component={Profile} />
          </Switch>
      #...

Not Found

Last but not least, if a user navigates to a route that doesn't exist on our application, they are given an ugly 'not found' response. We can create our own custom 404 page to replace it (in app/static/js/components/NotFound.jsx).

import React from 'react';

export default () => (
  <div className="container-page">
      <div className="row">
          <div className="col-md-6 offset-md-3 col-xs-12">
             <h1>Not Found</h1>
             <p>Sorry! We couldn't find the page you're looking for.</p>
          </div>
      </div>
  </div>
);

Router, Again

After, we import the NotFound component to our AppRouter and provide it as the last option within the Switch tag, which renders if no other paths are matching (in app/static/js/AppRouter.jsx).

#...
import NotFound from './components/NotFound';
#...


class AppRouter extends Component {
  #...
  
  render() {
    return (
      <HashRouter>
        #...
          <Switch>
            #...
            <Route component={NotFound} />
          </Switch>
        #...
            
            

User Not Found

We can also create a conditional render of the 'not found' component if a user searches for another user that doesn't exist (in app/static/js/components/Profile.jsx).
https://gist.github.com/Porter97/28b5154cd08edfc0b09f59302c025adf

Sanity Check #2

Lastly, we check that the profile component is working as expected by launching the React app and the Flask app from the CLI. If you click on the gravatar image after signing in, you will see your new Profile component!

Final Thoughts

Congratulations! If you are coming from using vanilla HTML/JS/CSS with Flask, React-Redux authentication can be a big leap. However, we have now fully integrated our auth methods and routes along with a user profile page into our app. Now that we have these central components down, we can start diving into integrating Stream React components into our service as we create our first collections.

As always, thanks for reading and Happy Hacking!

decorative lines
Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->