Files
novaconium/novaconium/pages/admin/docs/session/index.twig
T
code b882c304b1 Replace Basic Auth with multi-user login, roles, groups, and Lib\Access
Admin login & user management (novaconium/ISSUES.md): session-based
login against a SQLite users table replaces the single-user HTTP Basic
Auth stopgap (admin_username/admin_password_hash and /admin/password-hash
are gone; one admin_auth_enabled flag, off by default with zero DB
footprint). New /admin/login, /admin/logout (POST-only, real page), and
/admin/users pages plus bin/create-admin-user.php.

First user created is the admin; everyone after is registered with a
unique normalized email and an optional group. /admin/* and drafts are
admin-only; Lib\Access gates page content from sidecars
(Access::require('group:members')) with login-redirect/404 responses —
public by default, static pages always public by construction. User
management covers disable/enable, delete, promote/demote, group, email,
and password, with last-active-admin lockout guards.

Also: Session::regenerate() against fixation, friendly missing-PDO-driver
errors in Lib\Db, docs at /admin/docs/access-control and updates across
admin-auth/drafts/sidecars/config/libraries and README/AGENTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:17:54 +00:00

49 lines
3.6 KiB
Twig

{% extends 'admin/docs/_layout/layout.twig' %}
{% import '_layout/icons.twig' as icons %}
{% block title %}Session{% endblock %}
{% block description %}Lib\Session — a thin wrapper around native PHP sessions, with CodeIgniter-style flash data.{% endblock %}
{% block robots %}noindex, nofollow{% endblock %}
{% block docs_content %}
<h1>Session</h1>
<p><code>Lib\Session</code> (<code>novaconium/lib/Session.php</code>) is a thin wrapper around PHP's native session handling — plain <code>session_start()</code>/<code>$_SESSION</code>, not a custom session store — so sidecars have a consistent get/set API instead of touching <code>$_SESSION</code> directly. It's a <a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Lib\</a> class like <code>Input</code>/<code>Csrf</code>/<code>Mailer</code>, so a project can override it entirely by dropping its own <code>App/lib/Session.php</code>.</p>
<h2>Using it</h2>
<pre><code>use Lib\Session;
Session::set('user_id', 42);
$userId = Session::get('user_id'); // 42
$loggedIn = Session::has('user_id'); // true
Session::remove('user_id');</code></pre>
<p>The session is started lazily — nothing calls <code>session_start()</code> until the first real call to any <code>Session</code> method, so a page that never touches <code>Session</code> never gets a session cookie. <a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Lib\Csrf</a> uses the exact same lazy-start mechanism to run its own session-token CSRF protection — both classes can touch the same native session in the same request without conflict, since <code>session_start()</code> is only ever actually called once (PHP no-ops a second call).</p>
<p><code>Session::regenerate()</code> swaps the session id for a fresh one while keeping the session's data — call it on any privilege change, so a session id an attacker planted or observed before the change is worthless after it (session fixation). <a class="icon-link" href="/admin/docs/admin-auth">{{ icons.lock() }}Admin authentication</a> does exactly this on login and logout.</p>
<h2>Flash data</h2>
<p>A flashed value is readable on exactly the next request, then gone — useful for post/redirect/GET flows (a "message sent" banner after a redirect) without a query-string flag like <code>?sent=1</code>:</p>
<pre><code>use Lib\Session;
use App\Response;
// In the sidecar handling the POST:
Session::flash('message', 'Sent! We\'ll be in touch soon.');
return Response::redirect('/contact');
// In the sidecar handling the following GET (the redirect target):
return [
'flashMessage' =&gt; Session::getFlash('message'),
];</code></pre>
<p><code>Session::getFlash($key, $default = null)</code> returns the value on the request immediately after <code>flash()</code> was called, and the default on every request after that — regardless of whether <code>getFlash()</code> was actually called on that one request in between. A value flashed during the current request is never visible to <code>getFlash()</code> during that same request; it becomes visible on the next one.</p>
<p>Mechanically, this is a single swap rather than a separate expiry/sweep step: the first time any <code>Session</code> method runs in a request, it snapshots whatever was flashed on the previous request into an in-memory value for that request's <code>getFlash()</code> calls, then immediately clears the stored flash bucket so <code>flash()</code> calls made during the current request start filling a fresh bucket — the one the next request will snapshot in turn.</p>
{% endblock %}