view mrjunejune/admin_api.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents 04fee26ecce0
children
line wrap: on
line source

#include "mrjunejune/admin_api.h"
#include "mrjunejune/auth_api.h"
#include "mrjunejune/template_renderer.h"

#include "auth/auth_store.h"
#include "auth/auth_crypto.h"
#include "seobeo/seobeo.h"
#include "dowa/dowa.h"

#include <openssl/crypto.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* ------------------------------------------------------------------ */
/* Constants                                                            */
/* ------------------------------------------------------------------ */

#define ADMIN_BODY_MAX_BYTES   4096
#define ADMIN_UUID_SIZE        37

/* ------------------------------------------------------------------ */
/* Internal helpers                                                     */
/* ------------------------------------------------------------------ */

static const char *admin_req_value(
    Seobeo_Request_Entry *p_req,
    const char           *key)
{
  void *p = Dowa_HashMap_Get_Ptr(p_req, (char *)key);
  return p ? ((Seobeo_Request_Entry *)p)->value : NULL;
}

static boolean admin_extract_secret_field(
    Dowa_JSON_Entry *obj,
    const char      *key,
    char            *out_buf,
    size_t           max_len)
{
  char *arena_ptr = Dowa_JSON_Get_String(obj, key);
  if (!arena_ptr || arena_ptr[0] == '\0')
    return FALSE;

  size_t field_len = strlen(arena_ptr);
  if (field_len > max_len)
  {
    OPENSSL_cleanse(arena_ptr, field_len);
    return FALSE;
  }

  memcpy(out_buf, arena_ptr, field_len);
  out_buf[field_len] = '\0';
  OPENSSL_cleanse(arena_ptr, field_len);
  return TRUE;
}

static Seobeo_Request_Entry *admin_json_response(
    Dowa_Arena *p_arena,
    const char *status,
    const char *body)
{
  Seobeo_Request_Entry *resp = NULL;
  Dowa_HashMap_Push_Arena(resp, "status", (char *)status, p_arena);
  Dowa_HashMap_Push_Arena(
      resp, "content-type", "application/json; charset=utf-8", p_arena);
  Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", p_arena);
  Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", p_arena);
  Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", p_arena);
  Dowa_HashMap_Push_Arena(resp, "body", (char *)body, p_arena);
  return resp;
}

static Seobeo_Request_Entry *admin_error(
    Dowa_Arena *p_arena,
    const char *status,
    const char *code,
    const char *message)
{
  char buf[512];
  snprintf(buf, sizeof(buf),
           "{\"error\":{\"code\":\"%s\",\"message\":\"%s\"}}",
           code, message);
  char *body = Dowa_Arena_Allocate(p_arena, strlen(buf) + 1);
  if (body) strcpy(body, buf);
  return admin_json_response(p_arena, status, body ? body : "{}");
}

static Seobeo_Request_Entry *admin_html_redirect(
    Dowa_Arena *p_arena,
    const char *location)
{
  Seobeo_Request_Entry *resp = NULL;
  Dowa_HashMap_Push_Arena(resp, "status",   "302",          p_arena);
  Dowa_HashMap_Push_Arena(resp, "Location", (char *)location, p_arena);
  Dowa_HashMap_Push_Arena(resp, "body",     "",             p_arena);
  return resp;
}

/*
 * Gate: resolve principal, enforce admin+active, reject forced-password.
 * On failure sets *pp_err_resp and returns FALSE.
 * On success fills *p_principal and returns TRUE.
 *
 * For page requests (is_page==TRUE), unauthenticated → 302 redirect.
 * For API requests (is_page==FALSE), unauthenticated → 401 JSON.
 */
