Элегантная форма входа в админку на Laravel и Sentry

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

Статья содержит описание некоторых базовых приемов использования Laravel при разработки сайтов и будет полезна тем, кто начинает осваивать данный фреймворк. Для примера использую Ubuntu 12.04, PostgreSQL 9.3, Nginx 1.1.19, PHP 5.5.7, Composer и свежий проект, созданный с использованием Laravel 4.1. Под управление PostgreSQL крутится база данных examples, к которой имеет доступ пользователь examples c одноименным паролем. Nginx же настроен таким образом, что при обращении по адресу examples.loc в браузере открывается главная страницу-заглушка, которая идет с Laravel в комплекте, с надписью «You have arrived.»

Все пути к редактируемым файлам указаны относительно директории проекта.

Окружение


Сначала настраиваю локальное окружение в Laravel. Для этого создаю директорию app/config/local и добавляю в нее файл database.php:
<?php
/**
 * app/config/local/database.php
 */
return array(
    'default' => 'pgsql',

    'connections' => array(
        'pgsql' => array(
			'driver'   => 'pgsql',
			'host'     => 'localhost',
			'database' => 'examples',
			'username' => 'examples',
			'password' => 'examples',
			'charset'  => 'utf8',
			'prefix'   => '',
			'schema'   => 'public',
		),
    ),
);

Прошу Laravel использовать окружение local по умолчанию. Для этого редактирую файл bootstrap/start.php и заменяю строку 'your-machine-name' на '*':
// Фрагмент из bootstrap/start.php
$env = $app->detectEnvironment(array(

	'local' => array('*'),

));


Подключение Sentry


Ссылку на инструкцию по подключению Sentry можно найти в конце статьи. Кратко, делаю следующее.
Добавляю в composer.json строку "cartalyst/sentry": "2.0.*" в блок require.
// Фрагмент composer.json
{
	"name": "laravel/laravel",
	"description": "The Laravel Framework.",
	"keywords": ["framework", "laravel"],
	"license": "MIT",
	"require": {
		"laravel/framework": "4.1.*",
		"cartalyst/sentry": "2.0.*"
	},
...

Выполняю команду:
$ composer update
Символ $ набирать не надо
Символ $ означает что, надо набрать в командной строке composer update

Добавляю в список сервис-провайдеров в файле app/config/app.php:
'Cartalyst\Sentry\SentryServiceProvider',

Добавляю в список псевдонимов в файле app/config/app.php:
'Sentry' => 'Cartalyst\Sentry\Facades\Laravel\Sentry',

Выполняю миграции Sentry:
$ php artisan migrate --package=cartalyst/sentry

Публикую конфигурационный файл Sentry:
$ php artisan config:publish cartalyst/sentry

Открываю на редактирование app/config/packages/cartalyst/sentry/config.php. Нахожу в нем строку 'login_attribute' => 'email' и заменяю на 'login_attribute' => 'username', для того чтобы Sentry выполнял аутентификацию пользователя по его логину, а не e-mail.

При выполнении миграций Sentry была создана таблица users, в которой есть поле email, но нет username. Поэтому, чтобы Sentry работал, надо добавить недостающее поле. Для этого создаю миграцию:
$ php artisan migrate:make alter_users_add_username

В директории app/database/migration появится файл имя, которого будет состоять из текущей даты и времени и заканчивается на alter_users_add_username.php. Редактирую его следующим образом:
<?php
/**
 * app/database/migration/0000_00_00_000000_alter_users_add_username.php
 */
use Illuminate\Database\Migrations\Migration;

class AlterUsersAddUsername extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
        Schema::table('users', function($table)
        {
            $table->string('username');
        });
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
        Schema::table('users', function($table)
        {
            $table->dropColumn('username');
        });
	}

}

Для проверки миграции выполняю:
$ php artisan migrate

Для проверки, что миграция успешно откатывается, выполняю:
$ php artisan migrate:rollback

Создаю еще одну миграцию, которая добавляет в таблицу users запись о суперпользователе:
$ php artisan migrate:make add_user_admin

Нахожу в директории app/database/migrate файл, который заканчивается на add_user_admin.php и редактирую его:
<?php
/**
 * app/database/migration/0000_00_00_000001_add_user_admin.php
 */
use Illuminate\Database\Migrations\Migration;

class AddUserAdmin extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
        $user = Sentry::createUser(array(
            'username' => 'admin',
            'email' => 'admin@examples.loc',
            'password' => 'password',
            'activated' => 1,
            'permissions' => array(
                'superuser' => 1,
            ),
        ));
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
        User::where('username', '=', 'admin')->firstOrFail()->delete();
	}

}

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

Форма входа


В директории app/controllers создаю файл AuthController.php:
<?php
/**
 * app/controllers/AuthController.php
 */
class AuthController extends BaseController {

     /**
     * Отображает страницу входа
     *
     * @return Illuminate\View\View
     */
    public function getLogin()
    {
        $title = 'Вход';
        return View::make('auth.login', compact('title'));
    }
}

Метод getLogin() передает заголовок страницы через переменную $title в представление auth.login, которое служит для отображения страницы входа в админку.

