•9 months ago
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!
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
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.
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
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
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.
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
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
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
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
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
After that, our login portion is completed, and we can move onto the settings.
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
Settings The Settings
Now we need to create a new reducer to handle actions regarding state for our settings component (in
As we have created a new reducer, we need to include it in our combineReducer function within the store (in
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
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
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
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.
Next, we create our endpoint to handle the settings request (in
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.
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
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
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
As we have created a new reducer, we will once again need to integrate it into our store (in
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
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.
Next, we import the profile component and set up its path. We define the parameter for username after the colon within the Route (in
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
We can also use the custom errors we created in the errors.py file to return a more accurate error message to a requestor.
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).
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).
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).
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').
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
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
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
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!
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!