<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../backend/_lib/Validator.php';
require_once __DIR__ . '/../../../backend/_lib/JsonResponse.php';
require_once __DIR__ . '/../../../backend/_lib/Auth.php';
use DemoBuilder\Auth;
use DemoBuilder\JsonResponse;
use DemoBuilder\Validator;
$auth = Auth::file(__DIR__ . '/users.json');
$action = (string) ($_POST['action'] ?? '');
switch ($action) {
case 'register':
$p = Validator::make($_POST)
->required('email')->required('password')->validateOrFail();
$user = $auth->register((string) $p['email'], (string) $p['password']);
$auth->login((string) $p['email'], (string) $p['password']);
JsonResponse::success(['user' => $user, 'action' => 'register']);
case 'login':
$p = Validator::make($_POST)
->required('email')->required('password')->validateOrFail();
if (!$auth->login((string) $p['email'], (string) $p['password'])) {
JsonResponse::error('Invalid credentials', 401);
}
JsonResponse::success(['user' => $auth->currentUser(), 'action' => 'login']);
case 'logout':
$auth->logout();
JsonResponse::success(['action' => 'logout']);
case 'whoami':
JsonResponse::success(['user' => $auth->currentUser(), 'action' => 'whoami']);
default:
JsonResponse::error('Unknown action', 400);
}
Session Auth
Multi-user save flow scoped per session. Defaults to a flat <code>users.json</code> driver — swap one line for PDO when you go to production.
Walkthrough
-
Start a session, hash passwords
Auth::file($path)auto-starts the session, lazy-createsusers.json, and usespassword_hash(PASSWORD_DEFAULT)for storage andpassword_verifyfor comparison. Plaintext passwords never touch disk.PHPuse DemoBuilder\Auth; $auth = Auth::file(__DIR__ . '/users.json'); // Register: hashes + persists + auto-logs-in. $auth->register('alice@example.com', 'a-good-password'); // Login: verifies + writes the user-id into the session. if (!$auth->login($email, $password)) { JsonResponse::error('Invalid credentials', 401); } $user = $auth->currentUser(); // ['id' => 1, 'email' => '…'] -
Guard the save endpoint
On every save call,
$auth->requireUser()returns the row or emits401+exit. The save handler can assume$useris non-null after the guard and key the storage path byuser['id'].PHP$user = $auth->requireUser(); // 401 + exit if not logged-in $payload = Validator::make($_POST) ->required('slug')->regex('/^[a-z0-9-]+$/i')->maxLength(120) ->required('html')->maxLength(5_000_000) ->required('data')->maxLength(10_000_000) ->validateOrFail(); $dir = $storageRoot . '/' . (int) $user['id']; $slug = (string) $payload['slug']; file_put_contents("$dir/$slug.html", $payload['html']); file_put_contents("$dir/$slug.json", $payload['data']); -
Wire the builder
Point
endpoints.savein$builderConfigatsave-as-user.php. The browser already carries thePHPSESSIDcookie, so the saved data lands under that user's directory automatically. Logged-out clicks return401; the chrome's toast surfaces the error.PHP// demo/builder.php — top of file $builderConfig = [ 'endpoints' => [ 'save' => '/examples/backend/3-auth/save-as-user.php', // ← here // … ], // … ]; -
Swap to PDO for production
When you outgrow a flat file, swap to PDO with one line. The
Auth::pdo($pdo)driver expects auserstable; the schema is in_lib/Auth.php's class DocBlock and reproduced below. Same fluent API; same session contract.PHPuse DemoBuilder\Db; use DemoBuilder\Auth; $pdo = Db::connect(); // resolves DB_DSN env $auth = Auth::pdo($pdo); // Schema (run once): // CREATE TABLE users ( // id INTEGER PRIMARY KEY AUTOINCREMENT, // email TEXT NOT NULL UNIQUE, // password_hash TEXT NOT NULL, // created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) // );
The whole snippet
Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../backend/_lib/Validator.php';
require_once __DIR__ . '/../../../backend/_lib/JsonResponse.php';
require_once __DIR__ . '/../../../backend/_lib/Auth.php';
use DemoBuilder\Auth;
use DemoBuilder\JsonResponse;
use DemoBuilder\Validator;
$auth = Auth::file(__DIR__ . '/users.json');
$user = $auth->requireUser(); // 401 + exit if logged out
$payload = Validator::make($_POST)
->required('slug')->regex('/^[a-z0-9-]+$/i')->maxLength(120)
->required('html')->maxLength(5_000_000)
->required('data')->maxLength(10_000_000)
->validateOrFail();
$userDir = __DIR__ . '/pages/' . (int) $user['id'];
@mkdir($userDir, 0775, true);
$slug = (string) $payload['slug'];
file_put_contents("$userDir/$slug.html", $payload['html']);
file_put_contents("$userDir/$slug.json", $payload['data']);
JsonResponse::success([
'slug' => $slug,
'user' => $user,
'message' => 'Saved.',
]);
Notes
Try it. Register a throwaway account against the live endpoints below, then click Whoami to confirm the session round-trips.
What about CSRF? The demo form is same-origin so a real CSRF token isn't strictly required. For production wire your favourite CSRF middleware (e.g., a per-session token verified inside auth.php before calling Validator::make).
Lockout / rate-limit. Pure session auth has no built-in lockout; for production add a per-IP throttle in front of auth.php (Cloudflare Turnstile, fail2ban, or your reverse-proxy's rate-limit module).
Storing pages elsewhere. The example writes to pages/{user-id}/. Buyers ready to combine W5.1 + W5.3 should: (a) keep Auth::pdo($pdo) for users; (b) extend mysql-save.php's schema with a user_id column; (c) call $auth->requireUser() at the top of the save handler. The contract collapses to one DB.