<h1>Security API – <code>Security</code> Class</h1>
<p>
    The <strong>Security</strong> class provides a complete authentication, authorisation and
    protection toolkit. It covers user login/logout, session management, permissions,
    owner‑based row‑level security, password hashing, CSRF, rate limiting, TOTP two‑factor
    authentication and secure "Remember Me" cookies.
    Every method is static and safe – no manual session manipulation needed.
</p>

<h2>Contents</h2>
<ul>
    <li><a href="#recipes"><strong>Recipes – real-world one-liners</strong></a></li>
    <li><a href="#auth">Authentication – Login, Logout, LoginAs</a></li>
    <li><a href="#state">User State – IsLoggedIn, IsAdmin, CurrentUser*, etc.</a></li>
    <li><a href="#guards">Auth Guards – RequireAuth, RequireRole</a></li>
    <li><a href="#permissions">Table‑level Permissions</a></li>
    <li><a href="#allowedpages">Allowed Pages</a></li>
    <li><a href="#ownerid">OwnerID (Row‑level Security)</a></li>
    <li><a href="#password">Password Utilities</a></li>
    <li><a href="#rememberme">Remember Me</a></li>
    <li><a href="#csrf">CSRF Protection</a></li>
    <li><a href="#ratelimit">Rate Limiting</a></li>
    <li><a href="#platform-2fa">Two‑factor for your console account (setup)</a></li>
    <li><a href="#totp">Two‑factor Authentication (TOTP) — developer API</a></li>
    <li><a href="#displayname">Display Name</a></li>
    <li><a href="#token">Secure Token Generation</a></li>
</ul>

<!-- =========================================== -->
<h2 id="recipes">Recipes – real-world one-liners</h2>
<p class="lead">The everyday security jobs, the short way. Most reads are also available through
<a href="<?= htmlspecialchars(rtrim((string)($baseUrl ?? ''), '/')) ?>/help/pp_server_api"><code>pp()</code></a>
(<code>pp()->user()</code>, <code>pp()->role()</code>, <code>pp()->can()</code>) — use whichever reads best.</p>

<h3>Who's logged in?</h3>
<pre><code>if (!Security::IsLoggedIn()) redirect(pp()->url('login'));
$name = Security::CurrentUsername();          // "jsmith"
$me   = Security::CurrentUserData();          // the whole user row
$role = Security::CurrentUserGroup();         // "admin" / "user" …
$admin = Security::IsAdmin();</code></pre>

<h3>Guard a page or an action</h3>
<pre><code>Security::RequireAuth();                       // must be logged in (redirects otherwise)
Security::RequireRole('admin');                // must have this role

// a softer, inline check that returns a bool
if (!Security::HasPermission('edit', 'orders')) {
    pp()->toast('error', 'You cannot edit orders');
    return;
}</code></pre>

<h3>Scope data to the current user (row-level security)</h3>
<pre><code>// only show the signed-in user's own records
$mine = pp()->table('tickets')->where('assigned_to', Security::GetOwnerId())->get();

// …or, in a Designer WHERE tab / Lookup, the SQL-variable way:
//   assigned_to = :userid
// see the SQL Variables guide.</code></pre>

<h3>Passwords — never store plain text</h3>
<pre><code>$hash = Security::HashPassword(post('password'));       // bcrypt/argon
pp()->update('users', $id, ['password' => $hash]);

// on login
if (Security::VerifyPassword(post('password'), $row['password'])) { /* ok */ }</code></pre>

<h3>Protect a form (CSRF) &amp; slow down brute force</h3>
<pre><code>// in the form
echo '&lt;input type="hidden" name="_token" value="' . Security::CSRF()->Token() . '"&gt;';
// on submit
if (!Security::CSRF()->Check(post('_token'))) { http_response_code(419); exit; }

// rate-limit an endpoint: 5 tries per 60s per key
if (!Security::RateLimit('login:' . post('username'), 5, 60)) {
    pp()->toast('error', 'Too many attempts — try again soon');
    return;
}</code></pre>

