Skip to content

Latest commit

 

History

History
259 lines (220 loc) · 6.91 KB

06.2-react-simple-auth.md

File metadata and controls

259 lines (220 loc) · 6.91 KB

Simple Authentication : PART II (React)

Auth React-Side

Our job in React is simple enough:

  1. send requests for login
  2. if the login is successful, get the token and store it
  3. from there on, send the token with each request
  4. also, do something with the interface so the user knows they're logged in (show their avatar, etc)

Some Utilities to Help Us

We have a little problem to solve first though. Needing to send the token sometimes means the url would be, at times http://localhost:8080/contacts/list?order=email and other times http://localhost:8080/contacts/list?order=email&token=xxxxx.

As we add parameters, this may becomes more and more cumbersome. Let's create a little function to handle that for us:

// front/src/utils.js

/**
 * Takes an object like 
 * ```
 * { name:'john', surname:'silver', guineas:400}
 * ```
 * and transforms it into a string like
 * ```
 * name=john&surname=silver&guineas=400
 * ```
 * This is a very simplistic function that will break on nested objects or arrays
 * @param {Object} params An Object containing all the keys
 * @returns {string} the query parameters string
 */
export const objectToQuery = params => {
  return Object.keys(params)
  .filter( key => params[key] !== undefined)
  .map( key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
  .join('&')
  .trim();
}

/**
 * creates a url string with a query object
 * For example:
 * ```
 * const url = makeRequestUrl('/stardust',{a:1,username:'ziggy'})
 * ```
 * url in this case will be: `/stardust?a=1&username=ziggy`
 * @param {String} path the path that you want to request
 * @param {Object} params an object of parameters 
 * @returns {string} the url
 */
export const makeRequestUrl = (path, params) => {
  if(!params){ return path } // if no parameters were provided, return early
  const query = objectToQuery(params)
  if(query.length){
    const has_interrogation_mark = path.indexOf('?') >= 0 
    const url = path + (has_interrogation_mark ? '&' : '?') + query;
    return url
  }
  return path
}

While we're at it, let's create a shortcut so we avoid writing the full URL every time:

// front/src/App.js
...
import { pause, makeRequestUrl } from "./utils.js";
...

const makeUrl = (path, params) => makeRequestUrl(`http://localhost:8080/${path}`,params)
...

Now, instead of this:

const response = await fetch(`http://localhost:8080/contacts/list?order=${order}`);

we can write this:

const url = makeUrl(`contacts/list`, {order, token: this.state.token})
const response = await fetch(url);

This "token" thing begins empty, but when we log in, it will be filled, and from that point on, it will be sent with each request

Login and Logout

Those are particularly simple:

// front/src/App.js
...
state = {
  ...
  token:null,
  nick:null
}
...
  login = async (username, password) => {
    try {
      const url = makeUrl(`login`, { username, password, token: this.state.token });
      const response = await fetch(url);
      const answer = await response.json();
      if (answer.success) {
        const { token, nick } = answer.result
        this.setState({ token, nick });
        toast(`successful login`);
      } else {
        this.setState({ error_message: answer.message });
        toast.error(answer.message);
      }
    } catch (err) {
      this.setState({ error_message: err.message });
      toast.error(err.message);
    }
  };
  logout = async token => {
    try {
      const url = makeUrl(`logout`, { token: this.state.token });
      const response = await fetch(url);
      const answer = await response.json();
      if (answer.success) {
        this.setState({ token:null, nick:null });
        toast(`successful logout`);
      } else {
        this.setState({ error_message: answer.message });
        toast.error(answer.message);
      }
    } catch (err) {
      this.setState({ error_message: err.message });
      toast.error(err.message);
    }
  };
...

One last thing to do, we want a form for logging in:

onLoginSubmit = (evt) => {
  evt.preventDefault();
  const username = evt.target.username.value
  const password = evt.target.password.value
  if(!username){
    toast.error("username can't be empty");
    return
  }
  if(!password){
    toast.error("password can't be empty");
    return
  }
  this.login(username,password)
}
renderUser(){
  const { token } = this.state
  if(token){ // user is logged in
    return this.renderUserLoggedIn()
  }else{
    return this.renderUserLoggedOut()
  }
}
renderUserLoggedOut(){
  return (<form className="third" onSubmit={this.onLoginSubmit}>
    <input name="username" placeholder="username" type="text"/>
    <input name="password" placeholder="password" type="password"/>
    <input type="submit" value="ok"/>
  </form>)
}
renderUserLoggedIn(){
  const {nick} = this.state
  return (<div>Hello, {nick}! <button onClick={this.logout}>logout</button></div>)
}
...

Finally, change the previous renderProfilePage to:

// front/src/App.js
...
// change the 
renderProfilePage = () => {
  return (
    <div>
      <p>profile page</p>
      {this.renderUser()}
    </div>
  );
};
...

Run the app!

We can verify this is working by trying a few usernames and passwords from the ones we've stored previously. For example, we can try:

username: nina.williams@tek.ken
password: hapkido

We should see "hello anna!" with a logout button. Let's try the logout.

We just have to verify that we're receiving and sending the token.

Let's just add a button that accesses the user's page /mypage:

// front/src/App.js
...
  getPersonalPageData = async () => {
    try {
      const url = makeUrl(`mypage`, { token: this.state.token });
      const response = await fetch(url);
      const answer = await response.json();
      if (answer.success) {
        const message = answer.result
        // we should see: "received from the server: 'ok, user <username> has access to this page'"
        toast(`received from the server: '${message}'`);
      } else {
        this.setState({ error_message: answer.message });
        toast.error(answer.message);
      }
    } catch (err) {
      this.setState({ error_message: err.message });
      toast.error(err.message);
    }
  }
...
  render(){
    ...
    <button onClick={this.getPersonalPageData}>get personal page data</button>
    ...
  }

Then try to click it; you should get an error. Log in, using, say:

username: ken.masters@sf.er
password: shoryuken

You should get a message saying you accessed the page successfully.

We're done!

Almost.

If this was a real application, we'd probably want to store the users in our SQL database, and we'd probably want a way for user to sign up, reset their password, and so on. But, as we said at the beginning, all of this is too sensitive to reinvent it with every app, which is why we...

...are going to scrape most of what we've done here, and do production-grade authentication in the next chapter.