static boolean admin_require_admin(
    Seobeo_Request_Entry  *p_req,
    Auth_Principal        *p_principal,
    Dowa_Arena            *p_arena,
    boolean                is_page,
    Seobeo_Request_Entry **pp_err_resp)
{
  Auth_Store *store = Auth_API_Get_Store();
  if (!store)
  {
    *pp_err_resp = admin_error(p_arena, "503", "service_unavailable",
                               "Auth not initialised");
    return FALSE;
  }

  boolean found = FALSE;
  if (!Auth_API_Resolve_Existing_Principal(p_req, p_principal, p_arena, &found))
  {
    *pp_err_resp = admin_error(p_arena, "500", "internal_error",
                               "Session error");
    return FALSE;
  }

  if (!found || p_principal->kind != AUTH_PRINCIPAL_USER)
  {
    *pp_err_resp = is_page
        ? admin_html_redirect(p_arena, "/login")
        : admin_error(p_arena, "401", "unauthenticated",
                      "Authentication required");
    return FALSE;
  }

  if (p_principal->must_change_password)
  {
    *pp_err_resp = is_page
        ? admin_html_redirect(p_arena, "/account/password")
        : admin_error(p_arena, "403", "password_change_required",
                      "Password change required");
    return FALSE;
  }

  if (strcmp(p_principal->role, "admin") != 0)
  {
    *pp_err_resp = admin_error(p_arena, "403", "forbidden",
                               "Admin access required");
    return FALSE;
  }

  *pp_err_resp = NULL;
  return TRUE;
}

/*
 * Validate that `:id` param is a non-empty UUID-shaped string.
 * Fills id_out (capacity >= 37).  Returns FALSE on invalid/missing.
 */
static boolean admin_get_id_param(
    Seobeo_Request_Entry *p_req,
    char                 *id_out,
    size_t                capacity)
{
  void *kv = Dowa_HashMap_Get_Ptr(p_req, ":id");
  if (!kv) return FALSE;
  const char *val = ((Seobeo_Request_Entry *)kv)->value;
  if (!val || val[0] == '\0') return FALSE;
  size_t vlen = strlen(val);
  if (vlen != 36) return FALSE;  /* UUID is 36 chars + NUL */
  if (vlen >= capacity) return FALSE;
  memcpy(id_out, val, vlen);
  id_out[vlen] = '\0';
  return TRUE;
}

/*
 * Append one JSON user object to buf (returns new offset, -1 on error).
 * Never includes password_hash, session digests, or guest IDs.
 * p_arena is used for temporary string escaping.
 */
static int admin_append_user_json(
    char                  *buf,
    size_t                 capacity,
    size_t                 offset,
    boolean                is_first,
    const Auth_User_Record *u,
    Dowa_Arena            *p_arena)
{
  char *safe_id   = Dowa_JSON_Escape_String(u->id,       0, p_arena);
  char *safe_user = Dowa_JSON_Escape_String(u->username, 0, p_arena);
  char *safe_role = Dowa_JSON_Escape_String(u->role,     0, p_arena);
  char *safe_stat = Dowa_JSON_Escape_String(u->status,   0, p_arena);
  if (!safe_id || !safe_user || !safe_role || !safe_stat)
    return -1;

  int n = snprintf(buf + offset, capacity - offset,
                   "%s{"
                   "\"id\":\"%s\","
                   "\"username\":\"%s\","
                   "\"role\":\"%s\","
                   "\"status\":\"%s\","
                   "\"mustChangePassword\":%s,"
                   "\"createdAt\":%lld,"
                   "\"updatedAt\":%lld,"
                   "\"passwordChangedAt\":%lld"
                   "}",
                   is_first ? "" : ",",
                   safe_id, safe_user, safe_role, safe_stat,
                   u->must_change_password ? "true" : "false",
                   (long long)u->created_at,
                   (long long)u->updated_at,
                   (long long)u->password_changed_at);

  if (n < 0 || (size_t)n >= capacity - offset)
    return -1;
  return (int)(offset + (size_t)n);
}

/* ------------------------------------------------------------------ */
/* Route: GET /admin/users  (page)                                      */
/* ------------------------------------------------------------------ */