<h3>A one-off secure token</h3>
<pre><code>$token = Security::GenerateToken(32);           // e.g. a password-reset / invite link
pp()->insert('invites', ['email' => post('email'), 'token' => $token, 'created' => now()]);</code></pre>

<!-- =========================================== -->
<h2 id="auth">Authentication</h2>

<h3>Login – Standard username/password</h3>
<pre><code>// Basic login
if (Security::Login($username, $password)) {
    echo "Welcome, " . Security::CurrentUsername();
} else {
    echo "Invalid credentials.";
}</code></pre>

<h3>Login with “Remember Me”</h3>
<pre><code>// Pass true to set a persistent cookie (30 days)
Security::Login($username, $password, true);</code></pre>

<h3>Login without password (e.g. after OAuth)</h3>
<pre><code>// LoginAs only needs a username – no password check
Security::LoginAs('john.doe');</code></pre>

<h3>Check credentials without logging in</h3>
<pre><code>if (Security::CheckUsernamePassword('admin', 'secret')) {
    // valid, but session is unchanged
}</code></pre>

<h3>Logout</h3>
<pre><code>Security::Logout();
// After logout, all session data is removed.
// You should redirect to the login page.
header('Location: /login');</code></pre>

<!-- =========================================== -->
<h2 id="state">User State</h2>
<pre><code>// Is anybody logged in?
if (Security::IsLoggedIn()) { … }

// Is the current user a superadmin?
if (Security::IsAdmin()) { … }

// Is the visitor a guest?
if (Security::IsGuest()) { … }

// Get the username
echo Security::CurrentUsername();   // e.g. "john.doe"

// Get the role/group
echo Security::CurrentUserGroup();  // "superadmin", "developer", "user"

// Get all groups (array) – currently wraps role in an array
$groups = Security::CurrentUserGroups();

// Full user data array (cached in session)
$user = Security::CurrentUserData();
echo $user['email'];

// Reload user data from database
Security::RefreshUserData();</code></pre>

<!-- =========================================== -->
<h2 id="permissions">Table‑level Permissions</h2>
<p>
    Each table has seven permission bits:
    <strong>A</strong>dd, <strong>D</strong>elete, <strong>E</strong>dit,
    <strong>S</strong>earch/List, <strong>P</strong>rint/Export,
    <strong>I</strong>mport, <strong>M</strong>anage (admin).
</p>

<h3>Set permissions for a table</h3>
<pre><code>// Using a string (each letter grants the corresponding right)
Security::SetPermissions('orders', 'ADES');   // can add, delete, edit, search

// Using an array
Security::SetPermissions('products', [
    'A' => true,
    'D' => false,
    'E' => true,
    'S' => true,
    'P' => true,
    'I' => false,
    'M' => false
]);</code></pre>

<h3>Check a specific permission</h3>
<pre><code>if (Security::HasPermission('orders', 'E')) {
    echo "User can edit orders.";
} else {
    echo "Edit access denied.";
}</code></pre>

<h3>Retrieve all permissions for a table</h3>
<pre><code>$perms = Security::GetPermissions('orders');
if ($perms['A']) { echo "Can add"; }</code></pre>

<!-- =========================================== -->
<h2 id="allowedpages">Allowed Pages</h2>
<p>
    Fine‑grained control over which individual pages a user can access for a
    table and page type (list, add, edit, view, print, search).
</p>

<h3>Set allowed pages</h3>
<pre><code>// Allow only the "summary" and "details" pages for the list type
Security::SetAllowedPages('orders', 'list', ['summary', 'details']);

// A single page as a string
Security::SetAllowedPages('orders', 'add', 'order_add');</code></pre>

<h3>Get allowed pages</h3>
<pre><code>$pages = Security::GetAllowedPages('orders', 'list');
if (in_array('summary', $pages)) {
    echo "Summary page is allowed.";
}</code></pre>

<!-- =========================================== -->
<h2 id="ownerid">OwnerID (Row‑level Security)</h2>
<p>
    When a table has “own data only” security, the system needs to know
    which ID belongs to the current user. OwnerID stores that value.
