📜 ⬆️ ⬇️

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

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 the browser database. Today we will set up synchronization with the server database and connect the 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. Currently, Pouchdb-Server is being actively developed, which based on LevelDB simulates the CouchDB API. Hoodie works with it by default; this is done to make installation easier for beginners. But he is even cheese for development purposes. Maybe I was just lucky, but I spent 3 days trying to get Hoodie for the first time and bumping into strange errors, 3 days of issues and pull-request. And on the verge of disappointment I decided to establish a 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 the opensource.


In most distributions CouchDB put regular means.


If you are using debian too

Here is the instruction that I used. However, the database was constantly falling until I deleted /etc/init.d/coucdb and did not give it under the supervision of a supervisord, here’s the config file:


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

Putting 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 only remains to correct the command to start the server in package.json :


 "server": "hoodie --port 8000 --dbUrl 'http://username:password@127.0.0.1:5984'" 

I hope you did it all :)


Authorization


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


 import NavBar from './NavBar' <NavBar account={hoodie.account} /> 

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



And events that you can subscribe to:



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 = ( <IconMenu iconButtonElement={<IconButton><AccountIcon /></IconButton>} targetOrigin={{horizontal: 'right', vertical: 'top'}} anchorOrigin={{horizontal: 'right', vertical: 'top'}} > <MenuItem primaryText="Sign Out" onTouchTap={() => this.props.account.signOut()} /> </IconMenu> ) } else { authMenu = ( <IconMenu iconButtonElement={<IconButton><KeyIcon /></IconButton>} targetOrigin={{horizontal: 'right', vertical: 'top'}} anchorOrigin={{horizontal: 'right', vertical: 'top'}} > <MenuItem primaryText="Sign Up" onTouchTap={() => this.setState({openedDialog: 'signup'})} /> <MenuItem primaryText="Sign In" onTouchTap={() => this.setState({openedDialog: 'signin'})} /> </IconMenu> ) } return ( <div> <AppBar title="Action Loop" showMenuIconButton={false} iconElementRight={authMenu} /> <AuthDialog account={this.props.account} action={this.state.openedDialog} handleClose={() => this.setState({openedDialog: null})} /> </div> ) } } 

In state , we have the current authorization status for drawing the icon and menu. And the currently open dialogue (registration, entry, or null - if everything is closed). In componentDidMount we subscribe to entry and exit events. And in the render display the desired icon in accordance with the status of authorization. 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 = [ <FlatButton label="Cancel" primary={true} onTouchTap={this.handleCancel} />, <FlatButton label="Submit" type="submit" primary={true} keyboardFocused={true} onTouchTap={this.handleConfirm} /> ]; return ( <div> <Dialog title={this.props.action == 'signup' ? 'Sign Up' : 'Sign In'} actions={buttons} modal={false} open={this.props.action !== null} onRequestClose={this.handleCancel} contentStyle={{maxWidth: 400}} > <form onSubmit={this.handleSubmit}> <TextField name="username" floatingLabelText="Username" onChange={(e) => this.setState({username: e.target.value})} /> <TextField name="password" floatingLabelText="Password" onChange={(e) => this.setState({password: e.target.value})} /> </form> </Dialog> </div> ); } } 

Registration and login dialogs have identical form fields, so we merge them into one. The logic of the component is elementary: we either enter or register with handleConfirm or register first and then enter.


It remains to reload the loops themselves when logging in. 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 of this part was probably the installation of CouchDB. Now our application will retain its functionality when the connection is broken, and when it appears it is synchronized. However, if you completely close the site, open it without the Internet will not work. 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 part2 tag.


')

Source: https://habr.com/ru/post/309572/


All Articles