webui: rename auth_store → webui_store

The plugin's SQLite store will own more than accounts (taxonomy, RSS),
so give it a general name. Mechanical rename of files + symbols; default
DB filename is now webui.db. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-23 21:59:30 -04:00
parent ab51774554
commit 1b79fe8079
5 changed files with 85 additions and 84 deletions

View file

@ -224,7 +224,7 @@ set_target_properties(naut_example PROPERTIES PREFIX "")
find_package(PkgConfig REQUIRED)
pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)
add_library(naut_webui MODULE plugins/webui/webui.c plugins/webui/auth_store.c)
add_library(naut_webui MODULE plugins/webui/webui.c plugins/webui/webui_store.c)
target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} naut_net
PkgConfig::SQLITE3 OpenSSL::Crypto pthread)

View file

@ -1,46 +0,0 @@
/* auth_store.h — SQLite-backed user account store for the web UI.
*
* Owned entirely by the webui plugin. Passwords are stored as PBKDF2-HMAC-
* SHA256 hashes with a per-user random salt. All calls are thread-safe (the
* store serializes access to its single SQLite connection internally). */
#ifndef NAUT_WEBUI_AUTH_STORE_H
#define NAUT_WEBUI_AUTH_STORE_H
#include <stdbool.h>
#include <stddef.h>
#include <jansson.h>
typedef struct auth_store auth_store;
/* Open (creating if needed) the account database at `path`. Returns NULL on
* failure. The schema is created/migrated on open. */
auth_store *auth_store_open(const char *path);
void auth_store_close(auth_store *s);
/* Number of accounts, or -1 on error. */
int auth_store_user_count(auth_store *s);
/* Number of admin accounts, or -1 on error. */
int auth_store_admin_count(auth_store *s);
bool auth_store_user_exists(auth_store *s, const char *username);
/* Verify a username/password pair (constant-time). On success, copies the
* account's role ("admin"/"user") into role_out. */
bool auth_store_verify(auth_store *s, const char *username,
const char *password, char *role_out, size_t role_sz);
/* Create an account. `role` must be "admin" or "user" (defaults to "user" if
* NULL/invalid). Returns false if the username already exists or on error. */
bool auth_store_create_user(auth_store *s, const char *username,
const char *password, const char *role);
bool auth_store_set_password(auth_store *s, const char *username,
const char *password);
/* Change an account's role ("admin"/"user"). */
bool auth_store_set_role(auth_store *s, const char *username, const char *role);
bool auth_store_delete_user(auth_store *s, const char *username);
/* Append {username, role, createdAt} objects (sorted by username) to the
* json array `out`. Returns false on error. */
bool auth_store_list_users(auth_store *s, json_t *out);
#endif /* NAUT_WEBUI_AUTH_STORE_H */

View file