</p>

<h3>Set OwnerID</h3>
<pre><code>// After login, set the user's ID as the owner for the customers table
Security::SetOwnerId('customers', $user['id']);</code></pre>

<h3>Get OwnerID</h3>
<pre><code>$ownerId = Security::GetOwnerId('customers');
DB::Select('customers', ['assigned_to' => $ownerId]);</code></pre>

<!-- =========================================== -->
<h2 id="password">Password Utilities</h2>

<h3>Hash a password</h3>
<pre><code>$hashed = Security::HashPassword('mySecureP@ss');
// Uses bcrypt (PASSWORD_DEFAULT)</code></pre>

<h3>Verify a password</h3>
<pre><code>if (Security::VerifyPassword('mySecureP@ss', $hashed)) {
    // valid
}</code></pre>

<h3>Check if a hash needs rehashing</h3>
<pre><code>if (Security::PasswordNeedsRehash($hashed)) {
    $newHash = Security::HashPassword($newPassword);
    DB::Update('users', ['password' => $newHash], ['id' => $userId]);
}</code></pre>

<!-- =========================================== -->
<h2 id="rememberme">Remember Me</h2>
<pre><code>// When logging in with rememberMe = true, a long‑lived cookie is set.
Security::Login($username, $password, true);

// On subsequent visits, before any authentication check:
if (!Security::IsLoggedIn() && Security::LoginFromRememberMe()) {
    // User was logged in via cookie; session is now active.
}</code></pre>
<div class="alert alert-info">
    <i class="bi bi-info-circle"></i>
    <strong>Security note:</strong> The “Remember Me” feature stores a hashed token in the
    database. The plain token is never stored on the server.
</div>

<!-- =========================================== -->
<h2 id="csrf">CSRF Protection</h2>
<pre><code>// Generate a CSRF token (stored in session)
$token = Security::CSRF()->Token();

// Output a hidden field in a form
echo Security::CSRF()->HiddenField();

// Check a submitted token
if (!Security::CSRF()->Check($_POST['_token'] ?? '')) {
    die('Invalid CSRF token.');
}

// Regenerate token (e.g. after login)
Security::CSRF()->Regenerate();</code></pre>
<div class="alert alert-warning">
    <i class="bi bi-exclamation-triangle"></i>
    <strong>Important:</strong> Always include the CSRF token in POST forms.
    The <code>Check()</code> method uses <code>hash_equals()</code> for timing‑safe comparison.
</div>

<!-- =========================================== -->
<h2 id="ratelimit">Rate Limiting</h2>
<pre><code>// Protect a login endpoint from brute‑force attacks
$key = 'login:' . $_SERVER['REMOTE_ADDR'];
$maxAttempts = 5;
$window = 60; // 60 seconds

if (!Security::RateLimit()->Check($key, $maxAttempts, $window)) {
    $remaining = Security::RateLimit()->Remaining($key, $maxAttempts, $window);
    die("Too many attempts. Try again in $window seconds.");
}

// After a failed login:
Security::RateLimit()->Hit($key);

// After a successful login, you can clear the rate limit:
// (Manually unset the session key or wait for the window to expire)</code></pre>

<!-- =========================================== -->
<h2 id="platform-2fa">Two‑factor for your console account <span class="badge bg-secondary" style="font-size:.55em;vertical-align:middle">all roles</span></h2>
<p>
    This is the ready-to-use, self-service 2FA for the <strong><?= htmlspecialchars(\PpPhp\Core\Brand::name()) ?> console login itself</strong>
    (distinct from the developer <a href="#totp"><code>Security::TOTP()</code></a> API below, which is for building 2FA into <em>generated</em> apps).
    Any signed-in user — user, developer, superadmin or owner — can turn it on for their own account.
