Mercurial
view mrjunejune/admin_api.c @ 273:e02e2036ef84 default tip
add Layer 2 JRPG component system
Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 08 Aug 2026 02:08:08 -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 */