Přihlašování uživatelů

Tak z minula už máme tabulku uživatelů a snad se ti povedlo i jednoho uživatele založit. Dnes si vyrobíme komponenty pro přihlášení a pro zapomenuté heslo. Taky si zabezpečíme administraci tak, aby se do ní mohl dostat jen přihlášený uživatel.

Začneme komponentou pro přihlášení. Jelikož je to celkem obecně použitelná věc, umístíme ji do BaseModule/AdminModule/Components. Tam založíme složku Sign/In.

ISignInFactory.php má klasický obsah:

<?php

namespace BaseModule\AdminModule\Components\Sign\In;

interface ISignInFactory
{

    /**
     * @return SignIn
     */
    public function create();
}

Samotná třída si v konstruktoru vyžaduje závislost na třídě User. Obsahuje také jednoduchý formulář pro přihlášení:

<?php

namespace BaseModule\AdminModule\Components\Sign\In;

use BaseModule\Components\FormFactory;
use UserModule\Model\Facade\UserFacade;
use Nette;
use Nette\Application\UI\Form;
use Nette\Security\User;

class SignIn extends Nette\Application\UI\Control
{
    /**
     * @var FormFactory
     */
    private $factory;

    /**
     * @var User
     */
    private $user;

    /**
     * @var string
     */
    private $templateFile = __DIR__ . '/Sign.in.latte';

    /**
     * @var UserFacade
     */
    public $facade;

    /**
     * SignIn constructor.
     *
     * @param FormFactory $factory
     * @param User        $user
     * @param UserFacade  $facade
     */
    public function __construct(FormFactory $factory, User $user, UserFacade $facade)
    {
        parent::__construct();
        $this->factory = $factory;
        $this->user = $user;
        $this->facade = $facade;
    }

    /**
     * Method render
     */
    public function render()
    {
        $this->template->setFile($this->templateFile);
        $this->template->render();
    }

    /**
     * Method create
     *
     * @return Form
     */
    public function createComponentSignInForm()
    {
        $form = $this->factory->create();
        $form->addText('username', 'Username:')
            ->setRequired('Please enter your username.');

        $form->addPassword('password', 'Password:')
            ->setRequired('Please enter your password.');

        $form->addCheckbox('remember', 'Keep me signed in');

        $form->addSubmit('send', 'Sign in');

        $form->onSuccess[] = function (Form $form, $values) {
            try {
                $this->user->setExpiration($values->remember ? '14 days' : '20 minutes');
                $this->facade->login($values->username, $values->password);
            } catch (Nette\Security\AuthenticationException $e) {
                $form->addError('Nesprávné přihlašovací údaje.');

                return;
            }
        };

        return $form;
    }
}

Šablona Sign.in.latte ošetřuje chybně odeslaná data, takže ji tu taky vypíšu. Jinak ale klidně použij nějaký vlastní formulář, pokud jsi na něj zvyklý:

{block content}
	<div class="form-2" n:if="$flashes">
		{snippet flash}
			<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} fade in">
				{$flash->message}
			</div>
		{/snippet}
	</div>

	<section class="main">
		<form n:name=signInForm class="form-2">
			<h1><span class="log-in"> Přihlásit</span></h1>
			<ul class="errors" n:if="$form->hasErrors()">
				<li n:foreach="$form->errors as $error">{$error}</li>
			</ul>
			<p class="float">
				<label n:name=username><i class="fa fa-user"></i>Uživatelské jméno</label>
				<input type="text" tabindex="1" n:name=username placeholder="Uživatelské jméno">
				<label n:name=remember><input type="checkbox" tabindex="3" n:name=remember>neodhlašovat</label>
			</p>
			<p class="float">
				<label n:name=password><i class="fa fa-lock"></i>heslo</label>
				<input type="password" tabindex="2" n:name=password placeholder="Heslo" class="showpassword">
				<label for="showPassword"><input id="showPassword" tabindex="4" class="showpasswordcheckbox"
															type="checkbox">ukázat</label>
			</p>
			<p class="clearfix">
				<input type="submit" n:name=send>
			</p>
			<p>
				<a href="{plink Sign:remember}" class="btn btn-link btn-sm" role="button"><i class="fa
				fa-mail-reply-all"></i>
					Zapomenuté heslo</a>
			</p>
		</form>
		​​
	</section>
{/block}

V BaseModule/DI/services.neon musíme přidat novou službu:

    - BaseModule\AdminModule\Components\Sign\In\ISignInFactory

Pak samozřejmě přidat routu do BaseExtension. Prostě do metody loadConfiguration() přidej řádek:

        $this->appendRoute('Sign:Admin', 'admin/sign/<action>', 'Sign:in');

V BaseExtension nám ale chybí mapování presenteru. To uděláme díky metodě:

    /**
     * Method beforeCompile
     *
     * @throws \Nette\DI\ServiceCreationException
     */
    public function beforeCompile()
    {
        $builder = $this->getContainerBuilder();
        $builder->getDefinition($builder->getByType(Nette\Application\IPresenterFactory::class))->addSetup(
            'setMapping',
            [
                ['Sign' => 'BaseModule\*Module\Presenters\*Presenter'],
            ]
        );
    }

Tak pokračujeme výrobou presenteru a jeho šablony. Uvažuji takto: přihlašování by asi mohlo mít vlastní modul, nicméně je tak svázané s každým dalším modulem, který chceme administrovat, že ho vložím do BaseModule/AdminModule/Presenters. Nebude ale dědit z BasePresenteru, jelikož ten naopak plánujeme upravit tak, aby vyžadoval přihlášení. Samotný SignPresenter však pro své spuštění nesmí vyžadovat přihlášeného uživatele.