@ -1,6 +1,6 @@
#include "naut/naut_plugin.h"
#include "naut/http_client.h"
#include "auth_store.h"
#include "webui_store.h"
#include <jansson.h>
@ -59,7 +59,7 @@ typedef struct {
char host_name[64];
char auth_user[64]; /* bootstrap admin name (for startup banner) */
char auth_password[64]; /* generated bootstrap password (banner only) */
auth_store *auth; /* SQLite-backed account store */
webui_store *auth; /* SQLite-backed account store */
int port;
int listener;
bool generated_password;
@ -297,7 +297,7 @@ static bool resolve_auth_db_path(char *out, size_t n) {
else if (home && *home) snprintf(dir, sizeof dir, "%s/.local/share/naut", home);
else return false;
if (mkdir_p(dir, 0700) != 0) return false;
return (size_t)snprintf(out, n, "%s/webui_accounts.db", dir) < n;
return (size_t)snprintf(out, n, "%s/webui.db", dir) < n;
}
/* Open the account store and, on first run (no accounts), bootstrap an admin
@ -308,7 +308,7 @@ static void init_auth(void) {
log_msg(0, "webui: cannot resolve account DB path; set NAUT_WEBUI_DB");
return;
}
g_webui.auth = auth_store_open(db_path);
g_webui.auth = webui_store_open(db_path);
if (!g_webui.auth) {
log_msg(0, "webui: failed to open account database");
return;
@ -318,7 +318,7 @@ static void init_auth(void) {
if (!user || !*user) user = "admin";
snprintf(g_webui.auth_user, sizeof g_webui.auth_user, "%s", user);
if (auth_store_user_count(g_webui.auth) > 0) return; /* already set up */
if (webui_store_user_count(g_webui.auth) > 0) return; /* already set up */
/* No accounts yet — create the initial admin. */
const char *password = getenv("NAUT_AUTH_PASSWORD");
@ -332,7 +332,7 @@ static void init_auth(void) {
log_msg(0, "webui: no CSPRNG; set NAUT_AUTH_PASSWORD to create the admin");
return;
}
if (!auth_store_create_user(g_webui.auth, user, password, "admin"))
if (!webui_store_create_user(g_webui.auth, user, password, "admin"))
log_msg(0, "webui: failed to create the initial admin account");
}
@ -2722,7 +2722,7 @@ static void serve_cached_torrents(int fd) {
/* ============================ account management ========================== *
* Admin-only user CRUD plus a self-service password change. The web layer owns
* everything via auth_store; the daemon is not involved. */
* everything via webui_store; the daemon is not involved. */
static bool valid_username(const char *u) {
if (!u || !*u || strlen(u) >= 64) return false;
@ -2736,7 +2736,7 @@ static bool valid_username(const char *u) {
/* GET /api/users → [{username, role, createdAt}] (admin only). */
static void api_users_list(int fd) {
json_t *users = json_array();
if (g_webui.auth) auth_store_list_users(g_webui.auth, users);
if (g_webui.auth) webui_store_list_users(g_webui.auth, users);
http_json(fd, 200, users);
json_decref(users);
}
@ -2754,7 +2754,7 @@ static void api_user_create(int fd, const char *body, size_t len) {
return;
}
bool ok = g_webui.auth &&
auth_store_create_user(g_webui.auth, user, pass,
webui_store_create_user(g_webui.auth, user, pass,
role && *role ? role : "user");
json_decref(req);
if (!ok) { http_text(fd, 409, "Conflict", "user already exists"); return; }
@ -2773,18 +2773,18 @@ static void api_user_delete(int fd, const char *body, size_t len,
char role[16] = {0};
/* Look up the target's role to guard the last-admin rule. */
json_t *list = json_array();
if (g_webui.auth) auth_store_list_users(g_webui.auth, list);
if (g_webui.auth) webui_store_list_users(g_webui.auth, list);
size_t i; json_t *u;
json_array_foreach(list, i, u)
if (strcasecmp(json_string_or(u, "username", ""), user) == 0)
snprintf(role, sizeof role, "%s", json_string_or(u, "role", ""));
json_decref(list);
if (strcmp(role, "admin") == 0 && auth_store_admin_count(g_webui.auth) <= 1) {
if (strcmp(role, "admin") == 0 && webui_store_admin_count(g_webui.auth) <= 1) {
json_decref(req);
http_text(fd, 409, "Conflict", "cannot delete the last admin");
return;
}
bool ok = g_webui.auth && auth_store_delete_user(g_webui.auth, user);
bool ok = g_webui.auth && webui_store_delete_user(g_webui.auth, user);
json_decref(req);
if (!ok) { http_text(fd, 404, "Not Found", "no such user"); return; }
drop_user_sessions(user);
@ -2804,7 +2804,7 @@ static void api_user_set_password(int fd, const char *body, size_t len) {
}
char user[64];
snprintf(user, sizeof user, "%s", uname);
bool ok = g_webui.auth && auth_store_set_password(g_webui.auth, user, pass);
bool ok = g_webui.auth && webui_store_set_password(g_webui.auth, user, pass);
json_decref(req);
if (!ok) { http_text(fd, 404, "Not Found", "no such user"); return; }
drop_user_sessions(user); /* force re-login with the new password */
@ -2823,11 +2823,11 @@ static void api_user_set_role(int fd, const char *body, size_t len) {
http_text(fd, 400, "Bad Request", "username and role (admin|user) required");
return;
}
if (strcmp(role, "user") == 0 && auth_store_admin_count(g_webui.auth) <= 1) {
if (strcmp(role, "user") == 0 && webui_store_admin_count(g_webui.auth) <= 1) {
/* Only block if the target is currently the sole admin. */
char cur[16] = {0};
json_t *list = json_array();
if (g_webui.auth) auth_store_list_users(g_webui.auth, list);
if (g_webui.auth) webui_store_list_users(g_webui.auth, list);
size_t i; json_t *u;
json_array_foreach(list, i, u)
if (strcasecmp(json_string_or(u, "username", ""), user) == 0)
@ -2839,7 +2839,7 @@ static void api_user_set_role(int fd, const char *body, size_t len) {
return;
}
}
bool ok = g_webui.auth && auth_store_set_role(g_webui.auth, user, role);
bool ok = g_webui.auth && webui_store_set_role(g_webui.auth, user, role);
json_decref(req);
if (!ok) { http_text(fd, 404, "Not Found", "no such user"); return; }
api_users_list(fd);
@ -2857,12 +2857,12 @@ static void api_account_password(int fd, const char *body, size_t len,
return;
}
char role[16] = {0};
if (!g_webui.auth || !auth_store_verify(g_webui.auth, actor, oldp, role, sizeof role)) {
if (!g_webui.auth || !webui_store_verify(g_webui.auth, actor, oldp, role, sizeof role)) {
json_decref(req);
http_text(fd, 403, "Forbidden", "current password is incorrect");
return;
}
bool ok = auth_store_set_password(g_webui.auth, actor, newp);
bool ok = webui_store_set_password(g_webui.auth, actor, newp);
json_decref(req);
if (!ok) { http_text(fd, 500, "Internal Server Error", "could not update password"); return; }
json_t *reply = json_pack("{s:b}", "ok", 1);
@ -2896,7 +2896,7 @@ static void handle_api(int fd, const char *method, char *path,
json_string_value(json_object_get(req, "password"));
char role[16] = {0};
bool ok = g_webui.auth && user && password &&
auth_store_verify(g_webui.auth, user, password, role, sizeof role);
webui_store_verify(g_webui.auth, user, password, role, sizeof role);
if (!ok) {
json_t *json = json_pack("{s:b,s:s}", "ok", 0,
"error", "invalid credentials");
@ -3373,7 +3373,7 @@ naut_err naut_plugin_shutdown(void) {
pthread_cond_destroy(&g_webui.conn_cond);
pthread_mutex_destroy(&g_webui.conn_lock);
pthread_mutex_destroy(&g_webui.auth_lock);
auth_store_close(g_webui.auth);
webui_store_close(g_webui.auth);
g_webui.auth = NULL;
return NAUT_OK;
}

View file

@ -1,5 +1,5 @@
/* auth_store.c — SQLite + PBKDF2 implementation of the web-UI account store. */
#include "auth_store.h"
/* webui_store.c — SQLite + PBKDF2 implementation of the web-UI account store. */
#include "webui_store.h"
#include <pthread.h>
#include <stdlib.h>
@ -15,7 +15,7 @@
#define SALT_BYTES 16
#define HASH_BYTES 32
struct auth_store {
struct webui_store {
sqlite3 *db;
pthread_mutex_t lock;
};
@ -53,8 +53,8 @@ static bool valid_role(const char *role) {
return role && (strcmp(role, "admin") == 0 || strcmp(role, "user") == 0);
}
auth_store *auth_store_open(const char *path) {
auth_store *s = calloc(1, sizeof *s);
webui_store *webui_store_open(const char *path) {
webui_store *s = calloc(1, sizeof *s);
if (!s) return NULL;
if (pthread_mutex_init(&s->lock, NULL) != 0) { free(s); return NULL; }
if (sqlite3_open(path, &s->db) != SQLITE_OK) {
@ -77,13 +77,13 @@ auth_store *auth_store_open(const char *path) {
char *err = NULL;
if (sqlite3_exec(s->db, schema, NULL, NULL, &err) != SQLITE_OK) {
sqlite3_free(err);
auth_store_close(s);
webui_store_close(s);
return NULL;
}
return s;
}
void auth_store_close(auth_store *s) {
void webui_store_close(webui_store *s) {
if (!s) return;
if (s->db) sqlite3_close(s->db);
pthread_mutex_destroy(&s->lock);
@ -91,7 +91,7 @@ void auth_store_close(auth_store *s) {
}
/* Run a "SELECT count(*) ... " style query returning a single integer. */
static int count_query(auth_store *s, const char *sql) {
static int count_query(webui_store *s, const char *sql) {
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(s->db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int n = -1;
@ -100,7 +100,7 @@ static int count_query(auth_store *s, const char *sql) {
return n;
}
int auth_store_user_count(auth_store *s) {
int webui_store_user_count(webui_store *s) {
if (!s) return -1;
pthread_mutex_lock(&s->lock);
int n = count_query(s, "SELECT count(*) FROM users;");
@ -108,7 +108,7 @@ int auth_store_user_count(auth_store *s) {
return n;
}
int auth_store_admin_count(auth_store *s) {
int webui_store_admin_count(webui_store *s) {
if (!s) return -1;
pthread_mutex_lock(&s->lock);
int n = count_query(s, "SELECT count(*) FROM users WHERE role='admin';");
@ -116,7 +116,7 @@ int auth_store_admin_count(auth_store *s) {
return n;
}
bool auth_store_user_exists(auth_store *s, const char *username) {
bool webui_store_user_exists(webui_store *s, const char *username) {
if (!s || !username) return false;
pthread_mutex_lock(&s->lock);
sqlite3_stmt *st = NULL;
@ -131,7 +131,7 @@ bool auth_store_user_exists(auth_store *s, const char *username) {
return found;
}
bool auth_store_verify(auth_store *s, const char *username,
bool webui_store_verify(webui_store *s, const char *username,
const char *password, char *role_out, size_t role_sz) {
if (!s || !username || !password) return false;
pthread_mutex_lock(&s->lock);
@ -173,7 +173,7 @@ static bool make_hash(const char *password, char salt_hex[SALT_BYTES * 2 + 1],
return true;
}
bool auth_store_create_user(auth_store *s, const char *username,
bool webui_store_create_user(webui_store *s, const char *username,
const char *password, const char *role) {
if (!s || !username || !*username || !password || !*password) return false;
if (!valid_role(role)) role = "user";
@ -198,7 +198,7 @@ bool auth_store_create_user(auth_store *s, const char *username,
return ok;
}
bool auth_store_set_password(auth_store *s, const char *username,
bool webui_store_set_password(webui_store *s, const char *username,
const char *password) {
if (!s || !username || !password || !*password) return false;
char salt_hex[SALT_BYTES * 2 + 1], hash_hex[HASH_BYTES * 2 + 1];
@ -220,7 +220,7 @@ bool auth_store_set_password(auth_store *s, const char *username,
return ok;
}
bool auth_store_set_role(auth_store *s, const char *username, const char *role) {
bool webui_store_set_role(webui_store *s, const char *username, const char *role) {
if (!s || !username || !valid_role(role)) return false;
pthread_mutex_lock(&s->lock);
sqlite3_stmt *st = NULL;
@ -236,7 +236,7 @@ bool auth_store_set_role(auth_store *s, const char *username, const char *role)
return ok;
}
bool auth_store_delete_user(auth_store *s, const char *username) {
bool webui_store_delete_user(webui_store *s, const char *username) {
if (!s || !username) return false;
pthread_mutex_lock(&s->lock);
sqlite3_stmt *st = NULL;
@ -251,7 +251,7 @@ bool auth_store_delete_user(auth_store *s, const char *username) {
return ok;
}
bool auth_store_list_users(auth_store *s, json_t *out) {
bool webui_store_list_users(webui_store *s, json_t *out) {
if (!s || !json_is_array(out)) return false;
pthread_mutex_lock(&s->lock);
sqlite3_stmt *st = NULL;

View file

@ -0,0 +1,47 @@
/* webui_store.h — SQLite-backed persistence for all web-UI-owned state:
* accounts, the category/tag taxonomy, and RSS feeds/rules/indexers.
*
* Owned entirely by the webui plugin (the daemon persists none of this).
* Account passwords are PBKDF2-HMAC-SHA256 with a per-user random salt. All
* calls are thread-safe (the store serializes access to its SQLite handle). */
#ifndef NAUT_WEBUI_STORE_H
#define NAUT_WEBUI_STORE_H
#include <stdbool.h>
#include <stddef.h>
#include <jansson.h>
typedef struct webui_store webui_store;
/* Open (creating if needed) the account database at `path`. Returns NULL on
* failure. The schema is created/migrated on open. */
webui_store *webui_store_open(const char *path);
void webui_store_close(webui_store *s);
/* Number of accounts, or -1 on error. */
int webui_store_user_count(webui_store *s);
/* Number of admin accounts, or -1 on error. */
int webui_store_admin_count(webui_store *s);
bool webui_store_user_exists(webui_store *s, const char *username);
/* Verify a username/password pair (constant-time). On success, copies the
* account's role ("admin"/"user") into role_out. */
bool webui_store_verify(webui_store *s, const char *username,
const char *password, char *role_out, size_t role_sz);
/* Create an account. `role` must be "admin" or "user" (defaults to "user" if
* NULL/invalid). Returns false if the username already exists or on error. */
bool webui_store_create_user(webui_store *s, const char *username,
const char *password, const char *role);
bool webui_store_set_password(webui_store *s, const char *username,
const char *password);
/* Change an account's role ("admin"/"user"). */
bool webui_store_set_role(webui_store *s, const char *username, const char *role);
bool webui_store_delete_user(webui_store *s, const char *username);
/* Append {username, role, createdAt} objects (sorted by username) to the
* json array `out`. Returns false on error. */
bool webui_store_list_users(webui_store *s, json_t *out);
#endif /* NAUT_WEBUI_STORE_H */