Создаю представление auth.login. Для этого в директорию app/views добавляю директорию auth и создаю в ней файл login.blade.php:
/**
 * app/views/auth/login.blade.php
 */
@extends('layout')

@section('main')
<div class="container">
{{ Form::open(array('class' => 'form-signin')) }}

    @if (!$errors->isEmpty())
    <div class="alert alert-danger">
        @foreach ($errors->all() as $error)
        <p>{{ $error }}</p>
        @endforeach
    </div>
    @endif

    <h2 class="form-signin-heading">{{ $title }}</h2>

    {{ Form::text('username', null, array('class' => 'form-control', 'placeholder' => 'Логин')) }}
    {{ Form::password('password', array('class' => 'form-control', 'placeholde' => 'Пароль')) }}

    <label class="checkbox">
        {{ Form::checkbox('remember-me', 1) }} Запомни меня
    </label>

    {{ Form::submit('Войти', array('class' => 'btn btn-lg btn-primary btn-block')) }}

{{ Form::close() }}
</div>
@stop

Шаблон login.blade.php расширяет шаблон layout.blade.php. Поэтому создаю его в директории app/views:
/**
 * app/views/layout.blade.php
 */
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>{{ $title }}</title>

        @section('styles')
        {{ HTML::style('//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css') }}
        {{ HTML::style(URL::asset('styles/base.css')) }}
        @show
    </head>
    <body>
        @yield('main')

        @section('scripts')
        {{ HTML::script('//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js') }}
        @show
    <body>
</html>

После того, как есть метод контроллера и возвращаемое им представление, необходимо настроить роутинг, чтобы GET запросы, отправляемые по адресу examples.loc/login, обрабатывались методом getLogin(). Для этого добавляю в файл app/routes.php следующий код:
// Фрагмент app/routes.php
...
Route::group(array('before' => 'guest'), function () 
{
    Route::get('login', array(
        'as' => 'auth.login', 
        'uses' => 'AuthController@getLogin'
    ));
});

С помощью Route::get() определяю роут с именем auth.login, который направляет GET запросы по адресу /login методу getLogin() контроллера AuthController.
Также поместил роут auth.login в группы роутов. Перед любым роутом, входящим в данную группу будет выполнятся фильтр guest. Данный фильтр означает, что обработка GET запросов по адресу /login будет происходить только в том случает, если пользователя является гостем, т.е. не авторизован.

Перепишу фильтр guest так, чтобы он использовал Sentry. Для этого в файле app/filters.php изменю код фильтра guest:
// Фрагмент app/filters.php
...
Route::filter('guest', function()
{
	if (Sentry::check()) return Redirect::to('/');
});
...

Теперь можно посмотреть, как выглядит форма входа в браузере. Для этого перехожу по адресу examples.loc/login и…

Вижу сообщение об ошибке Call to undefined method Illuminate\Cookie\CookieJar::get().