static Seobeo_Request_Entry *admin_page_handler(
    Seobeo_Request_Entry *p_req,
    Dowa_Arena            *p_arena)
{
  Auth_Principal principal;
  Seobeo_Request_Entry *err = NULL;
  if (!admin_require_admin(p_req, &principal, p_arena, TRUE, &err))
    return err;

  char *body = Dowa_Arena_Allocate(p_arena, 128 * 1024);
  if (!body || !Mjj_Template_Render_File(body, 128 * 1024, "/admin/users/index.html", p_arena))
  {
    return admin_error(p_arena, "500", "internal_error", "Render failed");
  }

  Seobeo_Request_Entry *resp = NULL;
  Dowa_HashMap_Push_Arena(resp, "status", "200", p_arena);
  Dowa_HashMap_Push_Arena(resp, "content-type", "text/html; charset=utf-8", p_arena);
  Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", p_arena);
  Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", p_arena);
  Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", p_arena);
  Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", p_arena);
  Dowa_HashMap_Push_Arena(
      resp, "content-security-policy", "frame-ancestors 'none'", p_arena);
  Dowa_HashMap_Push_Arena(resp, "body", body, p_arena);
  return resp;
}

static Seobeo_Request_Entry *admin_list_handler(
    Seobeo_Request_Entry *p_req,
    Dowa_Arena            *p_arena)
{
  Auth_Principal principal;
  Seobeo_Request_Entry *err = NULL;
  if (!admin_require_admin(p_req, &principal, p_arena, FALSE, &err))
    return err;

  Auth_Store *store = Auth_API_Get_Store();

  /* Pagination params from query string; defaults applied. */
  int64 page  = 1;
  int64 limit = ADMIN_API_DEFAULT_LIMIT;
  const char *qpage  = admin_req_value(p_req, "Query-page");
  const char *qlimit = admin_req_value(p_req, "Query-limit");
  if (qpage  && qpage[0]  != '\0') page  = atol(qpage);
  if (qlimit && qlimit[0] != '\0') limit = atol(qlimit);
  if (page  < 1)                   page  = 1;
  if (limit < 1)                   limit = 1;
  if (limit > ADMIN_API_MAX_LIMIT) limit = ADMIN_API_MAX_LIMIT;

  Dowa_Arena *list_arena = Dowa_Arena_Create(128 * 1024);
  if (!list_arena)
    return admin_error(p_arena, "500", "internal_error", "OOM");

  Auth_User_Record *records = NULL;
  Auth_Store_Result res = Auth_Store_List_Users(store, &records, list_arena);
  if (res != AUTH_STORE_OK)
  {
    Dowa_Arena_Free(list_arena);
    return admin_error(p_arena, "500", "internal_error", "List users failed");
  }

  int64 total = (int64)Dowa_Array_Length(records);
  int64 offset_start = total;
  if (page - 1 <= total / limit)
    offset_start = (page - 1) * limit;
  if (offset_start > total)
    offset_start = total;
  int64 offset_end = total - offset_start < limit
      ? total
      : offset_start + limit;

  /* Build JSON response in the request arena. */
  size_t resp_size = 64 + (size_t)(total > 0 ? total : 1) * 400;
  char *body = Dowa_Arena_Allocate(p_arena, resp_size);
  if (!body)
  {
    Dowa_Arena_Free(list_arena);
    return admin_error(p_arena, "500", "internal_error", "OOM");
  }

  int n = snprintf(body, resp_size,
                   "{\"total\":%lld,\"page\":%lld,\"limit\":%lld,\"users\":[",
                   (long long)total, (long long)page, (long long)limit);
  if (n < 0)
  {
    Dowa_Arena_Free(list_arena);
    return admin_error(p_arena, "500", "internal_error", "Encode error");
  }
  size_t pos = (size_t)n;
  boolean first = TRUE;

  for (int64 i = offset_start; i < offset_end && pos < resp_size - 2; i++)
  {
    int nw = admin_append_user_json(body, resp_size, pos, first, &records[i], p_arena);
    if (nw < 0)
    {
      Dowa_Arena_Free(list_arena);
      return admin_error(p_arena, "500", "internal_error", "Encode error");
    }
    pos   = (size_t)nw;
    first = FALSE;
  }

  if (pos + 2 >= resp_size)
  {
    Dowa_Arena_Free(list_arena);
    return admin_error(p_arena, "500", "internal_error", "Buffer too small");
  }
  body[pos++] = ']';
  body[pos++] = '}';
  body[pos]   = '\0';

  Dowa_Arena_Free(list_arena);
  return admin_json_response(p_arena, "200", body);
}

/* ------------------------------------------------------------------ */
/* Route: POST /api/admin/users  (create)                               */
/* ------------------------------------------------------------------ */

static Seobeo_Request_Entry *admin_create_handler(
    Seobeo_Request_Entry *p_req,
    Dowa_Arena            *p_arena)
{
  Auth_Principal principal;
  Seobeo_Request_Entry *err = NULL;
  if (!admin_require_admin(p_req, &principal, p_arena, FALSE, &err))
    return err;

  if (!Auth_API_Verify_CSRF(p_req, &principal))
    return admin_error(p_arena, "403", "csrf_invalid", "CSRF check failed");

  const char *body_str = admin_req_value(p_req, "Body");
  if (!body_str)
    return admin_error(p_arena, "400", "bad_request", "Invalid request body");

  size_t body_len = strlen(body_str);
  if (body_len > ADMIN_BODY_MAX_BYTES)
  {
    OPENSSL_cleanse((char *)body_str, body_len);
    return admin_error(p_arena, "400", "bad_request", "Invalid request body");
  }
  Dowa_JSON_Value jv =
      Dowa_JSON_Parse(body_str, (int32)body_len, p_arena);
  OPENSSL_cleanse((char *)body_str, body_len);
  if (jv.type != DOWA_JSON_OBJECT)
    return admin_error(p_arena, "400", "bad_request", "Expected JSON object");

  Dowa_JSON_Entry *obj = (Dowa_JSON_Entry *)jv.object_val;
  char *username_raw  = Dowa_JSON_Get_String(obj, "username");
  char *role_raw      = Dowa_JSON_Get_String(obj, "role");
  char password_buf[AUTH_CRYPTO_PASSWORD_MAX_BYTES + 1];
  memset(password_buf, 0, sizeof(password_buf));
  boolean have_password = admin_extract_secret_field(
      obj, "temporaryPassword", password_buf,
      AUTH_CRYPTO_PASSWORD_MAX_BYTES);
  char *password_raw = password_buf;

  if (!username_raw || username_raw[0] == '\0' ||
      !have_password)
  {
    OPENSSL_cleanse(password_buf, sizeof(password_buf));
    return admin_error(p_arena, "400", "bad_request",
                       "username and temporaryPassword required");
  }

  const char *role = "member";
  if (role_raw && role_raw[0] != '\0')
  {
    if (strcmp(role_raw, "admin") != 0 && strcmp(role_raw, "member") != 0)
    {
      OPENSSL_cleanse(password_raw, strlen(password_raw));
      return admin_error(p_arena, "400", "invalid_role",
                         "role must be admin or member");
    }
    role = role_raw;
  }

  size_t pw_len = strlen(password_raw);
  if (pw_len < 12 || pw_len > AUTH_CRYPTO_PASSWORD_MAX_BYTES)
  {
    OPENSSL_cleanse(password_raw, pw_len);
    return admin_error(p_arena, "400", "password_policy",
                       "Password must be 12 to 1024 characters");
  }

  char encoded_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  Auth_Crypto_Result hr =
      Auth_Crypto_Password_Hash(password_raw, encoded_hash, sizeof(encoded_hash));
  OPENSSL_cleanse(password_raw, pw_len);

  if (hr != AUTH_CRYPTO_OK)
  {
    OPENSSL_cleanse(encoded_hash, sizeof(encoded_hash));
    return admin_error(p_arena, "500", "internal_error", "Hash error");
  }

  char new_id[ADMIN_UUID_SIZE];
  Auth_Store *store = Auth_API_Get_Store();
  Auth_Store_Result cr = Auth_Store_Create_User_Audited(
      store,
      username_raw,
      encoded_hash,
      role,
      TRUE,   /* must_change_password */
      principal.user_id,
      new_id);
  OPENSSL_cleanse(encoded_hash, sizeof(encoded_hash));

  if (cr == AUTH_STORE_CONFLICT)
    return admin_error(p_arena, "409", "conflict", "Username already exists");
  if (cr != AUTH_STORE_OK)
    return admin_error(p_arena, "500", "internal_error", "Create user failed");

  /* Fetch the created record for response. */
  Auth_User_Record rec;
  memset(&rec, 0, sizeof(rec));
  if (Auth_Store_Get_User(store, new_id, &rec) != AUTH_STORE_OK)
    return admin_error(p_arena, "500", "internal_error", "Fetch failed");

  char *safe_id   = Dowa_JSON_Escape_String(rec.id, 0, p_arena);
  char *safe_user = Dowa_JSON_Escape_String(rec.username, 0, p_arena);
  char *safe_role = Dowa_JSON_Escape_String(rec.role, 0, p_arena);
  char *safe_stat = Dowa_JSON_Escape_String(rec.status, 0, p_arena);
  if (!safe_id || !safe_user || !safe_role || !safe_stat)
    return admin_error(p_arena, "500", "internal_error", "Encode error");

  char body_buf[512];
  snprintf(body_buf, sizeof(body_buf),
           "{\"id\":\"%s\",\"username\":\"%s\",\"role\":\"%s\","
           "\"status\":\"%s\",\"mustChangePassword\":true,"
           "\"createdAt\":%lld}",
           safe_id, safe_user, safe_role, safe_stat,
           (long long)rec.created_at);

  char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body_buf) + 1);
  if (!body_copy)
    return admin_error(p_arena, "500", "internal_error", "OOM");
  strcpy(body_copy, body_buf);

  return admin_json_response(p_arena, "201", body_copy);
}

/* ------------------------------------------------------------------ */
/* Route: PATCH /api/admin/users/:id  (update)                          */
/* ------------------------------------------------------------------ */

static Seobeo_Request_Entry *admin_update_handler(
    Seobeo_Request_Entry *p_req,
    Dowa_Arena            *p_arena)
{
  Auth_Principal principal;
  Seobeo_Request_Entry *err = NULL;
  if (!admin_require_admin(p_req, &principal, p_arena, FALSE, &err))
    return err;

  if (!Auth_API_Verify_CSRF(p_req, &principal))
    return admin_error(p_arena, "403", "csrf_invalid", "CSRF check failed");

  char target_id[ADMIN_UUID_SIZE];
  if (!admin_get_id_param(p_req, target_id, sizeof(target_id)))
    return admin_error(p_arena, "400", "bad_request", "Invalid or missing id");

  const char *body_str = admin_req_value(p_req, "Body");
  if (!body_str)
    return admin_error(p_arena, "400", "bad_request", "Invalid request body");

  size_t body_len = strlen(body_str);
  if (body_len > ADMIN_BODY_MAX_BYTES)
  {
    OPENSSL_cleanse((char *)body_str, body_len);
    return admin_error(p_arena, "400", "bad_request", "Invalid request body");
  }
  Dowa_JSON_Value jv =
      Dowa_JSON_Parse(body_str, (int32)body_len, p_arena);
  OPENSSL_cleanse((char *)body_str, body_len);
  if (jv.type != DOWA_JSON_OBJECT)
    return admin_error(p_arena, "400", "bad_request", "Expected JSON object");

  Dowa_JSON_Entry *obj = (Dowa_JSON_Entry *)jv.object_val;
  char *op_raw         = Dowa_JSON_Get_String(obj, "op");
  char temporary_password[AUTH_CRYPTO_PASSWORD_MAX_BYTES + 1];
  memset(temporary_password, 0, sizeof(temporary_password));
  boolean have_temporary_password = admin_extract_secret_field(
      obj, "temporaryPassword", temporary_password,
      AUTH_CRYPTO_PASSWORD_MAX_BYTES);
  if (!op_raw || op_raw[0] == '\0')
  {
    OPENSSL_cleanse(temporary_password, sizeof(temporary_password));
    return admin_error(p_arena, "400", "bad_request", "op field required");
  }

  Auth_Store *store = Auth_API_Get_Store();
  Auth_Store_Result res = AUTH_STORE_OK;

  if (strcmp(op_raw, "enable") == 0)
  {
    res = Auth_Store_Enable_User(store, target_id, principal.user_id);
  }
  else if (strcmp(op_raw, "disable") == 0)
  {
    res = Auth_Store_Disable_User_And_Revoke_Sessions(
        store, target_id, principal.user_id);
  }
  else if (strcmp(op_raw, "set_role") == 0)
  {
    char *new_role = Dowa_JSON_Get_String(obj, "role");
    if (!new_role || new_role[0] == '\0')
    {
      OPENSSL_cleanse(temporary_password, sizeof(temporary_password));
      return admin_error(p_arena, "400", "bad_request",
                         "role required for set_role op");
    }
    if (strcmp(new_role, "admin") != 0 && strcmp(new_role, "member") != 0)
    {
      OPENSSL_cleanse(temporary_password, sizeof(temporary_password));
      return admin_error(p_arena, "400", "invalid_role",
                         "role must be admin or member");
    }
    res = Auth_Store_Update_Role_And_Revoke_Sessions(
        store, target_id, new_role, principal.user_id);
  }
  else if (strcmp(op_raw, "temp_reset") == 0)
  {
    if (!have_temporary_password)
    {
      OPENSSL_cleanse(
          temporary_password, sizeof(temporary_password));
      return admin_error(p_arena, "400", "bad_request",
                         "temporaryPassword required for temp_reset op");
    }
    char *pw_raw = temporary_password;

    size_t pw_len = strlen(pw_raw);
    if (pw_len < 12 || pw_len > AUTH_CRYPTO_PASSWORD_MAX_BYTES)
    {
      OPENSSL_cleanse(pw_raw, pw_len);
      return admin_error(p_arena, "400", "password_policy",
                         "Password must be 12 to 1024 characters");
    }

    char encoded_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
    Auth_Crypto_Result hr =
        Auth_Crypto_Password_Hash(pw_raw, encoded_hash, sizeof(encoded_hash));
    OPENSSL_cleanse(pw_raw, pw_len);

    if (hr != AUTH_CRYPTO_OK)
    {
      OPENSSL_cleanse(encoded_hash, sizeof(encoded_hash));
      return admin_error(p_arena, "500", "internal_error", "Hash error");
    }

    res = Auth_Store_Admin_Reset_Password(store, target_id, encoded_hash,
                                          principal.user_id);
    OPENSSL_cleanse(encoded_hash, sizeof(encoded_hash));
  }
  else
  {
    OPENSSL_cleanse(temporary_password, sizeof(temporary_password));
    return admin_error(p_arena, "400", "invalid_op",
                       "op must be enable, disable, set_role, or temp_reset");
  }

  OPENSSL_cleanse(temporary_password, sizeof(temporary_password));

  if (res == AUTH_STORE_NOT_FOUND)
    return admin_error(p_arena, "404", "not_found", "User not found");
  if (res == AUTH_STORE_LAST_ADMIN)
    return admin_error(p_arena, "409", "last_admin",
                       "Cannot remove the last active admin");
  if (res == AUTH_STORE_INVALID_ARG)
    return admin_error(p_arena, "400", "bad_request", "Invalid argument");
  if (res != AUTH_STORE_OK)
    return admin_error(p_arena, "500", "internal_error", "Update failed");

  /* Return updated user record. */
  Auth_User_Record rec;
  memset(&rec, 0, sizeof(rec));
  if (Auth_Store_Get_User(store, target_id, &rec) != AUTH_STORE_OK)
    return admin_json_response(p_arena, "200", "{\"ok\":true}");

  char *safe_id   = Dowa_JSON_Escape_String(rec.id, 0, p_arena);
  char *safe_user = Dowa_JSON_Escape_String(rec.username, 0, p_arena);
  char *safe_role = Dowa_JSON_Escape_String(rec.role, 0, p_arena);
  char *safe_stat = Dowa_JSON_Escape_String(rec.status, 0, p_arena);
  if (!safe_id || !safe_user || !safe_role || !safe_stat)
    return admin_error(p_arena, "500", "internal_error", "Encode error");

  char body_buf[512];
  snprintf(body_buf, sizeof(body_buf),
           "{\"id\":\"%s\",\"username\":\"%s\",\"role\":\"%s\","
           "\"status\":\"%s\",\"mustChangePassword\":%s,"
           "\"updatedAt\":%lld}",
           safe_id, safe_user, safe_role, safe_stat,
           rec.must_change_password ? "true" : "false",
           (long long)rec.updated_at);

  char *body_copy = Dowa_Arena_Allocate(p_arena, strlen(body_buf) + 1);
  if (!body_copy)
    return admin_error(p_arena, "500", "internal_error", "OOM");
  strcpy(body_copy, body_buf);

  return admin_json_response(p_arena, "200", body_copy);
}