</p>
<ul>
    <li><strong>Where:</strong> your avatar menu → <em>Security &amp; 2FA</em>, or open <code>/account/security</code> directly (also linked from your Profile page).</li>
    <li><strong>Turn it on:</strong> flip the master switch — the setup expands. Pick <strong>Authenticator app</strong> (scan the QR with Google Authenticator / Authy / 1Password, or type the key), or <strong>Email codes</strong> (a 6-digit code emailed at each sign-in). Confirm one code to enable it.</li>
    <li><strong>Backup codes:</strong> you get 10 single-use recovery codes, shown once — save them. Regenerate a fresh set anytime.</li>
    <li><strong>Signing in:</strong> after your password, a verification screen asks for a code from your app/email, or a backup code if your device is lost.</li>
    <li><strong>Turn it off:</strong> confirm your current password in the <em>Turn off two-factor</em> card.</li>
</ul>
<p style="color:var(--pp-text-muted,#94a3b8);font-size:.9em">
    Notes: email codes require the platform email to be configured under <em>Settings</em>. The owner's signed break-glass access token bypasses 2FA by design, so a lost device can never lock the vendor out. There's also an in-page <strong>Guide</strong> button on the security page.
</p>

<!-- =========================================== -->
<h2 id="totp">Two‑factor Authentication (TOTP) — developer API</h2>
<pre><code>// Generate a secret for a user
$secret = Security::TOTP()->GenerateSecret();   // e.g. "JBSWY3DPEHPK3PXP"
DB::Update('users', ['totp_secret' => $secret], ['id' => $userId]);

// Get the QR code URL (for apps like Google Authenticator)
$qrUrl = Security::TOTP()->GetQRCodeUrl($username, $secret, 'MyApp');
// Display as a QR image using a service or JavaScript library

// Verify a code entered by the user
$code = $_POST['totp_code'];   // 6 digits
if (Security::TOTP()->Verify($secret, $code)) {
    echo "Code is valid!";
} else {
    echo "Invalid code.";
}

// Verify with a wider time drift (±2 periods, ±60 seconds)
Security::TOTP()->Verify($secret, $code, 2);</code></pre>

<!-- =========================================== -->
<h2 id="displayname">Display Name</h2>
<pre><code>// Get the current user's display name
echo Security::GetDisplayName();   // returns fullname, or username as fallback

// Set a custom display name in the session (temporary)
Security::SetDisplayName('John (Admin)');</code></pre>

<!-- =========================================== -->
<h2 id="token">Secure Token Generation</h2>
<pre><code>// Generate a cryptographically secure random token (hex)
$token = Security::GenerateToken();           // 64 hex chars (32 bytes)
$shortToken = Security::GenerateToken(16);    // 32 hex chars (16 bytes)
// Use for CSRF, password resets, invitation links, etc.</code></pre>

<!-- =========================================== -->
<h2 id="guards">Auth Guards – RequireAuth, RequireRole</h2>
<p>
    These are shortcut methods that immediately stop the request if the user
    is not authenticated or lacks the required role. They replace the
    repetitive guard code that was previously duplicated in every controller.
</p>

<h3>RequireAuth()</h3>
<p>Redirects to the login page if the user is not logged in. Use it at the top of any protected method.</p>
<pre><code>// Protect a whole controller method
public function listProjects(): void {
    Security::RequireAuth();
    // ... your code here
}</code></pre>

<h3>RequireRole(array $roles)</h3>
<p>Stops with a 403 Forbidden message if the current user's role is not in the allowed list.</p>
<pre><code>// Only superadmin and developer can create projects
Security::RequireRole(['superadmin', 'developer']);</code></pre>

<h3>Combined example</h3>
<pre><code>public function createProject(): void {
    Security::RequireAuth();
    Security::RequireRole(['superadmin', 'developer']);
    // proceed with project creation...
}</code></pre>

<hr>
<h3>🔗 Related Help Topics</h3>
<p>
    <a href="DB" class="btn btn-sm btn-outline-primary me-1">Database API</a>
    <a href="Email" class="btn btn-sm btn-outline-primary me-1">Email API</a>
    <a href="SMS" class="btn btn-sm btn-outline-primary">SMS API</a>
</p>