Разделение доступа

ОБНОВЛЕНИЕ 2018: в учебнике хорошая теория, но ему уже два года. Проверяйте версии пакетов. За выходом нового учебника можно следить в telegram канале или twitter

На канале так же проводятся бесплатные вебинары, публикуются переводы и авторские материалы, присоединяйтесь!

Разделение доступа

В данном разделе, по шагам будет разобран вход на сайт в качестве администратора. Следовательно, мы сделаем недоступным для посещения адрес localhost:3000/admin, а так же рассмотрим редирект на "главную" или в "админку" после ввода логина.

Создадим страницу логина.

В качестве "сервера для авторизации", будем использовать localStorage.

Алгоритм простой: вводится логин - кладется в localStorage.

src/components/Login/index.js

import React, { Component } from 'react'

export default class Login extends Component {
  handleSubmit(e) {
    e.preventDefault()
    const value = e.target.elements[0].value
    window.localStorage.setItem('rr_login', value)
  }
  render() {
    return (
      <div className='row'>
        <div className='col-md-12'>Пожалуйста, введите логин:</div>
        <form className='col-md-4' onSubmit={this.handleSubmit}>
          <input type='text' placeholder='login'/>
          <button type='submit'>Войти</button>
        </form>
      </div>
    )
  }
}

src/routes.js

...
import Login from './components/Login'
...
<Route path='/login' component={Login} />
...

src/containers/App/index.js

...
<li><NavLink to='/login'>Войти</NavLink></li>
...

Напоминаю: несмотря на то, что ссылка появилась в шапке (hot-reload работает для компонента <App />), страницу все равно нужно перезагрузить (так как hot-reload не работает для обновления списка роутов).

Если ввести новый "логин" - старый перетирается. Данное допущение сейчас не мешает нам проверить работоспособность, а наоборот облегчает проверку.

Нам необходимо закрыть доступ "не админам", для этого потребуется разобрать "события", которые возникают в процессе изменений URL'a.

onEnter, onLeave

Предлагаю добавить слово "хук" в словарь. По-моему, хук, не что иное как "действие на событие".

Итак, есть возможность использовать хуки на события onLeave и onEnter. По названию понятно: onLeave возникает, когда "роут покинут", а onEnter - в момент "захожу на роут".

Представьте адрес:

react-site.com/profile/photos/

его "роутер-реализацию": (не стоит так говорить в приличном месте)

/ + profile + photos

и его реализацию компонентами:

<App /> + <Profile /> + <Photos />

Представьте, что вы будучи на странице с фото, кликнули на ссылку для перехода на главную страницу. Произойдет:

  • onLeave на /profile/photos

  • onLeave на /profile

  • onEnter на /

И обратная ситуация: вы находитесь на главной, и решили перейти в раздел фото:

  • onLeave на /

  • onEnter на /profile

  • onEnter на /profile/photos

Вернемся к "хукам".

Добавьте в routes.js функцию checkLogin и непосредственно сам хук.

src/routes.js

...
function checkLogin() {
  const login = window.localStorage.getItem('rr_login')
  if (login === 'admin') {
    console.log('пропусти')
  }
}
...
<Route path='/admin' component={Admin} onEnter={checkLogin}/>
...

Теперь если вы залогинетесь как admin - в консоли браузера вас "пропустят". Причем, заметьте, хук сработает при вводе url напрямую в строке ввода адреса + enter, либо если вы кликнете по ссылке. Так же, если вы уже находитесь в разделе admin и попробуете кликнуть на ссылку "Админка" - onEnter не произойдет, а следовательно и хук не сработает. Кажется, разработчики react-router'a постарались на славу.

На всякий случай, напоминаю, что очистить localStorage можно командой localStorage.clear()

У хука есть полезные аргументы - nextState, replace, callback. Нам понадобится replace. На пару слов подробнее можно прочитать в офф.документации.

Перепишем функцию checkLogin

src/routes.js

...
function checkLogin(nextState, replace) {
  const login = window.localStorage.getItem('rr_login')
  if (login !== 'admin') {
    replace('/')
  }
}
...

Попробуйте сейчас залогиниться под другим именем: вы не сможете войти на страницу /admin, независимо от того введете ли вы адрес и нажмете enter, или кликните по ссылке.

Страницу API Reference настоятельно рекомендую добавить в закладки.

Использование static method в качестве хука на onEnter

Наш код работает, но функция checkLogin, как будто мешается в файле с роутами. Может быть вынести ее в отдельный файл? А вы знаете, есть еще одно интересное решение: использовать static метод класса Admin.

src/components/Admin/index.js

import React, { Component } from 'react'

export default class Admin extends Component {
  static onEnter(nextState, replace) {
    const login = window.localStorage.getItem('rr_login')
    if (login !== 'admin') {
      replace('/')
    }
  }
  render() {
    return (
      <div className='row'>
        <div className='col-md-12'>Раздел /admin</div>
      </div>
    )
  }
}

Исправим routes.js (привожу полный листинг)

src/routes.js

import React from 'react'
import { Route, IndexRoute } from 'react-router'

import App from './containers/App'
import Admin from './components/Admin'
import List from './components/List'
import Genre from './components/Genre'
import Release from './components/Release'
import Home from './components/Home'
import Login from './components/Login'
import NotFound from './components/NotFound'

export const routes = (
  <div>
    <Route path='/' component={App}>
      <IndexRoute component={Home} />
      {/* в качестве хука на onEnter - статический метод класса Admin */}
      <Route path='/admin' component={Admin} onEnter={Admin.onEnter}/>
      <Route path='/genre/:genre' component={Genre}>
        <Route path='/genre/:genre/:release' component={Release} />
      </Route>
      <Route path='/list' component={List} />
      <Route path='/login' component={Login} />
    </Route>
    <Route path='*' component={NotFound} />
  </div>
)

Все, теперь с чистой совестью можете приступать к задачке на повторение.

Задача: если пользователь ввел admin - после нажатия кнопки "Войти" - направить его на /admin, иначе на /

Подсказка #1: Если данная задача вызвала у вас трудность, прочитайте еще раз предыдущую главу.

Решение:

src/components/Login/index.js

import React, { Component, PropTypes } from 'react'

export default class Login extends Component {
  constructor() {
    super()
    this.handleSubmit = this.handleSubmit.bind(this)
  }
  handleSubmit(e) {
    e.preventDefault()
    const login = e.target.elements[0].value
    window.localStorage.setItem('rr_login', login)

    if (login === 'admin') {
      this.context.router.push('/admin')
    } else {
      this.context.router.push('/')
    }
  }
  render() {
    return (
      <div className='row'>
        <div className='col-md-12'>Пожалуйста, введите логин:</div>
        <form className='col-md-4' onSubmit={this.handleSubmit}>
          <input type='text' placeholder='login'/>
          <button type='submit'>Войти</button>
        </form>
      </div>
    )
  }
}

Login.contextTypes = {
  router: PropTypes.object.isRequired
}

Итого: мы познакомились с возможностью "вклиниваться" в процесс роутинга. Разобрали рабочую ситуацию: как ограничить доступ юзеру в раздел администратора. Закрепили знания по программной навигации.

Исходный код на данный момент.

Last updated