Prejsť na obsah

Príklad modulu Users

Postavte živý modul Users pre DotApp 2.0: samostatné prihlásenie, registráciu, dvojfaktorové potvrdenie, chránenú stránku aplikácie a AJAX formuláre poháňané dotapp.js. Otvoriť živú ukážku.

Prehľad

Živý modul sa nachádza v /app/modules/Users. DotApp smeruje požiadavky priamo na verejné statické metódy kontroléra, napríklad Users:Login@page!, Users:Login@save! a Users:Register@save!.

Predvolený prefix URL sa konfiguruje pomocou Config::module('Users', 'prefix') a predvolene je /documentation/examples/run/users. Návod nižšie používa tieto trasy /users.

Predpoklady

Tento príklad predpokladá, že poznáte tok zabezpečených formulárov: DotApp podpisuje formuláre, overuje odoslania pomocou $request->crcCheck() a vracia JSON cez DotApp::DotApp()->ajaxReply($body, $code).

  • Zabezpečené formuláre s dotapp.js
  • Databáza MySQL nakonfigurovaná v /app/config.php.
  • Autentifikačné tabuľky frameworku pripravené príkazom php dotapper.php --prepare-database.

Vytvorenie modulu

Modul, dva kontroléry a middleware brány trás vytvorte pomocou DotApper:


php dotapper.php --create-module=Users
php dotapper.php --module=Users --create-controller=Login
php dotapper.php --module=Users --create-controller=Register
php dotapper.php --module=Users --create-middleware=AuthGate
        

Živý modul obsahuje Controllers/Login.php, Controllers/Register.php, Middleware/AuthGate.php, samostatné view v views/ a frontend skript v assets/js/users.js.

Nastavenie databázy

Tabuľky používateľov a autentifikácie sú tabuľky frameworku generované príkazom na prípravu databázy. Používajú váš nakonfigurovaný prefix databázy.


php dotapper.php --prepare-database
        

Demo potrebuje nakonfigurované MySQL, inak registrácia ani prihlásenie nebudú fungovať. Ak je zoznam nakonfigurovaných databáz prázdny, volania ako Auth::createUser() a Auth::login() zlyhajú a kontroléry vrátia priateľskú správu o nedostupnosti.

Konfigurácia

Databázu nakonfigurujte v /app/config.php. Presné prihlasovacie údaje závisia od vášho lokálneho prostredia:


Config::db('driver', 'pdo');
Config::addDatabase('main', '127.0.0.1', 'Username', 'Password', 'DBNAME', 'UTF8', 'MYSQL', 'pdo');
        

Modul pri štarte inicializuje svoj prefix:


Config::module('Users', 'prefix') ?? Config::module('Users', 'prefix', '/documentation/examples/run/users');
$p = rtrim((string) Config::module('Users', 'prefix'), '/');
        

Users.prefix prepíšte v konfigurácii projektu, keď chcete ten istý modul namontovať inde.

Kontroléry

DotApp volá verejné statické metódy kontroléra. Koncové ! v cieľoch trás označuje metódy ako page(), save(), twoFactorPage(), twoFactorSave(), app() a logout(). Tieto metódy nepoužívajú dependency injection.

View sa vykresľujú pomocou Renderer::new()->module('Users')->setView($name), po ktorom nasledujú volania setViewVar(). Ak renderer vráti prázdny reťazec, živé kontroléry vrátia new Response(500, 'Template error').

Stránka prihlásenia a tok uloženia


<?php
class Login extends \Dotsystems\App\Parts\Controller
{
    public static function page($request)
    {
        if (Auth::isLogged()) {
            return Response::redirect(self::prefix() . '/app', 302);
        }
        return self::view('login', 'Sign in', self::prefix() . '/login');
    }

    public static function save($request)
    {
        if (!$request->crcCheck()) {
            return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400);
        }

