webui: persist login sessions so they survive restarts
Sessions lived in an in-memory array, so every daemon restart wiped them and forced a re-login. Move them into the webui DB: - New sessions table storing a SHA-256 of the bearer token (never the raw token, so a DB read can't be replayed), the user, role, and an absolute expiry. - create/lookup/touch/delete + per-user delete + prune in webui_store. - Login persists the session; auth checks validate against the DB with a throttled sliding expiry (re-extended at most hourly to avoid a write per request); logout and admin reset/delete drop the rows. Expired rows are reaped lazily on lookup and pruned at startup. - TTL is configurable via NAUT_SESSION_TTL (default 7 days) and drives the cookie Max-Age. Removes the in-memory session array + auth_lock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
096535292d
commit
067b62c23a
3 changed files with 190 additions and 70 deletions
|
|
@ -29,20 +29,11 @@
|
||||||
#define DEFAULT_PORT 8080
|
#define DEFAULT_PORT 8080
|
||||||
#define READ_LIMIT (8u << 20)
|
#define READ_LIMIT (8u << 20)
|
||||||
#define SESSION_COOKIE "naut_session"
|
#define SESSION_COOKIE "naut_session"
|
||||||
#define SESSION_TTL_SECONDS (60 * 60 * 24 * 7)
|
#define SESSION_TTL_SECONDS (60 * 60 * 24 * 7) /* default; NAUT_SESSION_TTL */
|
||||||
#define MAX_SESSIONS 64
|
|
||||||
#define MAX_CONNECTIONS 128
|
#define MAX_CONNECTIONS 128
|
||||||
#define SPEED_SLOTS 256
|
#define SPEED_SLOTS 256
|
||||||
#define ETA_INFINITY 8640000 /* torrent-ui renders >= this as the infinity glyph */
|
#define ETA_INFINITY 8640000 /* torrent-ui renders >= this as the infinity glyph */
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char token[96];
|
|
||||||
char user[64];
|
|
||||||
char role[16];
|
|
||||||
time_t expires;
|
|
||||||
bool used;
|
|
||||||
} webui_session;
|
|
||||||
|
|
||||||
/* Single-writer (sampler thread) running estimate of a torrent's download
|
/* Single-writer (sampler thread) running estimate of a torrent's download
|
||||||
* rate, derived from successive byte counts. */
|
* rate, derived from successive byte counts. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -70,8 +61,6 @@ typedef struct {
|
||||||
bool sampler_started;
|
bool sampler_started;
|
||||||
pthread_t sampler;
|
pthread_t sampler;
|
||||||
|
|
||||||
pthread_mutex_t auth_lock;
|
|
||||||
|
|
||||||
pthread_mutex_t conn_lock;
|
pthread_mutex_t conn_lock;
|
||||||
pthread_cond_t conn_cond;
|
pthread_cond_t conn_cond;
|
||||||
size_t active_connections;
|
size_t active_connections;
|
||||||
|
|
@ -103,8 +92,6 @@ typedef struct {
|
||||||
bool rss_thread_started;
|
bool rss_thread_started;
|
||||||
pthread_cond_t rss_cond; /* wakes the poller for an immediate refresh */
|
pthread_cond_t rss_cond; /* wakes the poller for an immediate refresh */
|
||||||
bool rss_wake; /* set with rss_cond to force an early re-poll */
|
bool rss_wake; /* set with rss_cond to force an early re-poll */
|
||||||
|
|
||||||
webui_session sessions[MAX_SESSIONS];
|
|
||||||
} webui_state;
|
} webui_state;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -381,52 +368,52 @@ static bool cookie_token(const char *headers, const char *end,
|
||||||
/* Look up the session for this request. On a live session, refreshes its TTL
|
/* Look up the session for this request. On a live session, refreshes its TTL
|
||||||
* and (optionally) copies the account's username and role. Returns true if a
|
* and (optionally) copies the account's username and role. Returns true if a
|
||||||
* valid session was found. */
|
* valid session was found. */
|
||||||
|
/* Session lifetime in seconds (sliding). Override with NAUT_SESSION_TTL. */
|
||||||
|
static long session_ttl(void) {
|
||||||
|
const char *env = getenv("NAUT_SESSION_TTL");
|
||||||
|
if (env && *env) {
|
||||||
|
char *e = NULL;
|
||||||
|
long v = strtol(env, &e, 10);
|
||||||
|
if (e && e != env && !*e && v > 0) return v;
|
||||||
|
}
|
||||||
|
return SESSION_TTL_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Don't rewrite the session row on every request; only re-extend the sliding
|
||||||
|
* expiry once it has advanced by more than this. */
|
||||||
|
#define SESSION_REFRESH_THRESHOLD 3600
|
||||||
|
|
||||||
static bool current_identity(const char *headers, const char *end,
|
static bool current_identity(const char *headers, const char *end,
|
||||||
char *user, size_t user_sz,
|
char *user, size_t user_sz,
|
||||||
char *role, size_t role_sz) {
|
char *role, size_t role_sz) {
|
||||||
char token[96];
|
char token[96];
|
||||||
if (!cookie_token(headers, end, token, sizeof token)) return false;
|
if (!cookie_token(headers, end, token, sizeof token)) return false;
|
||||||
bool ok = false;
|
if (!g_webui.store) return false;
|
||||||
time_t now = time(NULL);
|
char u[64] = {0}, r[16] = {0};
|
||||||
pthread_mutex_lock(&g_webui.auth_lock);
|
long expires = 0;
|
||||||
for (size_t i = 0; i < MAX_SESSIONS; i++) {
|
if (!webui_store_session_lookup(g_webui.store, token, u, sizeof u,
|
||||||
webui_session *session = &g_webui.sessions[i];
|
r, sizeof r, &expires))
|
||||||
if (!session->used || strcmp(session->token, token) != 0) continue;
|
return false;
|
||||||
if (session->expires < now) {
|
long now = (long)time(NULL);
|
||||||
session->used = false;
|
if (expires <= now) { /* expired: clean it up */
|
||||||
break;
|
webui_store_session_delete(g_webui.store, token);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
session->expires = now + SESSION_TTL_SECONDS;
|
long fresh = now + session_ttl(); /* sliding window, throttled */
|
||||||
if (user) snprintf(user, user_sz, "%s", session->user);
|
if (fresh - expires > SESSION_REFRESH_THRESHOLD)
|
||||||
if (role) snprintf(role, role_sz, "%s", session->role);
|
webui_store_session_touch(g_webui.store, token, fresh);
|
||||||
ok = true;
|
if (user) snprintf(user, user_sz, "%s", u);
|
||||||
break;
|
if (role) snprintf(role, role_sz, "%s", r);
|
||||||
}
|
return true;
|
||||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
|
||||||
return ok;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool create_session(const char *user, const char *role,
|
static bool create_session(const char *user, const char *role,
|
||||||
char *out, size_t out_size) {
|
char *out, size_t out_size) {
|
||||||
char token[96];
|
char token[96];
|
||||||
if (!random_hex(token, sizeof token, 24)) return false;
|
if (!g_webui.store || !random_hex(token, sizeof token, 24)) return false;
|
||||||
time_t now = time(NULL);
|
long expires = (long)time(NULL) + session_ttl();
|
||||||
time_t expires = now + SESSION_TTL_SECONDS;
|
if (!webui_store_session_create(g_webui.store, token, user, role, expires))
|
||||||
pthread_mutex_lock(&g_webui.auth_lock);
|
return false;
|
||||||
webui_session *slot = NULL;
|
|
||||||
for (size_t i = 0; i < MAX_SESSIONS; i++) {
|
|
||||||
webui_session *s = &g_webui.sessions[i];
|
|
||||||
if (!s->used || s->expires < now) { slot = s; break; }
|
|
||||||
/* Otherwise track the session that expires soonest, so a full table
|
|
||||||
* evicts the oldest rather than always clobbering slot 0. */
|
|
||||||
if (!slot || s->expires < slot->expires) slot = s;
|
|
||||||
}
|
|
||||||
snprintf(slot->token, sizeof slot->token, "%s", token);
|
|
||||||
snprintf(slot->user, sizeof slot->user, "%s", user ? user : "");
|
|
||||||
snprintf(slot->role, sizeof slot->role, "%s", role ? role : "user");
|
|
||||||
slot->expires = expires;
|
|
||||||
slot->used = true;
|
|
||||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
|
||||||
snprintf(out, out_size, "%s", token);
|
snprintf(out, out_size, "%s", token);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -434,23 +421,13 @@ static bool create_session(const char *user, const char *role,
|
||||||
/* Invalidate every session belonging to `user` (after delete / password reset
|
/* Invalidate every session belonging to `user` (after delete / password reset
|
||||||
* by an admin). */
|
* by an admin). */
|
||||||
static void drop_user_sessions(const char *user) {
|
static void drop_user_sessions(const char *user) {
|
||||||
pthread_mutex_lock(&g_webui.auth_lock);
|
if (g_webui.store) webui_store_sessions_delete_user(g_webui.store, user);
|
||||||
for (size_t i = 0; i < MAX_SESSIONS; i++)
|
|
||||||
if (g_webui.sessions[i].used &&
|
|
||||||
strcmp(g_webui.sessions[i].user, user) == 0)
|
|
||||||
g_webui.sessions[i].used = false;
|
|
||||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_session(const char *headers, const char *end) {
|
static void clear_session(const char *headers, const char *end) {
|
||||||
char token[96];
|
char token[96];
|
||||||
if (!cookie_token(headers, end, token, sizeof token)) return;
|
if (g_webui.store && cookie_token(headers, end, token, sizeof token))
|
||||||
pthread_mutex_lock(&g_webui.auth_lock);
|
webui_store_session_delete(g_webui.store, token);
|
||||||
for (size_t i = 0; i < MAX_SESSIONS; i++)
|
|
||||||
if (g_webui.sessions[i].used &&
|
|
||||||
strcmp(g_webui.sessions[i].token, token) == 0)
|
|
||||||
g_webui.sessions[i].used = false;
|
|
||||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool bad_static_path(const char *path) {
|
static bool bad_static_path(const char *path) {
|
||||||
|
|
@ -2764,8 +2741,8 @@ static void handle_api(int fd, const char *method, char *path,
|
||||||
char cookie[256];
|
char cookie[256];
|
||||||
snprintf(cookie, sizeof cookie,
|
snprintf(cookie, sizeof cookie,
|
||||||
"Set-Cookie: %s=%s; Path=/; HttpOnly; SameSite=Lax; "
|
"Set-Cookie: %s=%s; Path=/; HttpOnly; SameSite=Lax; "
|
||||||
"Max-Age=%d\r\n",
|
"Max-Age=%ld\r\n",
|
||||||
SESSION_COOKIE, token, SESSION_TTL_SECONDS);
|
SESSION_COOKIE, token, session_ttl());
|
||||||
json_t *json = json_pack("{s:b,s:s,s:s}", "ok", 1,
|
json_t *json = json_pack("{s:b,s:s,s:s}", "ok", 1,
|
||||||
"user", user, "role", role);
|
"user", user, "role", role);
|
||||||
http_json_extra(fd, 200, json, cookie);
|
http_json_extra(fd, 200, json, cookie);
|
||||||
|
|
@ -3110,10 +3087,8 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
||||||
memset(&g_webui, 0, sizeof g_webui);
|
memset(&g_webui, 0, sizeof g_webui);
|
||||||
g_webui.listener = -1;
|
g_webui.listener = -1;
|
||||||
g_webui.host = *host;
|
g_webui.host = *host;
|
||||||
if (pthread_mutex_init(&g_webui.auth_lock, NULL) != 0)
|
|
||||||
return NAUT_ERR_NOMEM;
|
|
||||||
if (pthread_mutex_init(&g_webui.conn_lock, NULL) != 0)
|
if (pthread_mutex_init(&g_webui.conn_lock, NULL) != 0)
|
||||||
goto fail_conn_lock;
|
return NAUT_ERR_NOMEM;
|
||||||
if (pthread_cond_init(&g_webui.conn_cond, NULL) != 0)
|
if (pthread_cond_init(&g_webui.conn_cond, NULL) != 0)
|
||||||
goto fail_conn_cond;
|
goto fail_conn_cond;
|
||||||
if (pthread_mutex_init(&g_webui.snap_lock, NULL) != 0)
|
if (pthread_mutex_init(&g_webui.snap_lock, NULL) != 0)
|
||||||
|
|
@ -3132,6 +3107,8 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
||||||
pthread_mutex_init(&g_webui.rss_lock, NULL);
|
pthread_mutex_init(&g_webui.rss_lock, NULL);
|
||||||
pthread_cond_init(&g_webui.rss_cond, NULL);
|
pthread_cond_init(&g_webui.rss_cond, NULL);
|
||||||
init_auth();
|
init_auth();
|
||||||
|
if (g_webui.store) /* drop sessions that expired while we were down */
|
||||||
|
webui_store_sessions_prune(g_webui.store, (long)time(NULL));
|
||||||
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
||||||
"webui");
|
"webui");
|
||||||
if (error != NAUT_OK) goto fail_store;
|
if (error != NAUT_OK) goto fail_store;
|
||||||
|
|
@ -3158,8 +3135,6 @@ fail_snap_lock:
|
||||||
pthread_cond_destroy(&g_webui.conn_cond);
|
pthread_cond_destroy(&g_webui.conn_cond);
|
||||||
fail_conn_cond:
|
fail_conn_cond:
|
||||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
pthread_mutex_destroy(&g_webui.conn_lock);
|
||||||
fail_conn_lock:
|
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3212,7 +3187,6 @@ naut_err naut_plugin_shutdown(void) {
|
||||||
pthread_mutex_destroy(&g_webui.snap_lock);
|
pthread_mutex_destroy(&g_webui.snap_lock);
|
||||||
pthread_cond_destroy(&g_webui.conn_cond);
|
pthread_cond_destroy(&g_webui.conn_cond);
|
||||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
pthread_mutex_destroy(&g_webui.conn_lock);
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
|
||||||
webui_store_close(g_webui.store);
|
webui_store_close(g_webui.store);
|
||||||
g_webui.store = NULL;
|
g_webui.store = NULL;
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,13 @@ webui_store *webui_store_open(const char *path) {
|
||||||
" pw_iters INTEGER NOT NULL,"
|
" pw_iters INTEGER NOT NULL,"
|
||||||
" role TEXT NOT NULL DEFAULT 'user',"
|
" role TEXT NOT NULL DEFAULT 'user',"
|
||||||
" created_at INTEGER NOT NULL);"
|
" created_at INTEGER NOT NULL);"
|
||||||
|
"CREATE TABLE IF NOT EXISTS sessions ("
|
||||||
|
" token_hash TEXT PRIMARY KEY,"
|
||||||
|
" username TEXT NOT NULL,"
|
||||||
|
" role TEXT NOT NULL DEFAULT 'user',"
|
||||||
|
" expires INTEGER NOT NULL);"
|
||||||
|
"CREATE INDEX IF NOT EXISTS sessions_user ON sessions(username);"
|
||||||
|
"CREATE INDEX IF NOT EXISTS sessions_expires ON sessions(expires);"
|
||||||
"CREATE TABLE IF NOT EXISTS categories ("
|
"CREATE TABLE IF NOT EXISTS categories ("
|
||||||
" name TEXT PRIMARY KEY,"
|
" name TEXT PRIMARY KEY,"
|
||||||
" save_path TEXT NOT NULL DEFAULT '');"
|
" save_path TEXT NOT NULL DEFAULT '');"
|
||||||
|
|
@ -323,6 +330,131 @@ bool webui_store_list_users(webui_store *s, json_t *out) {
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- sessions ------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* SHA-256 of a bearer token, hex-encoded. We persist only this, never the raw
|
||||||
|
* token, so a DB leak can't be replayed as a live cookie. */
|
||||||
|
static void sha256_hex(const char *token, char out[65]) {
|
||||||
|
unsigned char d[32];
|
||||||
|
unsigned int dl = 0;
|
||||||
|
EVP_Digest(token, strlen(token), d, &dl, EVP_sha256(), NULL);
|
||||||
|
to_hex(d, 32, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool webui_store_session_create(webui_store *s, const char *token,
|
||||||
|
const char *user, const char *role,
|
||||||
|
long expires) {
|
||||||
|
if (!s || !token || !*token || !user || !*user) return false;
|
||||||
|
char th[65];
|
||||||
|
sha256_hex(token, th);
|
||||||
|
pthread_mutex_lock(&s->lock);
|
||||||
|
sqlite3_stmt *st = NULL;
|
||||||
|
bool ok = false;
|
||||||
|
if (sqlite3_prepare_v2(s->db,
|
||||||
|
"INSERT OR REPLACE INTO sessions (token_hash,username,role,expires)"
|
||||||
|
" VALUES (?,?,?,?);", -1, &st, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(st, 1, th, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(st, 2, user, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(st, 3, role && *role ? role : "user", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_int64(st, 4, (sqlite3_int64)expires);
|
||||||
|
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
pthread_mutex_unlock(&s->lock);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool webui_store_session_lookup(webui_store *s, const char *token,
|
||||||
|
char *user, size_t user_sz,
|
||||||
|
char *role, size_t role_sz, long *expires_out) {
|
||||||
|
if (!s || !token || !*token) return false;
|
||||||
|
char th[65];
|
||||||
|
sha256_hex(token, th);
|
||||||
|
pthread_mutex_lock(&s->lock);
|
||||||
|
sqlite3_stmt *st = NULL;
|
||||||
|
bool ok = false;
|
||||||
|
if (sqlite3_prepare_v2(s->db,
|
||||||
|
"SELECT username, role, expires FROM sessions WHERE token_hash=?;",
|
||||||
|
-1, &st, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(st, 1, th, -1, SQLITE_STATIC);
|
||||||
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
const char *u = (const char *)sqlite3_column_text(st, 0);
|
||||||
|
const char *r = (const char *)sqlite3_column_text(st, 1);
|
||||||
|
if (user) snprintf(user, user_sz, "%s", u ? u : "");
|
||||||
|
if (role) snprintf(role, role_sz, "%s", r ? r : "user");
|
||||||
|
if (expires_out) *expires_out = (long)sqlite3_column_int64(st, 2);
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
pthread_mutex_unlock(&s->lock);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool webui_store_session_touch(webui_store *s, const char *token, long expires) {
|
||||||
|
if (!s || !token) return false;
|
||||||
|
char th[65];
|
||||||
|
sha256_hex(token, th);
|
||||||
|
pthread_mutex_lock(&s->lock);
|
||||||
|
sqlite3_stmt *st = NULL;
|
||||||
|
bool ok = false;
|
||||||
|
if (sqlite3_prepare_v2(s->db,
|
||||||
|
"UPDATE sessions SET expires=? WHERE token_hash=?;",
|
||||||
|
-1, &st, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)expires);
|
||||||
|
sqlite3_bind_text(st, 2, th, -1, SQLITE_STATIC);
|
||||||
|
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
pthread_mutex_unlock(&s->lock);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool webui_store_session_delete(webui_store *s, const char *token) {
|
||||||
|
if (!s || !token) return false;
|
||||||
|
char th[65];
|
||||||
|
sha256_hex(token, th);
|
||||||
|
pthread_mutex_lock(&s->lock);
|
||||||
|
sqlite3_stmt *st = NULL;
|
||||||
|
bool ok = false;
|
||||||
|
if (sqlite3_prepare_v2(s->db, "DELETE FROM sessions WHERE token_hash=?;",
|
||||||
|
-1, &st, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(st, 1, th, -1, SQLITE_STATIC);
|
||||||
|
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
pthread_mutex_unlock(&s->lock);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool webui_store_sessions_delete_user(webui_store *s, const char *user) {
|
||||||
|
if (!s || !user) return false;
|
||||||
|
pthread_mutex_lock(&s->lock);
|
||||||
|
sqlite3_stmt *st = NULL;
|
||||||
|
bool ok = false;
|
||||||
|
if (sqlite3_prepare_v2(s->db, "DELETE FROM sessions WHERE username=?;",
|
||||||
|
-1, &st, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(st, 1, user, -1, SQLITE_STATIC);
|
||||||
|
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
pthread_mutex_unlock(&s->lock);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void webui_store_sessions_prune(webui_store *s, long now) {
|
||||||
|
if (!s) return;
|
||||||
|
pthread_mutex_lock(&s->lock);
|
||||||
|
sqlite3_stmt *st = NULL;
|
||||||
|
if (sqlite3_prepare_v2(s->db, "DELETE FROM sessions WHERE expires<=?;",
|
||||||
|
-1, &st, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)now);
|
||||||
|
sqlite3_step(st);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
pthread_mutex_unlock(&s->lock);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- taxonomy ------------------------------------------------------------- */
|
/* --- taxonomy ------------------------------------------------------------- */
|
||||||
|
|
||||||
/* Replace one table's contents from a json array, inside a transaction. The
|
/* Replace one table's contents from a json array, inside a transaction. The
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,20 @@ bool webui_store_delete_user(webui_store *s, const char *username);
|
||||||
* json array `out`. Returns false on error. */
|
* json array `out`. Returns false on error. */
|
||||||
bool webui_store_list_users(webui_store *s, json_t *out);
|
bool webui_store_list_users(webui_store *s, json_t *out);
|
||||||
|
|
||||||
|
/* --- sessions (persisted so logins survive daemon restarts) --------------- *
|
||||||
|
* Only a SHA-256 of the bearer token is stored, so a DB read can't be replayed
|
||||||
|
* as a live cookie. `expires` is an absolute unix time. */
|
||||||
|
bool webui_store_session_create(webui_store *s, const char *token,
|
||||||
|
const char *user, const char *role, long expires);
|
||||||
|
/* On a live (unexpired) session, copies username/role and the stored expiry. */
|
||||||
|
bool webui_store_session_lookup(webui_store *s, const char *token,
|
||||||
|
char *user, size_t user_sz,
|
||||||
|
char *role, size_t role_sz, long *expires_out);
|
||||||
|
bool webui_store_session_touch(webui_store *s, const char *token, long expires);
|
||||||
|
bool webui_store_session_delete(webui_store *s, const char *token);
|
||||||
|
bool webui_store_sessions_delete_user(webui_store *s, const char *user);
|
||||||
|
void webui_store_sessions_prune(webui_store *s, long now);
|
||||||
|
|
||||||
/* --- category / tag taxonomy (web-UI organization, owned here) ------------- *
|
/* --- category / tag taxonomy (web-UI organization, owned here) ------------- *
|
||||||
* The save_* calls replace the whole list atomically; the load_* calls append
|
* The save_* calls replace the whole list atomically; the load_* calls append
|
||||||
* to the (array) `out`. Categories are {name, savePath}; tags are strings. */
|
* to the (array) `out`. Categories are {name, savePath}; tags are strings. */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue