<?php
/**
 * Active sessions API.
 *
 * GET  ?action=list               -> JSON list of active nonce rows AND
 *                                    persistent-auth-token rows. Scoped to
 *                                    AUTH_USER unless the caller is root.
 *                                    Each row has source='nonce'|'token'.
 * POST ?action=terminate          -> body: nonce=<sha1>. Deletes the row if the
 *                                    caller owns it (or is root).
 * POST ?action=terminate_token    -> body: selector=<16-hex>. Deletes a persist
 *                                    token row, same auth as terminate.
 * POST ?action=terminate_user     -> body: user=<username>. Root-only bulk.
 *                                    Deletes both nonces and persist tokens.
 *
 * auto-prepend.php handles auth, nonce validation, and CSRF (the latter on
 * POSTs); we just verify AUTH_USER is set and authorize per-row.
 */

header('Content-Type: application/json');

if (!defined('AUTH_USER')) {
    http_response_code(403);
    echo json_encode(['success' => false, 'error' => 'Not authenticated']);
    exit;
}

$action = $_REQUEST['action'] ?? 'list';
$is_root = (AUTH_USER === 'root');
$current_session_nonce = $_SESSION['nonce'] ?? null;

// Selector half of the caller's persist-token cookie (if any). Used to mark
// the matching token row as "this session" in the UI list. The verifier
// half stays out of the response; we never echo secrets.
$current_token_selector = null;
if (!empty($_COOKIE['whp_persist'])
    && preg_match('/^([a-f0-9]{16}):[a-f0-9]{64}$/', $_COOKIE['whp_persist'], $cs_match)) {
    $current_token_selector = $cs_match[1];
}

// $SecMan is normally instantiated by auto-prepend; fall back to a fresh
// instance if the page-context isn't carrying it (e.g., direct cron-style
// invocations during smoke tests).
if (!isset($SecMan) || !($SecMan instanceof security_wrapper)) {
    require_once('/docker/whp/web/libs/security.php');
    $SecMan = new security_wrapper();
}

switch ($action) {
    case 'list':
        $scope_user = $is_root ? null : AUTH_USER;
        $nonce_rows = $SecMan->list_nonces($scope_user);
        $token_rows = $SecMan->list_persist_tokens($scope_user);
        $out = [];
        foreach ($nonce_rows as $r) {
            $out[] = [
                'source'     => 'nonce',
                'id'         => $r['nonce'],
                'nonce'      => $r['nonce'],
                'user'       => $r['user'],
                'ip'         => $r['ip'],
                'last_time'  => (int)$r['last_time'],
                'persist'    => !empty($r['persist']),
                'is_current' => $current_session_nonce !== null && $r['nonce'] === $current_session_nonce,
            ];
        }
        foreach ($token_rows as $r) {
            $out[] = [
                'source'       => 'token',
                'id'           => $r['selector'],
                'selector'     => $r['selector'],
                'user'         => $r['user'],
                'ip'           => $r['ip'],
                'last_time'    => (int)$r['last_used_time'],
                'created_time' => (int)$r['created_time'],
                'expires_time' => (int)$r['expires_time'],
                'persist'      => true,
                'is_current'   => $current_token_selector !== null && $r['selector'] === $current_token_selector,
            ];
        }
        echo json_encode(['success' => true, 'sessions' => $out]);
        break;

    case 'terminate':
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Method not allowed']);
            exit;
        }
        $nonce = $_POST['nonce'] ?? '';
        if ($nonce === '' || !preg_match('/^[a-f0-9]{40}$/', $nonce)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Invalid nonce']);
            exit;
        }
        $row = $SecMan->get_nonce($nonce);
        if (!$row) {
            // Already gone — treat as success (idempotent).
            echo json_encode(['success' => true, 'already_gone' => true]);
            exit;
        }
        // Authorize: root can kill any row; user can only kill their own.
        if (!$is_root && $row['user'] !== AUTH_USER) {
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Not authorized to terminate this session']);
            exit;
        }
        $SecMan->delete_nonce($nonce);
        $self_terminated = $current_session_nonce !== null && $nonce === $current_session_nonce;
        // If the user just terminated their OWN session, the paired persist
        // token would silently re-establish them via recovery on the next
        // request. Tear it down too so "Terminate this session" really means
        // logged out. Other devices' tokens are untouched (different selector).
        if ($self_terminated && $current_token_selector !== null) {
            $SecMan->delete_persist_token_by_selector($current_token_selector);
        }
        echo json_encode([
            'success'         => true,
            'self_terminated' => $self_terminated,
        ]);
        break;

    case 'terminate_token':
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Method not allowed']);
            exit;
        }
        $selector = $_POST['selector'] ?? '';
        if ($selector === '' || !preg_match('/^[a-f0-9]{16}$/', $selector)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Invalid selector']);
            exit;
        }
        $token_row = $SecMan->get_persist_token_by_selector($selector);
        if (!$token_row) {
            // Already gone — idempotent success, matches the 'terminate' shape.
            echo json_encode(['success' => true, 'already_gone' => true]);
            exit;
        }
        if (!$is_root && $token_row['user'] !== AUTH_USER) {
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Not authorized to terminate this session']);
            exit;
        }
        $SecMan->delete_persist_token_by_selector($selector);
        $self_terminated = $current_token_selector !== null && $selector === $current_token_selector;
        // Mirror of the terminate-nonce branch: if the user killed their own
        // persist token, also kill the live session nonce so they aren't left
        // in a half-state where the in-browser PHPSESSID still resolves.
        if ($self_terminated && $current_session_nonce !== null) {
            $SecMan->delete_nonce($current_session_nonce);
        }
        echo json_encode([
            'success'         => true,
            'self_terminated' => $self_terminated,
        ]);
        break;

    case 'terminate_user':
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Method not allowed']);
            exit;
        }
        if (!$is_root) {
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Root only']);
            exit;
        }
        $target_user = $_POST['user'] ?? '';
        if ($target_user === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $target_user)) {
            http_response_code(400);
            echo json_encode(['success' => false, 'error' => 'Invalid username']);
            exit;
        }
        // Wipe both kinds of credential — leaving persist tokens behind would
        // defeat the bulk-terminate use case (compromised-account response).
        $SecMan->delete_nonces_by_user($target_user);
        $SecMan->delete_persist_tokens_by_user($target_user);
        echo json_encode(['success' => true]);
        break;

    default:
        http_response_code(400);
        echo json_encode(['success' => false, 'error' => 'Unknown action: ' . $action]);
        break;
}