Navíc budeme chtít jinou šablonu pro layout, než samotná administrace. No nemuseli bychom - stačilo by prostě použít layout pro administraci, ale aspoň si předvedeme jak to udělat, když potřebujeme různé layouty.

Takže začátek presentru SingPresenter.php:

<?php

namespace BaseModule\AdminModule\Presenters;

use Nette;
use BaseModule\Components\FormFactory;
use BaseModule\AdminModule\Components\Sign\In\ISignInFactory;
use BaseModule\AdminModule\Components\Sign\In\SignIn;
use BaseModule\Components\ICssFactory;
use BaseModule\Components\Css;
use BaseModule\Components\IJsFactory;
use BaseModule\Components\Js;
use UserModule\Model\Facade\UserFacade;

class SignPresenter extends Nette\Application\UI\Presenter
{

    /** @var  ICssFactory @inject */
    public $cssFactory;

    /** @var  IJsFactory @inject */
    public $jsFactory;

    /** @var ISignInFactory @inject */
    public $signInFormFactory;

    /** @var UserFacade @inject */
    public $facade;

    /** @var FormFactory @inject */
    public $formFactory;

    public function __construct()
    {
        parent::__construct();
    }

Takže si injectneme pár služeb, které známe - Css, Js atd. Pak vložíme metodu pro vytvoření komponenty:

    /**
     * @return SignIn
     */
    protected function createComponentSignIn()
    {
        $control = $this->signInFormFactory->create();
        $control['signInForm']->onSuccess[] = function () {
            $this->redirect(':Page:Admin:Page:');
        };

        return $control;
    }

Pak si vyrobíme komponenty pro css a js zdroje:

    /**
     * @return Css
     */
    protected function createComponentCss()
    {
        $control = $this->cssFactory->create();
        $control->setStyles(
            [
                'css/login.css',
            ]
        );

        $control->setRemoteStyles(
            [
                '//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css',
                '//cdn.rawgit.com/twbs/bootstrap/v3.3.6/dist/css/bootstrap.min.css',
            ]
        );

        return $control;
    }

    /**
     * @return Js
     */
    protected function createComponentJs()
    {
        $control = $this->jsFactory->create();
        $control->setScripts(
            [
                'js/showPassword.js',
            ]
        );

        $control->setRemoteScripts(
            [
                '//cdn.rawgit.com/nette/forms/v2.2.4/src/assets/netteForms.js',
                '//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js',
            ]
        );

        return $control;
    }

Načítám si tam nějaký stylopis pro stránku s přihlášením a malý javascript, který umí zobrazit zadávané heslo v čitelné podobě. VYpisovat zdroj tady nebudu, ale můžeš si je pak stáhnout s dnešní prací.

A teď ten layout. Ve složce BaseModule/AdminModule/Presenters/templates vytvoř novou složku Sign. V ní soubor @layout.latte s obsahem:

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{block title}{/block}</title>
    {control css}
</head>
<body>
<div class="container">
    {include content}
</div>
{control js}
</body>
</html>

No a pak ještě šablonu pro akci In. Čili přidej soubor in.latte:

{block content}
    {control signIn}
{/block}

{block title}
    Přihlášení
{/block}

Po zobrazení http://rsrs.loc/admin/sing/in bys měl vidět přihlašovací formulář. Zkus se přihlásit.

Přihlášený uživatel by měl mít možnost odhlášení. K omu nepotřebujeme komponentu. Stačí obyčejná akce presenteru:

    /**
     * @throws \Nette\Application\AbortException
     */
    public function actionOut()
    {
        $this->facade->logout();
        $this->redirect(':Homepage:Front:Homepage:default');
    }

A tohle stačí. Jen zadej adresu http://rsrs.loc/admin/sign/out a měl bys být odhlášen.

Takže si zabezpečíme celou administraci tak, aby vyžadovala přihlášení. Stačí do metody startup přidat na začátek pár řádků:

        if (!$this->getUser()->isLoggedIn()) {
            if ($this->getUser()->logoutReason === Nette\Security\IUserStorage::INACTIVITY) {
                $this->flashMessage('Byl jste odhlášen z důvodu neaktivity');
            } else {
                $this->flashMessage('Přihlaste se prosím');
            }
            $this->redirect(':Sign:Admin:Sign:in');
        }

Logika je prostá: pokud uživatel není přihlášen, nebo kvůli dlouhé neaktivitě byl odhlášen, přesměrujeme ho na přihlášení. Pokud jsi odhlášen, zkus jít na https://rsrs.loc/admin. Měl bys být přesměrován na přihlášení. Až se ti povede přihlásit se, uvidíš normálně administraci.

Měl jsem v plánu ještě popsat proceduru se zapomenutým heslem, ale to si nechám na příště.

Dnes bude asi důležité mít možnost stáhnout si dnešní práci.

Celý seriál o vývoji redakčního systému

Komentáře

O mně

Jmenuji se Rudolf Svátek - lektor výpočetní techniky, trochu PHP programátor a SEO konzultant na volné noze.

Adresa

Příčná 326/3
736 01 Havířov

Kontakty

Email: office@rudolfsvatek.cz
Telefon: +420 777 828 353
Skype: svatekr