Backend · 3 of 4

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

  1. Start a session, hash passwords

    Auth::file($path) auto-starts the session, lazy-creates users.json, and uses password_hash(PASSWORD_DEFAULT) for storage and password_verify for comparison. Plaintext passwords never touch disk.

    PHP
    use 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' => '…']
  2. Guard the save endpoint

    On every save call, $auth->requireUser() returns the row or emits 401 + exit. The save handler can assume $user is non-null after the guard and key the storage path by user['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']);
  3. Wire the builder

    Point endpoints.save in $builderConfig at save-as-user.php. The browser already carries the PHPSESSID cookie, so the saved data lands under that user's directory automatically. Logged-out clicks return 401; 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
            // …
        ],
        // …
    ];
  4. Swap to PDO for production

    When you outgrow a flat file, swap to PDO with one line. The Auth::pdo($pdo) driver expects a users table; the schema is in _lib/Auth.php's class DocBlock and reproduced below. Same fluent API; same session contract.

    PHP
    use 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)
    //   );

Live demo

demo-mini-builder--minimal
Not logged in

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.

2 files · 73 lines · 2.3 KB
<?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);
}
<?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.

Submit a form action to see the JSON response.

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.