/* ------------------------------------------------------------------ */
/* Route: DELETE /api/admin/users/:id/sessions  (revoke sessions)       */
/* ------------------------------------------------------------------ */

static Seobeo_Request_Entry *admin_revoke_sessions_handler(
    Seobeo_Request_Entry *p_req,
    Dowa_Arena            *p_arena)
{
  Auth_Principal principal;
  Seobeo_Request_Entry *err = NULL;
  if (!admin_require_admin(p_req, &principal, p_arena, FALSE, &err))
    return err;

  if (!Auth_API_Verify_CSRF(p_req, &principal))
    return admin_error(p_arena, "403", "csrf_invalid", "CSRF check failed");

  char target_id[ADMIN_UUID_SIZE];
  if (!admin_get_id_param(p_req, target_id, sizeof(target_id)))
    return admin_error(p_arena, "400", "bad_request", "Invalid or missing id");

  Auth_Store *store = Auth_API_Get_Store();

  Auth_Store_Result res = Auth_Store_Revoke_All_Sessions_Audited(
      store, target_id, NULL, principal.user_id);
  if (res == AUTH_STORE_NOT_FOUND)
    return admin_error(p_arena, "404", "not_found", "User not found");
  if (res != AUTH_STORE_OK)
    return admin_error(p_arena, "500", "internal_error", "Revoke failed");

  return admin_json_response(p_arena, "200", "{\"ok\":true}");
}

/* ------------------------------------------------------------------ */
/* Public API                                                           */
/* ------------------------------------------------------------------ */

void Admin_API_Register_Routes(void)
{
  Seobeo_Router_Register("GET",    "/admin/users",                   admin_page_handler);
  Seobeo_Router_Register("GET",    "/api/admin/users",               admin_list_handler);
  Seobeo_Router_Register("POST",   "/api/admin/users",               admin_create_handler);
  Seobeo_Router_Register("PATCH",  "/api/admin/users/:id",           admin_update_handler);
  Seobeo_Router_Register("DELETE", "/api/admin/users/:id/sessions",  admin_revoke_sessions_handler);
}

/* ------------------------------------------------------------------ */
/* Test hooks                                                           */
/* ------------------------------------------------------------------ */

#ifdef ADMIN_API_TEST_HOOKS
Seobeo_Request_Entry *Admin_API_Test_Page_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena)
{ return admin_page_handler(p_req, p_arena); }

Seobeo_Request_Entry *Admin_API_Test_List_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena)
{ return admin_list_handler(p_req, p_arena); }

Seobeo_Request_Entry *Admin_API_Test_Create_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena)
{ return admin_create_handler(p_req, p_arena); }

Seobeo_Request_Entry *Admin_API_Test_Update_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena)
{ return admin_update_handler(p_req, p_arena); }

Seobeo_Request_Entry *Admin_API_Test_Revoke_Sessions_Handler(
    Seobeo_Request_Entry *p_req, Dowa_Arena *p_arena)
{ return admin_revoke_sessions_handler(p_req, p_arena); }
#endif /* ADMIN_API_TEST_HOOKS */