Zum Inhalt springen

Users-Modul-Beispiel

Bauen Sie das Live-Modul Users von DotApp 2.0: eigenständiger Login, Registrierung, Zwei-Faktor-Bestätigung, eine geschützte App-Seite und AJAX-Formulare, betrieben von dotapp.js. Live-Demo öffnen.

Übersicht

Das Live-Modul liegt in /app/modules/Users. DotApp leitet Anfragen direkt an öffentliche statische Controllermethoden wie Users:Login@page!, Users:Login@save! und Users:Register@save!.

Das Standard-URL-Präfix wird mit Config::module('Users', 'prefix') konfiguriert und ist standardmäßig /documentation/examples/run/users. Die folgende Anleitung verwendet diese /users- Routen.

Voraussetzungen

Dieses Beispiel setzt den Ablauf sicherer Formulare voraus: DotApp signiert Formulare, prüft Übermittlungen mit $request->crcCheck() und gibt JSON über DotApp::DotApp()->ajaxReply($body, $code) zurück.

  • Sichere Formulare mit dotapp.js
  • Eine in /app/config.php konfigurierte MySQL-Datenbank.
  • Framework-Auth-Tabellen, vorbereitet mit php dotapper.php --prepare-database.

Modul erstellen

Erstellen Sie das Modul, zwei Controller und die Routen-Gate-Middleware mit 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
        

Das Live-Modul enthält Controllers/Login.php, Controllers/Register.php, Middleware/AuthGate.php, eigenständige Views in views/ und das Frontend-Skript unter assets/js/users.js.

Datenbankeinrichtung

Benutzer- und Authentifizierungstabellen sind Framework-Tabellen, die der Befehl zur Datenbankvorbereitung erzeugt. Sie verwenden Ihr konfiguriertes Datenbankpräfix.


php dotapper.php --prepare-database
        

Die Demo benötigt eine konfigurierte MySQL-Datenbank, bevor Registrierung oder Login funktionieren. Ist die konfigurierte Datenbankliste leer, schlagen Aufrufe wie Auth::createUser() und Auth::login() fehl und die Controller geben eine freundliche Meldung zurück, dass der Dienst nicht verfügbar ist.

Konfiguration

Konfigurieren Sie Ihre Datenbank in /app/config.php. Die konkreten Zugangsdaten hängen von Ihrer lokalen Umgebung ab:


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

Das Modul initialisiert sein Präfix beim Start:


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

Überschreiben Sie Users.prefix in der Projektkonfiguration, wenn Sie dasselbe Modul an einer anderen Stelle einhängen möchten.

Controller

DotApp ruft öffentliche statische Controllermethoden auf. Das abschließende ! in Routen- Zielen kennzeichnet Methoden wie page(), save(), twoFactorPage(), twoFactorSave(), app() und logout(). Diese Methoden verwenden keine Dependency Injection.

Views werden mit Renderer::new()->module('Users')->setView($name) gerendert, gefolgt von beliebigen setViewVar()-Aufrufen. Gibt der Renderer eine leere Zeichenkette zurück, liefern die Live-Controller new Response(500, 'Template error').

Login-Seite und Speicherablauf


<?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']);
    }
}
        

Registrierung: Speicherablauf


<?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']);
    }
}
        

Zwei-Faktor- und App-Methoden


<?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;
}
        

Die relevanten Auth-Aufrufe sind Auth::login(), Auth::createUser(), Auth::confirmTwoFactor(), Auth::isLogged(), Auth::loggedStage() === 2, Auth::logout() und Auth::can($rights).

Middleware

Die Live-Middleware ist AuthGate. Sie wird an geschützte Routen mit ->before('#Users:AuthGate@check!') angehängt; sie ist nicht selbst der Routen-Handler und benötigt keine separate Alias-Registrierung.


<?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');
        }
    }
}
        

Routen

Routen werden in /app/modules/Users/module.init.php konfiguriert. Jede Route wird als Paar registriert, sodass sowohl der Pfad als auch die Variante mit abschließendem Schrägstrich funktionieren.


<?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

Die Live-Views sind vollständige HTML-Dokumente. Sie binden das Modul-CSS, das Framework- Skript /assets/dotapp/dotapp.js und das Modulskript /assets/modules/Users/js/users.js ein.


<!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>
        

Die wichtigen Details sind das eigene Element <fo-rm>, das Template-Tag {{ formName(loginForm) }} zwischen öffnendem und schließendem Tag sowie die aktuellen Asset-Pfade.

Layouts

Die Live-Demo verwendet absichtlich keine verschachtelten Layouts. Jede Datei in /app/modules/Users/views/ ist eine eigenständige HTML-Seite, die direkt mit setView('login'), setView('register'), setView('twofactor') oder setView('app') gerendert wird.


<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>
        

Module können Views mit {{ layout:name }} und Renderer-Aufrufen wie setView('x', 'layout') umschließen. Diese Demo hält die Seiten in sich abgeschlossen, damit das Beispiel leicht zu kopieren ist.

Assets

Die Live-Views laden diese Assets:

  • /assets/modules/Users/css/users.css für das Styling des Moduls.
  • /assets/dotapp/dotapp.js für DotApp-Client-Helfer und die Verarbeitung signierter Formulare.
  • /assets/modules/Users/js/users.js für Login, Registrierung, 2FA und Logout.

<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

Das Frontend-Skript ist reines JavaScript mit $dotapp. Es wartet auf das Ereignis dotapp, wenn der Helfer noch nicht verfügbar ist; es verwendet kein jQuery.


(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 });
})();
        

Die Registrierung verwendet dasselbe Muster $dotapp().form("#registerForm"). Logout sendet mit $dotapp().load(url, "POST", {}, callback) vom Button der angemeldeten Seite.

Live-Demo

Probieren Sie die Live-Demo unter /documentation/examples/run/users/login oder erstellen Sie ein Konto unter /documentation/examples/run/users/register.

DOWNLOAD unter https://github.com/dotsystems-sk/moduleUsers