V tomto díle seriálu se budeme věnovat přihlašování a registraci uživatelů.

Seriál Webová aplikace v Nette pro začátečníky

Seriál provází tvorbou webové aplikace v Nette Frameworku, od prvotní konfigurace prostředí, až po hotový produkt. Napíšeme si aplikaci, která bude umět registrovat, přihlásit a spravovat uživatele.

Zdrojáky v Gitu

Pro lepší orientaci jsou nyní zdrojáky k dispozici na GitHubu. Díly mají svoje tagy.

Registrace

Začneme od šablon – v souboru app/templates/@la­yout.latte změňte obsah mezi tagy bodytakto

<script> document.body.className+=' js' </script>

<div id="header">
    <h1><a href="{plink Homepage:}">Uživatelé</a></h1>
</div>

<div id="nav">
    <a href="{plink Register:register}">Registrovat</a>
    <a href="{plink Sign:in}">Přihlásit</a>
</div>

<div n:foreach="$flashes as $flash" class="flash {$flash->type}">{$flash->message}</div>

<div id="content">
    {include #content}
</div>

V headeru jsme si vyrobili odkaz na homepage, aby se nám vždycky dobře vracelo domů. Vyrobili jsme taky odkazy na přihlášení a registraci.

Odkaz ve tvaru plink Register:register znamená „utíkej do presenteru Register na metodu register“. plink Homepage: zase znamená „fofrem do presenteru Homepage na metodu default“, kterou tam nemusím psát, když se mi nechce, protože je default.

Když si uložíme a mrkneme na výsledek v prohlížeči, vidíme, že odkaz na Přihlásit se tváří celkem normálně, kdežto odkaz na Registrovat je celý brunátný. Když ho prozkoumáme (firebugem, nebo něčím podobným), zjistíme, že error: Cannot load presenter ‚Register‘, class ‚RegisterPresenter‘ was not found in '/var/www/User­s/app/presenter­s/RegisterPre­senter.php.

Znamená to jednoduše to, že se odkazujeme na presenter a metodu, která neexistuje. Naproti tomu odkaz Přihlásit volá presenter SignPresenter a metodu renderIn (nebo actionIn), jež jsou součástí sanboxu, který jsme v minulém díle stáhli.

Podrobné info o vytváření odkazů v dokumentaci.

Pojďme vytvořit v adresáři app/presenters/ RegisterPresen­ter.php s tímto obsahem

<?php

class RegisterPresenter extends BasePresenter {

    protected function startup() {
        parent::startup();
    }

    public function renderRegister(){
    }
}

Uložíme, mrkneme na výsledek a vidíme, že odkaz je už spokojený. Klikneme na něj a zase chyba (říkám tomu Exception Driven Development – můžu si to patentovat, nebo tady teď byl?). Chyba mluví jasnou řečí – musíme pro akci vytvořit šablonu.

Vytvoříme v app/templates/ adresář Register a soubor register.latte s obsahem

{block #content}

<h2>Registrovat</h2>

{control registerForm}

Nevypadá to ale, posunuli jsme se. Nová exception říká, že neexistuje komponenta registračního formuláře registerForm, kterou voláme pomocí {control registerForm}register.latte.

Vytvořme proto továrničku createComponen­tRegisterFormapp/presenter­s/RegisterPre­senter.php která nám formulář vyrobí:

use Nette\Application\UI,
    Nette\Application\UI\Form as Form;


class RegisterPresenter extends BasePresenter {

    /** @var Users */
    private $users;

    protected function startup() {
        parent::startup();
        $this->users = $this->context->users;
    }

    public function renderRegister(){
    }


    protected function createComponentRegisterForm() {
        $form = new Form;
        $form->addText('name', 'Jméno');
        $form->addText('email', 'E-mail: *', 35)
                ->setEmptyValue('@')
                ->addRule(Form::FILLED, 'Vyplňte Váš email')
                ->addCondition(Form::FILLED)
                ->addRule(Form::EMAIL, 'Neplatná emailová adresa');
        $form->addPassword('password', 'Heslo: *', 20)
                ->setOption('description', 'Alespoň 6 znaků')
                ->addRule(Form::FILLED, 'Vyplňte Vaše heslo')
                ->addRule(Form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků.', 6);
        $form->addPassword('password2', 'Heslo znovu: *', 20)
                ->addConditionOn($form['password'], Form::VALID)
                ->addRule(Form::FILLED, 'Heslo znovu')
                ->addRule(Form::EQUAL, 'Hesla se neshodují.', $form['password']);
        $form->addSubmit('send', 'Registrovat');
        $form->onSuccess[] = callback($this, 'registerFormSubmitted');
        return $form;
    }

}

Tím jsme vytvořili formulář i s validačními pravidly, která se budou kontrolovat na serveru i v klientovi. V klientovi proto, že v @layout.latte načítáme netteForms.js, který se o to postará. Podrobné info o tvorbě formulářů v dokumentaci.

Teď potřebujeme presenteru říct, co má dělat po vyplnění a odeslání formuláře. Formulář definuje metodu, kterou volá, v $form->onSuccess[] = callback($this, ‚registerFormSub­mitted‘), tudíž ji napíšeme

public function registerFormSubmitted(UI\Form $form) {
    $values = $form->getValues();
    $new_user_id = $this->users->register($values);
    if($new_user_id){
        $this->flashMessage('Registrace se zdařila, jo!');
        $this->redirect('Sign:in');
    }
}

Pomocí $form->getValues() jsme získali z formuláře data a vložili je do databáze. Pokud se podařilo, spravíme o úspěchu uživatele a přesměrujeme jej k přihlášení.

Vložení nového uživatele provedeme, jak říká právě napsaná metoda, v modelu – konkrétně v Users.php:

public function register($data) {
    unset($data["password2"]);
    $data["role"] = "guest";
    $data["password"] = sha1($data["password"] . self::$user_salt);
    return $this->db->insert($this->table, $data)->execute(dibi::IDENTIFIER);
}

Z dat z formuláře jsme odstranili potvrzjící opakování hesla, uživateli přiřadili primitivním způsobem jakousi roli (prozatím stačí) a vyrobili heslo. Salt si dodáme například takto

class Users extends \Nette\Object {

    /** @var \DibiConnection */
    private $db;
    private $table = "users";
    public static $user_salt = "AEcx199opQ";

// třída pokračuje...

Než to vyzkoušíme, potřebujeme změnit databázi

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `email` varchar(50) NOT NULL,
  `password` char(60) NOT NULL,
  `name` varchar(100) NOT NULL,
  `role` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

INSERT INTO `users` (`id`, `email`, `password`, `name`, `role`) VALUES
(1, 'franta@vokurka.cz',  'f63d757c919a7707aa6c0af5946fd24a28015779', 'Franta Vokurka', 'guest'),
(2,  'lojza@lojza.cz', 'ebad7341ea91cf83593b0638817c16a563bd4037', 'Lojza Přechodník', 'guest'),
(3,  'mala@tonicka.cz',  'e1822661e05f4595d4ccaf4c8cd5b7f997a5e82c', 'Tonička Malá', 'guest'),
(4,  'gerda@zapasnikova.cz', '51fcf163696a95f0674f123509a469092fc2d34e', 'Gerda Zápasníková',  'guest'),
(5,  'anna@k.cz',  '1ec2131552f3bd668591b0b99407b23355ba1ebc', 'Anna Kareninová',  'guest'),
(6,  'barca@bramburka.cz', 'f04003366fb2f36149d392b547a7440d778046d7', 'Barbora Brambora', 'guest'),
(7,  'hilda@zelena.cz',  'e08c606ff2fbc1a1991febf00b0fa8721d7d09ca', 'Hilda Zelená', 'guest'),
(8,  'hajdy@zhor.cz',  '626a453385ad62ad723655f706a2993eeb5c2922', 'Hajdy Zhor', 'guest'),
(9,  'johanka@vplutu.com', '2b49d3bf378036984695dfd2a96bee427627e87d', 'Johanka Tazpluta', 'guest');

Hotovo, vyzkoušejte a běda jak to nebude fungovat.

Přihlášení

Registrace vám bezvadně funguje, přihlášení se ale nezdaří, protože k přihlášení používáme „vestavěný“ app/model/Authen­ticator.php a ten jsme vůbec neupravili pro práci s naší databází. Smažte obsah třídy a nahraďte jej za toto

use Nette\Security as NS;

/**
 * Users authenticator.
 *
 * @author     John Doe
 * @package    MyApplication
 */
class Authenticator extends Nette\Object implements NS\IAuthenticator {

    /** @var \DibiConnection */
    private $db;
    private $table = "users";

    public function __construct(\DibiConnection $connection) {
        $this->db = $connection;
    }

    /**
     * Performs an authentication
     * @param  array
     * @return Nette\Security\Identity
     * @throws Nette\Security\AuthenticationException
     */
    public function authenticate(array $credentials) {
        list($username, $password) = $credentials;
        $row = $this->db->select('*')->from($this->table)->where('email = %s', $username)->fetch();

        if (!$row) {
            throw new NS\AuthenticationException("User '$username' not found.", self::IDENTITY_NOT_FOUND);
        }

        if ($row->password !== sha1($password . Users::$user_salt)) {
            throw new NS\AuthenticationException("Invalid password.", self::INVALID_CREDENTIAL);
        }

        unset($row->password);
        return new NS\Identity($row->id, $row->role, $row->toArray());
    }

}

Třídě jsme dodali připojení k databázi a ukázali tabulku z které brát data. Taky jsme v metodě authenticate řekli, že pokud najde uživatele s emailem a heslem shodným s tím co přišlo z formuláře, má vrátit jeho NS\Identity.

Nyní již máte všechny potřebné indicie k tomu se zaregistrovat i přihlásit. Gratulki.

Shrnutí, příště

Na to, že článek měl být bleskovým výcucem toho nejnutnějšího, je i tak dost dlouhý, takže pro další (důležité) info prosím račte navštívit ctěnou dokumentaci – Přihlašování & oprávnění uživatelů.

Příště bychom se možná konečně mohli dostat k „napojení“ Datatables – ačkoliv se to tváří jako zlatý hřeb seriálu, uvidíte, že se jedná o v podstatě triviální kratochvíli.

Komentáře

Petr před 3 lety

Doporučil bych vytváření formuláře přesunout do samostatného souboru – formulář pak půjde použít ve více presenterech a nebude v kódu presenteru překážet.

reagovat

Peter Láng před 3 lety

Ano, to je dobrá rada – více informací k vytváření formulářů mimo presenter v dokumentaci.

reagovat

Vojta před 3 lety

Osobně bych dal to hledání uživatele do User modelu pod metodu findUser($email) a do konstruktoru toho Authenticatoru posílal UserModel, aby bylo vidět, že authentikace probíhá nad uživateli.

Takže pak by to vypadalo $this->users->findUser($email);

reagovat

koudy před 3 lety

Peckovni tutorial ! :D ,,,,, ALE ! :D – nepotrebujeme zmenit databazi… potrebujeme tam VYTVORIT tabulku …chapu ze chces napred smazat existujici, ale potes panbu pokud si toho nekdo nevsimne a smaze si existujici usery :D radeji bych tam ten DROP nepsal :D

reagovat

Papik před 11 měsíci

Já bych tam spíš napsal CREATE NABLE IF NOT EXISTS – klasika…DROP určitě NE!

reagovat

Papik před 11 měsíci

Oprava: TABLE

reagovat

Jack Bauer před 10 měsíci

Maximální délka platné emailové adresy je o něco vyšší, než výše uvedených 50 znaků, ten sloupec v tabulce bude prostě příliš úzký. Já doporučuji nastavit jeho šířku na 254 znaků. http://stackoverflow.com/…mail-address

reagovat

Honzik před 3 měsíci

Diskuse evidentne zije vlastnim zivotem prestrelkou farmakologickych vyrobku… Kazdopadne dobry tutorial ;-)

reagovat

Peter Láng před 3 měsíci

Trochu jsem to pročistil ;-)

reagovat

Přidat komentář

  • Můžete použít Texy syntaxi, HTML není povoleno
  • Například: *kurzíva*, **tučně**, "text odkazu":adresa