        $answer = $request->form(['POST'], 'loginForm', function ($request) {
            $payload = $request->data(true)['data'] ?? [];
            $email = trim((string) ($payload['email'] ?? ''));
            $password = (string) ($payload['password'] ?? '');
            $remember = (($payload['remember'] ?? '') === 'on');

            $login = Auth::login(['email' => $email, 'password' => $password, 'stage' => 0], $remember);

            if (Auth::loggedStage() === 2) {
                return ['code' => 200, 'body' => [
                    'status' => 1,
                    'twofactor' => 1,
                    'redirectTo' => self::prefix() . '/2fa',
                ]];
            }

            return ['code' => 200, 'body' => ['status' => 1, 'redirectTo' => self::prefix() . '/app']];
        }, function () {
            return ['code' => 403, 'body' => ['status' => 0, 'message' => 'Invalid signature']];
        }, $request->getPath());

        return DotApp::DotApp()->ajaxReply($answer['body'], $answer['code']);
    }
}
        

Tok uloženia registrácie


<?php
class Register extends \Dotsystems\App\Parts\Controller
{
    public static function save($request)
    {
        if (!$request->crcCheck()) {
            return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400);
        }

        $answer = $request->form(['POST'], 'registerForm', function ($request) {
            $data = $request->data(true)['data'] ?? [];
            $email = trim((string) ($data['email'] ?? ''));
            $username = trim((string) ($data['username'] ?? ''));
            $password = (string) ($data['password'] ?? '');

            $r = Auth::createUser($username, $password, $email);

            if (($r['error'] ?? 99) === 1) {
                return ['code' => 200, 'body' => ['status' => 0, 'message' => 'That account already exists.']];
            }

            $p = rtrim((string) Config::module('Users', 'prefix'), '/');
            return ['code' => 200, 'body' => [
                'status' => 1,
                'message' => 'Account created.',
                'redirectTo' => $p . '/login',
            ]];
        }, function () {
            return ['code' => 403, 'body' => ['status' => 0, 'message' => 'Invalid signature']];
        }, $request->getPath());

        return DotApp::DotApp()->ajaxReply($answer['body'], $answer['code']);
    }
}
        

Metódy dvojfaktorového overenia a aplikácie


<?php
public static function twoFactorPage($request)
{
    if (Auth::isLogged()) {
        return Response::redirect(self::prefix() . '/app', 302);
    }
    if (Auth::loggedStage() !== 2) {
        return Response::redirect(self::prefix() . '/login', 302);
    }
    return self::view('twofactor', 'Authenticator code', self::prefix() . '/2fa');
}

public static function twoFactorSave($request)
{
    if (!$request->crcCheck()) {
        return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Bad request'], 400);
    }
    $data = $request->data(true)['data'] ?? [];
    $code = preg_replace('/\D+/', '', (string) ($data['tfa'] ?? ''));
    $r = Auth::confirmTwoFactor(['tfa' => $code]);

    if (!is_array($r) || ($r['confirmed'] ?? false) !== true) {
        return DotApp::DotApp()->ajaxReply(['status' => 0, 'message' => 'Verification failed'], 200);
    }
    return DotApp::DotApp()->ajaxReply(['status' => 1, 'redirectTo' => self::prefix() . '/app'], 200);
}

