RealWorld App with Odoo OWL - Part 2 - OWL Store and Authentication

This is the second part of our tutorial series about creating the RealWorld App with OWL (Odoo Web Library). In this part, we will implement the authentication layer with OWL Store, Actions, and getters. The OWL Store will help us and handle the global state of the application when the user is logged in or not, and what menu and actions should be available.
The app adheres to the spec of the RealWorld example apps repository on GitHub, and our current implementation in OWL is available here:
But, to continue directly where we left off last time, you should clone this branch:
git clone -b feature/basic-pages-structure-routing https://github.com/Coding-Dodo/owl-realworld-app.git
Or continue with your own project,
The problem of keeping the coherent state of an Application.
As our application grows and grows, we will have more and more components that need to communicate and share data between themselves.
For example, in our RealWorld App:
- The Navbar has to know if the user is logged in to show or hide the Login/Register/Settings links. It also shows the current Username if the user is connected.
- The Homepage has to know if the user is logged in to make the personalized "Feed" of the article available or not.
- Each article inside a list has to display the "Like" (heart button) if the user is logged in.
Passing props from Parent to Child works in a lot of cases but it can make the application complicated very fast and may sometimes not be possible at all.
App
/ \
RouterComp Navbar
/ \
Homepage Settings
/
ArticlesList
/
Article
In this example, handling the "user is logged in" info inside the App and passing it as props down the children's chain will be tedious. Article Component also has to know the user state and on the contrary, ArticlesList maybe doesn't need to know!
So it would force us to pass the user logged-in info as props to ArticlesList (that doesn't need it) to pass it again to Article (that needs it).
TLDR; Some data may need to be stored in a global state of the app and accessible from any components.
Creating and Using the OWL Store
OWL Store presentation
OWL gives us the possibility to use a centralized store, a class that owns data that will represent the state of the application.
This store is updated via actions, and OWL Components can connect to the store via a special hook to read that data.
The Components can also subscribe to the actions and dispatch them so they update the store. Without actions the store cannot be updated directly (assigning new values from within a Component), it is the job of the actions.
Store instantiation takes an Object as the argument:
const config = {
state, // Initial State of the App
actions, // Actions are functions that update the state
getters, // Needed to transform raw data from state
env, // The current env
};
const store = new Store(config);
Creating and connecting the store to our (root) App Component.
Let's instantiate a Store in our main.js that we will attach to our App. Our store will only have one Object called user.
We will have to actions functions for now:
- login action that will store the resulting user (from an API POST call done elsewhere) inside the store.
- logout cleanup our store and
We will define our User Object in the store to be similar to the User return by the API of "RealWorld app":
"user": {
"email": "jake@example.com",
"token": "jwt.token.here",
"username": "jake",
"bio": "I work at statefarm",
"image": null
}
We could directly make API calls inside our Store actions but it is important to not bind our Store logic to the underlying API. So our login and logout store actions will only store a user in the global state of the application and will not be responsible to authenticate against an external API.
We will also create a getter for the state called userLoggedIn that will return a boolean by checking if the user Object is present and filled with data. According to the specs seen earlier if the user object has the "token" property then we will say that it is correct and logged in. The other getter is getUser that will just return the current store user.
const actions = {
logout({ state }) {
state.user = {};
},
login({ state }, user) {
state.user = user;
},
};
const initialState = {
user: {},
};
const getters = {
userLoggedIn({ state }) {
if (state.user && state.user.token) {
return true;
}
return false;
},
getUser({ state }) {
return state.user;
},
};
./src/main.js
Finally, the initial state of the Application is an empty user Object.
Then, still inside ./src/main.js, we will create a function that will take all of these, create the Store and attach it to the env of the App.
// on top of the file main.js update the imports
// to import the Store
import { utils, router, mount, QWeb, Store } from "@odoo/owl";
// ...
// ...
async function makeEnvironment(store) {
const env = { qweb: new QWeb(), store: store };
env.router = new router.Router(env, ROUTES, { mode: "hash" });
await env.router.start();
return env;
}
function makeStore() {
const store = new Store({ initialState, actions, getters });
return store;
}
async function setup() {
let store = makeStore();
App.env = await makeEnvironment(store);
mount(App, { target: document.body });
}
Notice that we also changed the makeEnvironment function to take the store as a parameter and put it inside the env of our App.
Now that our Store is set up we will use its state and actions inside our components.
Making the Navbar links dynamic with the Store state changes
Now that we have our Store and our getters functions we will update our Navbar Component to make use of that, and render its links conditionally.
// Adding hooks import from owl library
import { Component, tags, router, hooks } from "@odoo/owl";
// importing the useGetters hook
const { useGetters } = hooks;
const Link = router.Link;
import { NavbarLink } from "./NavbarLink";
const NAVBAR_TEMPLATE = tags.xml/*xml*/ `
<nav class="navbar navbar-light">
<div class="container">
<Link to="'HOME'" class="navbar-brand">conduit</Link>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<NavbarLink to="'HOME'" class="nav-link">Home</NavbarLink>
</li>
<li class="nav-item">
<NavbarLink to="'EDITOR'" class="nav-link nav-link-editor" t-if="getters.userLoggedIn()">
<i class="ion-compose"></i> New Post
</NavbarLink>
</li>
<li class="nav-item">
<NavbarLink to="'SETTINGS'" class="nav-link" t-if="getters.userLoggedIn()">
<i class="ion-gear-a"></i> Settings
</NavbarLink>
</li>
<li class="nav-item">
<NavbarLink to="'LOG_IN'" class="nav-link" t-if="!getters.userLoggedIn()">
Sign in
</NavbarLink>
</li>
<li class="nav-item">
<NavbarLink to="'REGISTER'" class="nav-link" t-if="!getters.userLoggedIn()">
Sign up
</NavbarLink>
</li>
<li class="nav-item" t-if="getters.userLoggedIn()">
<NavbarLink to="'PROFILE'" class="nav-link">
<t t-esc="getters.getUser().username"/>
</NavbarLink>
</li>
</ul>
</div>
</nav>
`;
export class Navbar extends Component {
static template = NAVBAR_TEMPLATE;
static components = { Link, NavbarLink };
// registering the getters
getters = useGetters();
}
./src/components/Navbar.js
Since we registered the getters = useGetters we can use the getters functions directly inside our template in conditionals and other expressions
<li class="nav-item" t-if="getters.userLoggedIn()">
<NavbarLink to="'PROFILE'" class="nav-link">
<t t-esc="getters.getUser().username"/>
</NavbarLink>
</li>
For this example, the last link shows the username and navigate to the profile page if the user is logged in. So we surround the link with a conditional and then use the getUser() getter to have access to the Store user and display its username.
Dispatching Store action to Login and Logout.
Loggin in from the LogIn page
From the "LogIn page", we will call the Store action we created earlier login. For now, we will not implement Form logic yet and just dispatch the login action when the user clicks on the "login" button to test if our application state management works.
// import hooks from owl
import { Component, tags, router, hooks } from "@odoo/owl";
// import useDispatch hook to access actions
const { useDispatch } = hooks;
const Link = router.Link;
const { xml } = tags;
const LOG_IN_TEMPLATE = xml/* xml */ `
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Sign in</h1>
<p class="text-xs-center">
<Link to="'REGISTER'">Need an account?</Link>
</p>
<ul class="error-messages">
<li>Invalid credentials</li>
</ul>
<form>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password"/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right" t-on-click="login">
Sign In
</button>
</form>
</div>
</div>
</div>
</div>
`;
export class LogIn extends Component {
static components = { Link };
static template = LOG_IN_TEMPLATE;
dispatch = useDispatch();
login(ev) {
ev.preventDefault();
this.dispatch("login", {
email: "contact@codingdodo.com",
token: "jwt.token.here",
username: "CodingDodo",
bio: "I am a Coding Dodo",
image: null,
});
this.env.router.navigate({ to: "HOME" });
}
}
./src/pages/LogIn.js
After importing the correct dependencies and hook we declare that our LogIn Component should have a property dispatch = useDispatch.
On the login button, we bind the click event to an internal function of the component called login:
<button class="btn btn-lg btn-primary pull-xs-right" t-on-click="login">
Sign In
</button>
This function is implemented inside the component and just dispatch the Store actions login with a dummy user object for now:
login(ev) {
ev.preventDefault();
this.dispatch("login", {
email: "contact@codingdodo.com",
token: "jwt.token.here",
username: "CodingDodo",
bio: "I am a Coding Dodo",
image: null,
});
// redirect to the homepage:
this.env.router.navigate({ to: "HOME" });
}
We have access to ev that represents the event of clicking and we use preventDefault so it doesn't trigger the original browser page refresh. After we have dispatched the action we redirect to the home page programmatically via the router function navigate accessible through the this.env.router (accessible in all Components env).
Logging out from the Settings page
From the Settings page, there is a button to Log out, and we will do the same as on the Login page: dispatch a Store action with the click of the button:
import { Component, tags, hooks } from "@odoo/owl";
const { useDispatch } = hooks;
const { xml } = tags;
const SETTINGS_TEMPLATE = xml/* xml */ `
<div class="settings-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Your Settings</h1>
<form>
<fieldset>
<fieldset class="form-group">
<input class="form-control" type="text" placeholder="URL of profile picture"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name"/>
</fieldset>
<fieldset class="form-group">
<textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you"></textarea>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password"/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">
Update Settings
</button>
</fieldset>
</form>
<hr/>
<button class="btn btn-outline-danger" t-on-click="logout">Or click here to logout.</button>
</div>
</div>
</div>
</div>
`;
export class Settings extends Component {
static template = SETTINGS_TEMPLATE;
dispatch = useDispatch();
logout(ev) {
ev.preventDefault();
this.dispatch("logout");
this.env.router.navigate({ to: "HOME" });
}
}
./src/pages/Settings.js
Now we can test our Application http://localhost:8080/#/ and see that the Application test is handled correctly. Clicking on the "login" button changes the Navbar and with "logged in" menus actions, and show username.
Going into the Settings page and clicking Logout also change the state of the application correctly.
But there is a problem. Every time we refresh the page we lose the state of our application.
Using LocalStorage to keep the state of the application after refresh.
To keep the state of the application we will use localStorage to keep our user as data inside the browser. localStorage is natively present in JavaScript and will do fine for keeping our application state between refresh.
Inside our main.js, where we created our Store, we will modify the initial state of the application with localStorage info (if present) and we will bind change events on the state to also update localStorage. To do so, we will make some tweaks to the makeStore function we created
function makeStore() {
const localState = window.localStorage.getItem("owl-realworld-app");
const state = localState ? JSON.parse(localState) : initialState;
const store = new Store({ state, actions, getters });
store.on("update", null, () => {
localStorage.setItem("owl-realworld-app", JSON.stringify(store.state));
console.log(store.state);
});
return store;
}
Our application state will be stored inside the localStorage key "owl-realworld-app".
The OWL Store gives us a very easy way to listen to its state changes, so we use the store.on("update", ...) event binding to copy the OWL state into the localStorage.
Now if we check the app in the browser again, we can log in, refresh the page and we will stay logged in! ?
Create Register Form with useState
The registration page is a static form for now. We will use the useState hook to store the state of the Component and handle the Form logic.
The LogIn component will now have a local state, with these keys:
- Username, email, and password corresponding to the 3 inputs present in the form and necessary to make the registration
- loading state boolean to know if the action is being dispatched and prevent the user from spam clicking.
registerErrorstate boolean to know if the API call (not yet implemented) worked or not and display an error message to the user.
For now, the "LogIn page" just dispatched the store action with dummy user Object. We will now use the Form that is already there to gather data and store it in the state of the Component.
The LogIn component will now have a local state, with 2 keys, email, and password. It will also contain the register function that we will fill later.
//on top of the file
import { Component, tags, router, hooks, useState } from "@odoo/owl";
const { useDispatch } = hooks;
const Link = router.Link;
const { xml } = tags;
//...
export class Register extends Component {
static template = REGISTER_TEMPLATE;
static components = { Link };
dispatch = useDispatch();
state = useState({
username: "",
email: "",
password: "",
registerError: false,
loading: false,
});
register(ev) {
ev.preventDefault();
return;
}
}
Inside our template, we will use t-model to bind input value directly to our state data:
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Sign up</h1>
<p class="text-xs-center">
<Link to="'LOG_IN'">Have an account?</Link>
</p>
<ul class="error-messages" t-if="state.registerError">
<li>That email is already taken</li>
</ul>
<form>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name" t-model="state.username"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" t-model="state.email"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password" t-model="state.password"/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right" t-on-click="register" t-att-disabled="state.loading">
Sign up
</button>
</form>
</div>
</div>
</div>
</div>
Notice also that we added the disabled attribute to the button if the state of the component is currently "loading". By using t-att-ANYATTRIBUTE we make that attribute present or not depending on a condition. Here the condition is that if the state.loading is true then it is disabled.
Now the register function will take all the state data (username, email, password) send it to the API, and if ok, log the user in.
For now, we will just fake that the register call worked and log in the user via our store action:
register(ev) {
ev.preventDefault();
//Faking API call that returns a user if register OK
let user = {
email: this.state.email,
username: this.state.username,
token: "jwt.token.here",
bio: "",
image: null,
};
this.dispatch("login", user);
this.env.router.navigate({ to: "HOME" });
}
Try it in the browser http://localhost:8080/#/register and see that it's updating the state of the application correctly and show you the username you just wrote in the Navbar.
Guarding the routes that need authentication
Now that we have our internal authentication system working we need to guard some routes that need authentication. For example, right now, even if the "Editor" link is hidden when the user is not authenticated, navigating to http://localhost:8080/#/editor directly is possible even if not logged in!
We will fix that by implementing a navigation guard in our main.js file and applying it on the routes we need to be guarded:
async function authRoute({ env, to }) {
if (!env.store.getters.userLoggedIn()) {
return { to: "LOG_IN" };
}
return true;
}
export const ROUTES = [
{ name: "HOME", path: "/", component: Home },
{ name: "LOG_IN", path: "/login", component: LogIn },
{ name: "REGISTER", path: "/register", component: Register },
{
name: "SETTINGS",
path: "/settings",
component: Settings,
beforeRouteEnter: authRoute,
},
{
name: "EDITOR",
path: "/editor",
component: Editor,
beforeRouteEnter: authRoute,
},
{
name: "PROFILE",
path: "/profile",
component: Profile,
beforeRouteEnter: authRoute,
},
];
The navigation guard is a simple async function that has access to the env and the initial to (destination route). We will use the fact that the store is in the env and use the getter userLoggedIn to check if we should redirect the user or not.
A navigation guard always returns true if the route is accessible, else it should return the new route name to redirect to.
On each route that needs guarding, we then add beforeRouteEnter: authRoute reference call to our navigation guard function.
Now, if logged out, navigating directly to http://localhost:8080/#/editor should redirect you to the login page. ✅
Making API Calls with "redaxios" and custom hook.
Now that we have our internal authentication system done we will begin implementing some API Calls to make our app dynamic. We could use the standard fetch API to make our calls but for the sake of learning, we will import an and use redaxios. Feel free to use the library you prefer for that part.
Update as of 26 September 2021: The tutorial initially imported and used axios, but it was a pain to make it work with Rollup.js. Instead of rewriting entirely the class to use the standard fetch API i decided to pull redaxios that is a wrapper around the standard fetch but exposing the same API as axios. With the objective to not confuse people too much.
Importing redaxios
CD into the root of our project and run
npm install redaxios
Redaxios is a wrapper around the standard JavaScript fetch API but exposing the same functions as Axios. Be careful when using the fetch API, as it is a browser JavaScript API. It is not available in node, so your jest test suite may break. Take a look at the repository to see the changes made and the jest mockup used.
Custom useApi hook.
We will create a useApi hook to stay in the spirit of the OWL Framework and have it accessible from our component.
This hook will use the Store state to check if we have a JWT token on the user object. The hook will import Axios, configure the API endpoint and register the API calls available. Create a folder and file in ./src/hooks/useApi.js:
import { hooks } from "@odoo/owl";
const { useStore } = hooks;
import axios from "redaxios";
class ApiService {
constructor(token) {
let config = {
baseURL: "https://conduit.productionready.io/api",
timeout: 1000,
};
if (token) {
config.headers = { Authorization: `Token ${token}` };
}
this.service = axios.create(config);
}
async user() {
let response = {};
await this.service
.get("/user")
.then((res) => {
if (res.data && res.data.user) {
response = res.data.user;
}
})
.catch((error) => {
if (error && error.response) {
response = error.response.data;
}
});
return response;
}
async login(email, password) {
let response = {};
await this.service
.post("/users/login", {
user: {
email: email,
password: password,
},
})
.then((res) => {
if (res.data && res.data.user) {
response = res.data.user;
}
})
.catch((error) => {
if (error && error.response) {
response = error.response.data;
}
});
return response;
}
async register(username, email, password) {
let response = {};
await this.service
.post("/users", {
user: {
username: username,
email: email,
password: password,
},
})
.then((res) => {
if (res.data && res.data.user) {
response = res.data.user;
}
})
.catch((error) => {
if (error && error.response) {
response = error.response.data;
}
});
return response;
}
}
export function useApi() {
const user = useStore((state) => state.user);
let token = "";
if (user && user.token) {
token = user.token;
}
return new ApiService(token);
}
./src/hooks/useApi.js
We export the useApi function that will return our ApiService object instantiated with the correct endpoint, token, etc... The Components using this hook will have access to the API functions.
Updating the LogIn page to use our API
We will update the ./src/pages/LogIn.js Component to use this new hook and change the login function.
Like with the Register page we will create a local state for the Form storing email, password, loading, and login errors in state data:
// Imports:
import { Component, tags, router, hooks, useState } from "@odoo/owl";
const { useDispatch } = hooks;
const Link = router.Link;
const { xml } = tags;
import { useApi } from "../hooks/useApi";
// ...
// ...
export class LogIn extends Component {
static components = { Link };
static template = LOG_IN_TEMPLATE;
dispatch = useDispatch();
conduitApi = useApi();
state = useState({
email: "",
password: "",
logInError: false,
loading: false,
});
// ...
}
Notice that, like OWL hooks, we declare conduitApi = useApi in our Component to make use of it later.
Then we update the template to make use of the state with t-model
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Sign in</h1>
<p class="text-xs-center">
<Link to="'REGISTER'">Need an account?</Link>
</p>
<ul class="error-messages" t-if="state.logInError">
<li>Invalid credentials</li>
</ul>
<form>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" t-model="state.email"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password" t-model="state.password"/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right" t-on-click="login" t-att-disabled="state.loading">
Sign In
</button>
</form>
</div>
</div>
</div>
</div>
Now we will transform our login function by making it async and calling the API:
async login(ev) {
ev.preventDefault();
if (this.state.loading) return;
this.state.logInError = false;
this.state.loading = true;
let user = await this.conduitApi.login(
this.state.email,
this.state.password
);
if (user && user.token) {
this.state.loading = false;
this.dispatch("login", user);
this.env.router.navigate({ to: "HOME" });
} else {
this.state.logInError = true;
}
this.state.loading = false;
}
Explanation:
- We begin by updating the state as
loadingbefore the API call to prevent the user from click spamming the button (When in "loading state" the button is disabled). It's also to give feedback to the user that the request is being made. - We reset the
logingErrorstate to false to remove error messages. - We make the API call, it should return a
userobject if the authentication went through. - If the user object is present and has the token property, we dispatch the user to the store and redirect to the homepage. If not then we set the state of the component as "having errors" and don't redirect.
Updating the Register page to use our API
The same logic is applied to the ./src/pages/Register.js page:
import { Component, tags, router, hooks, useState } from "@odoo/owl";
const { useDispatch } = hooks;
const Link = router.Link;
const { xml } = tags;
import { useApi } from "../hooks/useApi";
const REGISTER_TEMPLATE = xml/* xml */ `
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Sign up</h1>
<p class="text-xs-center">
<Link to="'LOG_IN'">Have an account?</Link>
</p>
<ul class="error-messages" t-if="state.registerError">
<li>That email is already taken</li>
</ul>
<form>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name" t-model="state.username"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" t-model="state.email"/>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password" t-model="state.password"/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right" t-on-click="register" t-att-disabled="state.loading">
Sign up
</button>
</form>
</div>
</div>
</div>
</div>
`;
export class Register extends Component {
static template = REGISTER_TEMPLATE;
static components = { Link };
dispatch = useDispatch();
conduitApi = useApi();
state = useState({
username: "",
email: "",
password: "",
registerError: false,
loading: false,
});
async register(ev) {
ev.preventDefault();
if (this.state.loading) return;
this.state.registerError = false;
this.state.loading = true;
let user = await this.conduitApi.register(
this.state.username,
this.state.email,
this.state.password
);
if (user && user.token) {
this.state.loading = false;
this.dispatch("login", user);
this.env.router.navigate({ to: "HOME" });
} else {
this.state.registerError = true;
}
this.state.loading = false;
}
}
Closing thoughts
Now we have a fully functional routing and authentication system! To clone the branch corresponding to this part of the tutorial:
git clone -b feature/authentication-registration https://github.com/Coding-Dodo/owl-realworld-app.git
But, before directly viewing the source code for this part of the tutorial, I would encourage you to continue on your own with these tasks:
- Update all the pages that have info related to the user with
useGettersoruseStoreto display correct data (settings page, for example). - On the homepage Enable/Disable the "Article Feed" button if the user is logged in or not.
- Implement the User settings page and update.
- Display correct errors (from API) on Login/Register/Settings pages by creating an errors object and displaying the different errors on top of the form.
To help you with the error handling, look at the response from the API, errors are in key, value form. For example:
{
email: ["is invalid"]
password: ["is too short (minimum is 8 characters)"]
username: ["is too long (maximum is 20 characters)"]
}
In the third part, we will work on creating our ArticlesList component, see the willStartand willUpdateProps async functions, to make our component load data before rendering and fetch new data each time its props are updated.
Series Links
Part 1 - Basic Structure of the OWL Project and Components
Part 2 (this one) - Authentication, OWL Store
Part 3 - Dynamic Routing, and willStart functions.
Part 4 - Refactoring, and Hooks.
Thank you for reading, please consider sharing this article with anyone that might be interested!








