Back to Home

Offline-first app with Hoodie & React. Part Two: Authorization

offline-first · javascript · react.js · hoodie · couchdb · pouchdb

Offline-first app with Hoodie & React. Part Two: Authorization

  • Tutorial

Our goal is to write an offline-first application - SPA that loads and retains full functionality in the absence of an Internet connection. In the first part of the story, we learned how to use a browser database. Today we will configure synchronization with the server database and enable authorization. As a result, we will be able to edit our data on different devices even offline, with subsequent synchronization when a connection appears.


Couchdb


Yes, on the server we need this particular database. Pouchdb-Server is currently being actively developed , which imitates the CouchDB API based on LevelDB . Hoodie works with it by default, this is done in order to simplify the installation for beginners. But it is cheese even for development purposes. Maybe I was just lucky, but I spent 3 days trying to get a Hoodie for the first time and bumping into strange errors, 3 days of issues and pull-requests. And on the verge of disappointment, I decided to install normal CouchDB and all my problems were over. Therefore, I suggest you immediately put the latter, unless you also want to make a feasible contribution to opensource.


In most distributions, CouchDB is installed using standard tools.


If you use debian too

Here is the instruction that I used. However, the base was constantly falling until I deleted /etc/init.d/coucdband put it under the supervision of supervisord, here is the last config:


[program:couchdb]
user=couchdb
environment=HOME=/usr/local/var/lib/couchdb
command=/usr/local/bin/couchdb
autorestart=true
stdout_logfile=NONE
stderr_logfile=NONE

Having set the base, we create the admin:


curl -X PUT $HOST/_config/admins/username -d '"password"'

And turn on CORS:


npm install -g add-cors-to-couchdb
add-cors-to-couchdb -u username -p password

Now it remains only to slightly correct the command to start the server in package.json:


"server": "hoodie --port 8000 --dbUrl 'http://username:[email protected]:5984'"

I hope everything worked out for you :)


Login


In AppBar, we will have an authorization icon with a context menu. Therefore, we will place it in a separate component and will use it in App.jsinstead of AppBar:


import NavBar from './NavBar'

There we pass hoodie.accountwhich provides us with an API for authorization:


  • hoodie.account.SignUp({username, password)}
  • hoodie.account.SignIn({username, password)}
  • hoodie.account.SingOut()

And events for which you can subscribe:


  • hoodie.account.on('signin', callback)
  • hoodie.account.on('signout', callback)

And here is the component itself:


Navbar.js
import React from 'react'
import AppBar from 'material-ui/AppBar'
import FlatButton from 'material-ui/FlatButton'
import IconButton from 'material-ui/IconButton'
import IconMenu from 'material-ui/IconMenu'
import MenuItem from 'material-ui/MenuItem'
import KeyIcon from 'material-ui/svg-icons/communication/vpn-key'
import AccountIcon from 'material-ui/svg-icons/action/account-circle'
import AuthDialog from './AuthDialog'
export default class NavBar extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isSignedIn: this.props.account.isSignedIn(),
      openedDialog: null
    }
  }
  signOutCallback = () => this.setState({isSignedIn: false})
  signInCallback = () => this.setState({isSignedIn: true})
  componentDidMount() {
    this.props.account.on('signout', this.signOutCallback)
    this.props.account.on('signin', this.signInCallback)
  }
  componentWillUnmount() {
    this.props.account.off('signout', this.signOutCallback)
    this.props.account.off('signin', this.signInCallback)
  }
  render () {
    let authMenu;
    if (this.state.isSignedIn) {
      authMenu = (
        }
          targetOrigin={{horizontal: 'right', vertical: 'top'}}
          anchorOrigin={{horizontal: 'right', vertical: 'top'}}
        >
           this.props.account.signOut()} />
        
      )
    } else {
      authMenu = (
        }
          targetOrigin={{horizontal: 'right', vertical: 'top'}}
          anchorOrigin={{horizontal: 'right', vertical: 'top'}}
        >
           this.setState({openedDialog: 'signup'})} />
           this.setState({openedDialog: 'signin'})} />
        
      )
    }
    return (
      
this.setState({openedDialog: null})} />
) } }

As statewe have is the current authorization state to draw icons and menus. And the dialogue that is currently open (registration, entry, or null- if everything is closed). In componentDidMountwe subscribe to entry and exit events. And we renderdisplay the desired icon in accordance with the authorization status. It remains to draw an authorization dialog:


AuthDialog.js
import React from 'react';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import TextField from 'material-ui/TextField';
export default class AuthDialog extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      username: '',
      password: '',
    };
  }
  handleConfirm = () => {
    const account = this.props.account;
    const username = this.state.username.trim();
    const password = this.state.password.trim();
    if (!username || !password) {
      return;
    }
    if (this.props.action == 'signup') {
      account.signUp({username, password})
        .then(() => {
          return account.signIn({username, password})
        })
        .catch(console.error)
    } else {
      account.signIn({username, password})
        .catch(console.error)
    }
    this.props.handleClose();
    this.clearState();
  }
  handleCancel = () => {
    this.props.handleClose();
    this.clearState();
  }
  handleSubmit = (e) => {
    e.preventDefault();
    this.handleConfirm();
  }
  clearState = () => {
    this.setState({
      username: '',
      password: ''
    })
  }
  render () {
    const buttons = [
      ,
      
    ];
    return (
      
this.setState({username: e.target.value})} /> this.setState({password: e.target.value})} />
); } }

The registration and login dialogs have identical form fields, so we will combine them into one. The logic of the component is elementary: handleConfirmwe either enter or first register, and then enter.


It remains to reload the loops themselves during authorization. Add a reaction to events in App.js:


  componentDidMount() {
    hoodie.store.on('change', this.loadLoops);
    hoodie.account.on('signin', this.loadLoops)
    hoodie.account.on('signout', this.loadLoops)
  }
  componentWillUnmount() {
    hoodie.store.off('change', this.loadLoops);
    hoodie.account.off('signin', this.loadLoops)
    hoodie.account.off('signout', this.loadLoops)
  }

the end


So, authorization is ready. The biggest challenge to this part was probably installing CouchDB. Now our application will retain its functionality when the connection is disconnected, and when it appears, it is synchronized. However, if you completely close the site, you cannot open it without the Internet. We will fix this in the next, final part.


»The code for this part is available here: https://github.com/imbolc/action-loop under the tag part2.

Read Next