public static function app($request)
{
    $attrs = Auth::attributes();
    $email = htmlspecialchars((string) ($attrs['email'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

    $html = Renderer::new()->module('Users')
        ->setView('app')
        ->setViewVar('title', 'Signed in')
        ->setViewVar('email', $email)
        ->setViewVar('logoutAction', self::prefix() . '/logout')
        ->setViewVar('docsUrl', '/documentation/examples/users-module')
        ->renderView();

    return $html === '' ? new Response(500, 'Template error') : $html;
}
        

Relevantné volania autentifikácie sú Auth::login(), Auth::createUser(), Auth::confirmTwoFactor(), Auth::isLogged(), Auth::loggedStage() === 2, Auth::logout() a Auth::can($rights).

Middleware

Živé middleware je AuthGate. Na chránené trasy sa pripája pomocou ->before('#Users:AuthGate@check!'); samo nie je handlerom trasy a nepotrebuje samostatnú registráciu aliasu.


<?php
namespace Dotsystems\App\Modules\Users\Middleware;

use Dotsystems\App\Parts\Auth;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Response;

class AuthGate extends \Dotsystems\App\Parts\ModuleMiddleware
{
    public static function check($request, array $rights = [])
    {
        if (!Auth::isLogged()) {
            $p = rtrim((string) Config::module('Users', 'prefix'), '/');
            return Response::redirect($p . '/login', 302);
        }
        if (!empty($rights) && !Auth::can($rights)) {
            return new Response(403, 'Forbidden');
        }
    }
}
        

Trasy

Trasy sa konfigurujú v /app/modules/Users/module.init.php. Každá trasa sa registruje ako pár, takže funguje cesta aj verzia s koncovou lomkou.


<?php
Config::module('Users', 'prefix') ?? Config::module('Users', 'prefix', '/documentation/examples/run/users');
$p = rtrim((string) Config::module('Users', 'prefix'), '/');

$pair = function (string $path): array {
    $path = rtrim($path, '/');
    return [$path, $path . '/'];
};

Router::get($pair($p), 'Users:Login@page!', Router::STATIC_ROUTE);
Router::get($pair($p . '/login'), 'Users:Login@page!', Router::STATIC_ROUTE);
Router::post($pair($p . '/login'), 'Users:Login@save!', Router::STATIC_ROUTE);
Router::get($pair($p . '/2fa'), 'Users:Login@twoFactorPage!', Router::STATIC_ROUTE);
Router::post($pair($p . '/2fa'), 'Users:Login@twoFactorSave!', Router::STATIC_ROUTE);
Router::post($pair($p . '/logout'), 'Users:Login@logout!', Router::STATIC_ROUTE);
Router::get($pair($p . '/register'), 'Users:Register@page!', Router::STATIC_ROUTE);
Router::post($pair($p . '/register'), 'Users:Register@save!', Router::STATIC_ROUTE);
Router::get($pair($p . '/app'), 'Users:Login@app!', Router::STATIC_ROUTE)
    ->before('#Users:AuthGate@check!');
        

View

Živé view sú kompletné HTML dokumenty. Zahŕňajú CSS modulu, skript frameworku /assets/dotapp/dotapp.js a skript modulu /assets/modules/Users/js/users.js.


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{{ var: $title }} — DotApp</title>
  <link rel="stylesheet" href="/assets/modules/Users/css/users.css" />
</head>
<body class="u-body">
  <header class="u-top"><a href="{{ var: $docsUrl }}">Documentation</a><span>Users demo</span></header>
  <main class="u-main">
    <h1>Sign in</h1>
    <div id="error-message" class="u-error" hide="hide"></div>
    <div id="loginWrap">
      <fo-rm method="POST" id="loginForm">
        <label for="email">Email</label>
        <input type="text" id="email" name="email" autocomplete="username" required />
        <label for="password">Password</label>
        <input type="password" id="password" name="password" autocomplete="current-password" required />
        {{ formName(loginForm) }}
        <button type="submit" id="loginBtn" class="u-btn">Sign in</button>
      </fo-rm>
    </div>
    <p><a href="{{ var: $registerUrl }}">Create an account</a></p>
  </main>
  <script src="/assets/dotapp/dotapp.js"></script>
  <script src="/assets/modules/Users/js/users.js"></script>
</body>
</html>
        

Dôležité sú vlastný element <fo-rm>, značka šablóny {{ formName(loginForm) }} medzi otváracou a zatváracou značkou a aktuálne cesty k assetom.

Layouty

Živá ukážka zámerne nepoužíva vnorené layouty. Každý súbor v /app/modules/Users/views/ je samostatná HTML stránka vykreslená priamo pomocou setView('login'), setView('register'), setView('twofactor') alebo setView('app').


<fo-rm method="POST" id="registerForm">
  <label for="username">Username</label>
  <input type="text" id="username" name="username" required />
  <label for="email">Email</label>
  <input type="text" id="email" name="email" autocomplete="username" required />
  <label for="password">Password</label>
  <input type="password" id="password" name="password" autocomplete="new-password" required />
  {{ formName(registerForm) }}
  <button type="submit" id="registerBtn" class="u-btn">Create account</button>
</fo-rm>
        

Moduly môžu view obaliť značkou {{ layout:name }} a volaniami renderera ako setView('x', 'layout'). Táto ukážka ponecháva stránky samostatné, aby sa príklad ľahko kopíroval.

Assety

Živé view načítavajú tieto assety:

  • /assets/modules/Users/css/users.css na štýlovanie modulu.
  • /assets/dotapp/dotapp.js na klientské pomocné funkcie DotApp a spracovanie podpísaných formulárov.
  • /assets/modules/Users/js/users.js na správanie prihlásenia, registrácie, 2FA a odhlásenia.

<link rel="stylesheet" href="/assets/modules/Users/css/users.css" />
<script src="/assets/dotapp/dotapp.js"></script>
<script src="/assets/modules/Users/js/users.js"></script>
        

JavaScript

Frontend skript je čistý JavaScript používajúci $dotapp. Čaká na udalosť dotapp, keď pomocná knižnica ešte nie je dostupná; jQuery nepoužíva.


(function () {
  var runMe = function ($dotapp) {
    if (document.getElementById("loginForm")) {
      $dotapp()
        .form("#loginForm")
        .before(function (data, form) {
          if ($dotapp(form).attr("blocked") == 1) return $dotapp().halt();
          $dotapp(form).attr("blocked", "1");
          $dotapp("#loginBtn").attr("loading", "true").attr("loader", "dots");
          $dotapp("#loginWrap").addClass("u_busy");
          $dotapp("#error-message").attr("hide", "hide");
        })
        .after(function (data, response, form) {
          var reply = $dotapp().parseReply(response);
          if (reply && reply.status == 1 && reply.redirectTo) {
            window.location = reply.redirectTo;
            return;
          }
          if (reply && reply.message) $dotapp("#error-message").attr("hide", "false").html(reply.message);
          $dotapp(form).attr("blocked", "0");
          $dotapp("#loginBtn").removeAttr("loading").removeAttr("loader");
          $dotapp("#loginWrap").removeClass("u_busy");
        });
    }

    if (document.querySelector(".two-fa-inputs input")) {
      var box = document.querySelector(".two-fa-inputs");
      var twoFaUrl = (box && box.getAttribute("data-2fa")) || "/documentation/examples/run/users/2fa";
      $dotapp(".two-fa-inputs input").twoFactor(function (code) {
        $dotapp().load(twoFaUrl, "POST", { tfa: code }, function (raw) {
          var reply = $dotapp().parseReply(raw);
          if (reply && reply.status == 1 && reply.redirectTo) window.location = reply.redirectTo;
          else if (reply && reply.message) $dotapp("#error-message").attr("hide", "false").html(reply.message);
        });
      }, { length: 6, allowLetters: false, autoSubmit: true });
    }
  };

  if (window.$dotapp) runMe(window.$dotapp);
  else window.addEventListener("dotapp", function () { runMe(window.$dotapp); }, { once: true });
})();
        

Registrácia používa rovnaký vzor $dotapp().form("#registerForm"). Odhlásenie odosiela dáta pomocou $dotapp().load(url, "POST", {}, callback) z tlačidla na stránke po prihlásení.

Živá ukážka

Vyskúšajte živú ukážku na adrese /documentation/examples/run/users/login alebo si vytvorte účet na /documentation/examples/run/users/register.

STIAHNUŤ na https://github.com/dotsystems-sk/moduleUsers