Naut/plugins/webui/webui_store.h
ookami125 067b62c23a 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>
2026-06-24 21:00:36 -04:00

107 lines
5.6 KiB
C

/* 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);
/* --- 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) ------------- *
* The save_* calls replace the whole list atomically; the load_* calls append
* to the (array) `out`. Categories are {name, savePath}; tags are strings. */
bool webui_store_save_categories(webui_store *s, json_t *cats);
bool webui_store_load_categories(webui_store *s, json_t *out);
bool webui_store_save_tags(webui_store *s, json_t *tags);
bool webui_store_load_tags(webui_store *s, json_t *out);
/* --- RSS: feeds, articles, auto-download rules, Torznab indexers ----------- *
* Fully relational: articles live in their own table (deduped by feed+key,
* indexed), and a rule's feed scope lives in a rule_feeds join table. The web
* layer operates on rows, not whole-list blobs. */
/* Feeds. upsert preserves an existing feed's lastUpdate (only the url changes);
* remove also drops the feed's articles. feed_list appends
* {name,url,lastUpdate,articles:[...]} (newest article first). feed_targets
* appends lightweight {name,url} objects for the poller. */
bool webui_store_feed_upsert(webui_store *s, const char *name, const char *url);
bool webui_store_feed_remove(webui_store *s, const char *name);
bool webui_store_feed_set_updated(webui_store *s, const char *name, long ts);
bool webui_store_feed_list(webui_store *s, json_t *out);
bool webui_store_feed_targets(webui_store *s, json_t *out);
bool webui_store_feed_exists(webui_store *s, const char *name);
/* Articles. add inserts unless (feed,key) already exists: returns 1 if newly
* inserted, 0 if a duplicate, -1 on error. trim keeps the newest `keep` for a
* feed. mark_grabbed flags every article with this key. ungrabbed appends
* {feed,key,title,magnet,torrentUrl} for not-yet-grabbed articles. */
int webui_store_article_add(webui_store *s, const char *feed, json_t *article);
bool webui_store_article_trim(webui_store *s, const char *feed, int keep);
bool webui_store_article_mark_grabbed(webui_store *s, const char *key);
bool webui_store_articles_ungrabbed(webui_store *s, json_t *out);
/* Rules. upsert replaces the rule row and its feed scope; list/get assemble the
* rule with its affectedFeeds array. */
bool webui_store_rule_upsert(webui_store *s, json_t *rule);
bool webui_store_rule_remove(webui_store *s, const char *name);
bool webui_store_rule_list(webui_store *s, json_t *out);
json_t *webui_store_rule_get(webui_store *s, const char *name);
bool webui_store_rule_set_match(webui_store *s, const char *name, long ts);
/* Torznab indexers. */
bool webui_store_indexer_upsert(webui_store *s, json_t *indexer);
bool webui_store_indexer_remove(webui_store *s, const char *name);
bool webui_store_indexer_list(webui_store *s, json_t *out);
#endif /* NAUT_WEBUI_STORE_H */