Легким движением Google выясняю, что Sentry 2.0 совместим с Laravel 4.0, но не совместим 4.1. Хорошо, что уже зарелизился Sentry 2.1. Чтобы избавиться от ошибки, изменяю версию Sentry в composer.json на 2.1:
// Фрагмент composer.json
{
	"name": "laravel/laravel",
	"description": "The Laravel Framework.",
	"keywords": ["framework", "laravel"],
	"license": "MIT",
	"require": {
		"laravel/framework": "4.1.*",
		"cartalyst/sentry": "2.1.*"
	},
...

Выполняю команду:
$ composer update

Еще раз пытаюсь открыть examples.loc/login и вижу форму ввода логина и пароля. Для того чтобы форма выглядела элегантно, надо добавить немного CSS. Для этого в директории public создаю директорию styles и добавляю туда файл base.css:
/**
 * public/style/base.css 
 */
body {
    padding-top: 40px;
    padding-bottom: 40px;
    background-color: #eee;
}

.form-signin {
    max-width: 330px;
    padding: 15px;
    margin: 0 auto;
}

.form-signin .form-signin-heading,
.form-signin .checkbox {
    margin-bottom: 10px;
}

.form-signin .form-signin-heading {
    text-align: center;
}

.form-signin .checkbox {
    font-weight: normal;
}

.form-signin .form-control {
    position: relative;
    font-size: 16px;
    height: auto;
    padding: 10px;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}

.form-signin .form-control:focus {
    z-index: 2;
}

.form-signin input[type="text"] {
    margin-bottom: -1px;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.form-signin input[type="password"] {
    margin-bottom: 10px;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}


Контролеры


Теперь форма отображается красиво, но при нажатии на кнопку «Войти», возвращается ошибка 404. Значит надо добавить в AuthController обработчик POST запросов, отправляемых на адрес /login.
<?php
/**
 * app/controllers/AuthController.php
 */
class AuthController extends BaseController {

    /**
     * Отображает страницу входа
     *
     * @return Illuminate\View\View
     */
    public function getLogin()
    {
        $title = 'Вход';
        return View::make('auth.login', compact('title'));
    }

    /**
     * Аутентифицирует и редиректит в админку
     *
     * @return Illuminate\Http\RedirectResponse
     */
    public function postLogin()
    {
        Input::flash();

        try {
            $credentials = array(
                'username' => Input::get('username'), 
                'password' => Input::get('password')
            );
            $user = Sentry::authenticate($credentials, Input::get('remember-me'));
        } catch (Exception $e) {
            return Redirect::to(route('auth.login'))
                ->withErrors(array($e->getMessage()));
        }

        return Redirect::intended(route('admin'));
    }

    /**
     * Обрабатывает выход
     *
     * @return Illuminate\Http\RedirectResponse
     */
    public function getLogout()
    {
        Sentry::logout();
        return Redirect::route('auth.login');
    }
}

В методе postLogin() данные, переданные через форму сохраняются в сессии с помощью Input::flash(). В блоке try Sentry пытается аутентифицировать пользователя. Если в форме ввода логина и пароля пользователь установит галочку «Запомни меня», то вторым параметром в Sentry::authenticate() будет передано значение true и Sentry запомнит пользователя в случае успешной аутентификации. Если аутентификация по какой-либо причине не прошла успешно, то в блоке catch метод Redirect::to() отправит нас на страницу ввода логина и пароля в месте с сообщением об ошибке. В случае успешной аутентификации метод Redirect::intended() отправит пользователя на ту страницу, на которую он намеревался зайти, когда был перенаправлен на форму ввода логина и пароля. Если такая страница не задана, то откроется страница по адресу /admin, с которой связан роут с именем admin.

Метод getLogin() самая проста реализация как можно разлогинить пользователя. После выхода пользователь будет перенаправление на страницу ввода логина и пароля.

Прежде чем переходить к настройке роутинга, надо добавить метод, который будет обрабатывать запросы, отправляемые по адресу /admin. Для этого отредактирую файл app/controllers/HomeController.php следующим образом:
<?php
/**
 *  app/controllers/HomeController.php
 */
class HomeController extends BaseController {

	public function showWelcome()
	{
		return View::make('hello');
	}

	public function getAdmin()
	{
		return link_to(route('auth.logout'), 'Выход');
	}

}

Метод getAdmin() просто отображает на страницу ссылку «Выход», при нажатии на которою пользователь будет разлогиниваться.

Роутинг


Теперь, чтобы новые методы могли обрабатывать запрос, отредактирую в файл app/routes.php:
/**
 * app/routes.php
 */
Route::get('/', function()
{
	return View::make('hello');
});

Route::group(array('before' => 'guest'), function () 
{
    Route::get('login', array(
        'as' => 'auth.login', 
        'uses' => 'AuthController@getLogin'
    ));

    Route::post('login', array(
        'before' => 'csrf',
        'uses' => 'AuthController@postLogin'
    ));
});

Route::group(array('before' => 'auth'), function ()
{
    Route::get('admin', array(
        'as' => 'admin',
        'uses' => 'HomeController@getAdmin',
    ));

    Route::get('logout', array(
        'as' => 'auth.logout',
        'uses' => 'AuthController@getLogout'
    ));
}); 

Первая группа роутов, перед которой выполняется фильтр guest, содержи роутинг для адреса /login для GET и POST запросов. Фильтр guest означает, что по адресу /login будут обрабатываться запросы только от гостей, т.е. неавторизованных пользователей.

Вторая группа роутов, перед которой выполняется фильтр auth, содержит роутинг для адресов /admin и /logout. Фильтр auth означает, что по данным адресам будут обрабатываться запросы только от авторизованных пользователей.

Также необходимо отредактировать фильтр auth, так чтобы он использовал Sentry. Для этого открываю файл app/filters.php изменяю фильтр auth следующим образом:
// Фрагмент app/filters.php
...
Route::filter('auth', function()
{
	if (!Sentry::check()) return Redirect::guest(route('auth.login'));
});
...


Теперь можно пробовать логиниться в админку использую логин admin и пароль password. Неавторизованному пользователю не удастся зайти на страницу /admin, она будет доступна, только после успешной аутентификации. Также после аутентификации будет невозможно зайти на страницу /login, так как будет происходить редирект на главную страницу.

Надеюсь данный пример поможет начинающим разработчикам лучше понять некоторые элементы Laravel.

Ссылки по теме


getcomposer.org/doc/00-intro.md#installation-nix
laravel.com/docs/installation#install-laravel
cartalyst.com/manual/sentry/installation/laravel-4

UPD
Код примера github.com/bskton/examples/tree/eform
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 6

    +12
    Демонстрации «элегантной формы входа в админку» не хватает.
      –3
      Ну не будьте столь критичны. Автор расписал практическое руководство «Как возвести админку с нуля». Мне, незнакомому с Laravel, в принципе всё понятно. Если вы знакомы с более простыми и «элегантными» решениями, то поделитесь ссылкой.
        0
        Тогда может разместиться на github с мануалом по быстрому старту хотя бы?
    +13
    Элегантные 200кб кода
      0
      github.com/rydurham/Sentinel
      This pacakge provides an implementation of Sentry 2 for Laravel 4. By default it uses Bootstrap 3.0, but you can make use of whatever UI you want.

      Only users with full accounts can post comments. Log in, please.