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.phpkonfigurierte 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.cssfür das Styling des Moduls./assets/dotapp/dotapp.jsfür DotApp-Client-Helfer und die Verarbeitung signierter Formulare./assets/modules/Users/js/users.jsfü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