diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2026-02-21 11:14:42 +0100 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2026-02-21 11:14:42 +0100 |
| commit | 1b19afaa339f2f68520d61b634600ddeb04effb7 (patch) | |
| tree | 232ec9fdc03a8955a8c847b88a688306689d2633 /src/web.c | |
| parent | 0b6e8026a6b530c7d88607bfdf79b9692987abaa (diff) | |
Split up the rendering from the logic
Diffstat (limited to 'src/web.c')
| -rw-r--r-- | src/web.c | 1312 |
1 files changed, 50 insertions, 1262 deletions
@@ -1,21 +1,15 @@ #include "web.h" +#include "app.h" +#include "html.h" #include "mytime.h" #include <assert.h> -#include <errno.h> -#include <linux/limits.h> -#include <libgen.h> -#include <math.h> -#include <sys/socket.h> #include <string.h> -#include <sys/stat.h> #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <unistd.h> -#include <string.h> #include <microhttpd.h> -#include <sqlite3.h> #include <cjson/cJSON.h> #include <fcntl.h> #include <crypt.h> @@ -23,11 +17,6 @@ #define PAGE "<html><head><title>libmicrohttpd demo</title>"\ "</head><body>libmicrohttpd demo</body></html>" -static void sqliteError(void *pArg, int iErrCode, const char *zMsg) { - // @CLEANUP: We should stuff these somewhere, but for now just print it - fprintf(stderr, "(%d) %s\n", iErrCode, zMsg); -} - static int generate_secret(char *out) { unsigned char bytes[16]; int fd = open("/dev/urandom", O_RDONLY); @@ -64,1243 +53,6 @@ static int is_authenticated(struct App *app, struct MHD_Connection *connection) return strcmp(cookie, expected) == 0; } -// https://stackoverflow.com/questions/2336242/recursive-mkdir-system-call-on-unix -static void _mkdir(const char *dir) { - char tmp[PATH_MAX]; - char *p = NULL; - size_t len; - int rc; - - snprintf(tmp, sizeof(tmp),"%s",dir); - len = strlen(tmp); - if (tmp[len - 1] == '/') - tmp[len - 1] = 0; - for (p = tmp + 1; *p; p++) - if (*p == '/') { - *p = 0; - rc = mkdir(tmp, S_IRWXU); - if(rc != 0 && errno != EEXIST) { - abort(); - } - *p = '/'; - } - rc = mkdir(tmp, S_IRWXU); - if(rc != 0 && errno != EEXIST) { - abort(); - } - -} - -void prepare_database(struct App *app) { - // @CLEANUP: Move this to main? doesn't belong here at least - sqlite3_config(SQLITE_CONFIG_LOG, sqliteError, NULL); - sqlite3_config(SQLITE_CONFIG_URI, 1); - - int err; - - printf("Running migrations against %s\n", app->dbname); - - if(memcmp("file::memory", app->dbname, 12) != 0) { - char *full = strdup(app->dbname); - char *dbdir = dirname(full); - _mkdir(dbdir); - free(full); - } - - sqlite3 *conn; - if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) { - fprintf(stderr, "(%d) %s\n", err, sqlite3_errmsg(conn)); - abort(); - } - sqlite3_db_config(conn, SQLITE_DBCONFIG_ENABLE_FKEY, 1, NULL); - - if(sqlite3_exec(conn, "CREATE TABLE IF NOT EXISTS migration (id INTEGER PRIMARY KEY NOT NULL)", NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT MAX(id) FROM migration", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - - uint64_t maxVersion = 0; - while((err = sqlite3_step(stmt)) == SQLITE_ROW) { - maxVersion = sqlite3_column_int64(stmt, 0); - } - if(err != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(maxVersion < 1) { - char *prog = - "BEGIN TRANSACTION;" - "CREATE TABLE backups (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "client TEXT NOT NULL, " - "status TEXT NOT NULL, " - "started TEXT NOT NULL, " - "completed TEXT " - ") STRICT"; - if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - - if(sqlite3_bind_int64(stmt, 1, 1) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - } - - if(maxVersion < 2) { - char *prog = - "BEGIN TRANSACTION;" - "CREATE TABLE clients (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "name TEXT NOT NULL " - ") STRICT;" - "INSERT INTO clients(name) " - "SELECT b.client " - "FROM backups b " - "GROUP BY b.client;" - "CREATE TABLE backups_next (" - "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " - "client INTEGER NOT NULL, " - "status TEXT NOT NULL, " - "started TEXT NOT NULL, " - "completed TEXT ," - "FOREIGN KEY(client) REFERENCES clients(id)" - ") STRICT;" - "INSERT INTO backups_next(id, client, status, started, completed) " - "SELECT b.id, c.id, b.status, b.started, b.completed " - "FROM backups b " - "JOIN clients c ON b.client = c.name;" - "DROP TABLE backups;" - "ALTER TABLE backups_next RENAME TO backups;"; - if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - - if(sqlite3_bind_int64(stmt, 1, 2) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - } - - if(maxVersion < 3) { - char *prog = - "BEGIN TRANSACTION;" - "CREATE UNIQUE INDEX clients_name ON clients(name)"; - if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - - if(sqlite3_bind_int64(stmt, 1, 3) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - } - - if(maxVersion < 4) { - char *prog = - "BEGIN TRANSACTION;" - "ALTER TABLE clients ADD COLUMN secret TEXT NOT NULL DEFAULT ''"; - if(sqlite3_exec(conn, prog, NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "INSERT INTO migration (id) VALUES (?1)", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - - if(sqlite3_bind_int64(stmt, 1, 4) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_exec(conn, "COMMIT TRANSACTION", NULL, NULL, NULL) != SQLITE_OK) { - abort(); - } - } -} - -enum ReportEvent { - REV_BEGIN, - REV_FINISH, -}; - -// @CLEANUP: This should live somewhere else, but I don't have a spot for it yet. -// Returns 0 on success, -1 on error, -2 on secret mismatch -int submit_report(struct App *app, char *client, char *secret, enum ReportEvent event) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - struct timespec now = get_current_time(); - - int64_t clientId; - { - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT id, secret FROM clients WHERE name = ?1", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_text(stmt, 1, client, -1, SQLITE_STATIC) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_ROW) { - abort(); - } - - if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) { - abort(); - } - clientId = sqlite3_column_int64(stmt, 0); - - const char *stored_secret = (const char *)sqlite3_column_text(stmt, 1); - if(stored_secret == NULL || strcmp(stored_secret, secret) != 0) { - sqlite3_finalize(stmt); - sqlite3_close(conn); - return -2; - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - } - - switch(event) { - case REV_BEGIN: { - sqlite3_stmt *stmt; - - // Mark any existing RUNNING backup as INTERRUPTED - if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = 'INTERRUPTED', completed = datetime(?1, 'unixepoch', 'subsec') WHERE client = ?2 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_double(stmt, 1, TIME_AS_FLOAT(now)) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 2, clientId) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - stmt = NULL; - - // Insert the new backup - if(sqlite3_prepare_v2(conn, "INSERT INTO backups (client, status, started) VALUES (?1, ?2, datetime(?3, 'unixepoch', 'subsec'))", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_text(stmt, 2, "RUNNING", -1, SQLITE_STATIC) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_double(stmt, 3, TIME_AS_FLOAT(now)) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - break; - } - case REV_FINISH: { - uint64_t backupId; - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT id FROM backups WHERE client = ?1 AND status = 'RUNNING'", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, clientId) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_ROW) { - abort(); - } - - if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) { - abort(); - } - backupId = sqlite3_column_int64(stmt, 0); - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - stmt = NULL; - - if(sqlite3_prepare_v2(conn, "UPDATE backups SET status = ?1, completed = datetime(?2, 'unixepoch', 'subsec') WHERE id = ?3", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_text(stmt, 1, "FINISHED", -1, SQLITE_STATIC) != SQLITE_OK) { - abort(); - } - - if(sqlite3_bind_double(stmt, 2, TIME_AS_FLOAT(now)) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 3, backupId) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - break; - } - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int create_client(struct Request *req, char *name, struct CreateClientResult *result) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - if(generate_secret(result->secret) != 0) { - abort(); - } - - { - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "INSERT INTO clients (name, secret) VALUES (?1, ?2)", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_text(stmt, 2, result->secret, -1, SQLITE_STATIC) != SQLITE_OK) { - abort(); - } - - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - - result->id = sqlite3_last_insert_rowid(conn); - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int delete_client(struct Request *req, int64_t client_id) { - sqlite3 *conn; - if(sqlite3_open(req->server->app->dbname, &conn) != SQLITE_OK) { - abort(); - } - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "DELETE FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { - abort(); - } - if(sqlite3_step(stmt) != SQLITE_DONE) { - abort(); - } - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int list_clients(struct Request *req, struct ListClientResult *result) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - { - sqlite3_stmt *stmt; - const char *sql = - "SELECT c.id, c.name, c.secret, unixepoch(b_success.completed,'subsec'), b_last.status " - "FROM clients c " - "LEFT JOIN backups b_success ON b_success.client = c.id " - " AND b_success.id = (SELECT id FROM backups WHERE client = c.id AND status = 'FINISHED' ORDER BY id DESC LIMIT 1) " - "LEFT JOIN backups b_last ON b_last.client = c.id " - " AND b_last.id = (SELECT id FROM backups WHERE client = c.id ORDER BY id DESC LIMIT 1) " - "WHERE c.id > ?1 ORDER BY c.id ASC LIMIT ?2"; - if(sqlite3_prepare_v2(conn, sql, -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, -1) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 2, 16) != SQLITE_OK) { - abort(); - } - - int ret; - int index = 0; - while((ret = sqlite3_step(stmt)) == SQLITE_ROW) { - assert(index < 16); - - if(sqlite3_column_type(stmt, 0) == SQLITE_NULL) { - abort(); - } - - int64_t clientId = sqlite3_column_int64(stmt, 0); - const uint8_t *name = sqlite3_column_text(stmt, 1); - const uint8_t *secret = sqlite3_column_text(stmt, 2); - const double last_backup = sqlite3_column_double(stmt, 3); - const uint8_t *backup_status = sqlite3_column_text(stmt, 4); - - result->clients[index].id = clientId; - strncpy(result->clients[index].name, (char*)name, CLIENT_NAME_MAX); - strncpy(result->clients[index].secret, secret ? (char*)secret : "", CLIENT_SECRET_LEN); - result->clients[index].last_backup = time_from_double(last_backup); - - if (backup_status == NULL) { - strncpy(result->clients[index].status, "MISSING", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } else if (strcmp((char*)backup_status, "RUNNING") == 0) { - strncpy(result->clients[index].status, "RUNNING", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } else if (strcmp((char*)backup_status, "INTERRUPTED") == 0) { - strncpy(result->clients[index].status, "INTERRUPTED", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } else { - strncpy(result->clients[index].status, "OK", BACKUP_STATUS_MAX); - result->clients[index].is_stale = 0; - } - - index++; - } - if(ret != SQLITE_DONE) { - abort(); - } - - result->clients_len = index; - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int get_client(struct Request *req, int64_t client_id, struct GetClientOnlyResult *result) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - result->found = 0; - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT id, name, secret FROM clients WHERE id = ?1", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { - abort(); - } - - int ret = sqlite3_step(stmt); - if(ret == SQLITE_ROW) { - result->found = 1; - result->id = sqlite3_column_int64(stmt, 0); - const uint8_t *name = sqlite3_column_text(stmt, 1); - const uint8_t *secret = sqlite3_column_text(stmt, 2); - strncpy(result->name, (char*)name, CLIENT_NAME_MAX); - strncpy(result->secret, (char*)secret, CLIENT_SECRET_LEN); - } else if(ret != SQLITE_DONE) { - abort(); - } - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -int get_client_backups(struct Request *req, int64_t client_id, struct GetBackupsResult *result) { - sqlite3 *conn; - int err; - if((err = sqlite3_open(req->server->app->dbname, &conn)) != SQLITE_OK) { - abort(); - } - - uint16_t limit = result->backups_len; - - sqlite3_stmt *stmt; - if(sqlite3_prepare_v2(conn, "SELECT id, status, unixepoch(started, 'subsec'), unixepoch(completed, 'subsec') FROM backups WHERE client = ?1 ORDER BY started DESC LIMIT ?2", -1, &stmt, NULL) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int64(stmt, 1, client_id) != SQLITE_OK) { - abort(); - } - if(sqlite3_bind_int(stmt, 2, limit) != SQLITE_OK) { - abort(); - } - - int ret; - int index = 0; - while((ret = sqlite3_step(stmt)) == SQLITE_ROW) { - if(index >= limit) break; - - result->backups[index].id = sqlite3_column_int64(stmt, 0); - - const uint8_t *status = sqlite3_column_text(stmt, 1); - const double started = sqlite3_column_double(stmt, 2); - const double completed = sqlite3_column_double(stmt, 3); - - strncpy(result->backups[index].status, status ? (char*)status : "", BACKUP_STATUS_MAX); - result->backups[index].started = time_from_double(started); - result->backups[index].completed = time_from_double(completed); - - index++; - } - if(ret != SQLITE_DONE && ret != SQLITE_ROW) { - abort(); - } - - result->backups_len = index; - - if(sqlite3_finalize(stmt) != SQLITE_OK) { - abort(); - } - - if(sqlite3_close(conn) != SQLITE_OK) { - abort(); - } - return 0; -} - -static void emit_stylesheet(FILE *f) { - fprintf(f, - "<style>\n" - "*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n" - ":root {\n" - " --bg: #0e0f11;\n" - " --sur: #16181c;\n" - " --bor: #2a2d35;\n" - " --dim: #4a4f5e;\n" - " --txt: #c8cdd8;\n" - " --hi: #eef0f5;\n" - " --green: #4af0a0;\n" - " --yellow: #f0a04a;\n" - " --red: #f06060;\n" - " --purple: #c084fc;\n" - " --mono: 'Courier New', 'Lucida Console', monospace;\n" - "}\n" - "body {\n" - " background: var(--bg);\n" - " color: var(--txt);\n" - " font-family: var(--mono);\n" - " font-size: 14px;\n" - " line-height: 1.5;\n" - " min-height: 100vh;\n" - "}\n" - "/* TOP BAR */\n" - "header {\n" - " background: var(--sur);\n" - " border-bottom: 1px solid var(--bor);\n" - " padding: 0 20px;\n" - " height: 44px;\n" - " display: flex;\n" - " align-items: center;\n" - " justify-content: space-between;\n" - " position: sticky;\n" - " top: 0;\n" - " z-index: 1;\n" - "}\n" - "header b { color: var(--green); letter-spacing: 2px; }\n" - "header small { color: var(--dim); font-size: 12px; font-family: var(--mono); }\n" - "header a { color: var(--dim); font-size: 12px; }\n" - "header a:hover { color: var(--green); }\n" - "/* MAIN CONTENT */\n" - "main { max-width: 860px; margin: 0 auto; padding: 24px 16px; }\n" - "/* LOGIN */\n" - ".login-page {\n" - " min-height: 100vh;\n" - " display: flex;\n" - " align-items: center;\n" - " justify-content: center;\n" - " padding: 24px;\n" - "}\n" - ".login-page form {\n" - " background: var(--sur);\n" - " border: 1px solid var(--bor);\n" - " padding: 36px 28px;\n" - " width: 100%%;\n" - " max-width: 320px;\n" - " display: flex;\n" - " flex-direction: column;\n" - " gap: 14px;\n" - "}\n" - ".login-page h1 { color: var(--green); letter-spacing: 3px; text-align: center; font-size: 20px; }\n" - ".login-page label { display: block; color: var(--dim); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 5px; }\n" - ".login-page input {\n" - " width: 100%%;\n" - " background: var(--bg);\n" - " border: 1px solid var(--bor);\n" - " color: var(--hi);\n" - " font-family: var(--mono);\n" - " font-size: 16px;\n" - " padding: 9px 10px;\n" - " outline: none;\n" - "}\n" - ".login-page input:focus { border-color: var(--green); }\n" - ".login-page .error {\n" - " color: var(--red);\n" - " background: #2e0a0a;\n" - " border: 1px solid #5c1a1a;\n" - " padding: 7px 10px;\n" - " font-size: 12px;\n" - "}\n" - "/* SHARED BUTTON */\n" - "button {\n" - " font-family: var(--mono);\n" - " font-size: 11px;\n" - " letter-spacing: 1px;\n" - " text-transform: uppercase;\n" - " padding: 6px 14px;\n" - " border: 1px solid var(--green);\n" - " background: transparent;\n" - " color: var(--green);\n" - " cursor: pointer;\n" - " touch-action: manipulation;\n" - "}\n" - "button:hover, button:active { background: var(--green); color: var(--bg); }\n" - ".login-page button { padding: 9px; width: 100%%; }\n" - "/* PAGE HEADING */\n" - "main > h2 {\n" - " color: var(--hi);\n" - " font-size: 15px;\n" - " letter-spacing: 2px;\n" - " text-transform: uppercase;\n" - " border-bottom: 1px solid var(--bor);\n" - " padding-bottom: 12px;\n" - " margin-bottom: 20px;\n" - " display: flex;\n" - " justify-content: space-between;\n" - " align-items: baseline;\n" - " flex-wrap: wrap;\n" - " gap: 8px;\n" - "}\n" - "main > h2 small { color: var(--dim); font-size: 12px; font-weight: normal; letter-spacing: 0; }\n" - "/* CLIENT GRID */\n" - ".clients {\n" - " display: grid;\n" - " grid-template-columns: 1fr 150px 155px 80px 70px;\n" - " gap: 0;\n" - "}\n" - ".clients > span {\n" - " font-size: 11px;\n" - " letter-spacing: 1px;\n" - " text-transform: uppercase;\n" - " color: var(--dim);\n" - " padding: 0 12px 10px 12px;\n" - " border-bottom: 1px solid var(--bor);\n" - "}\n" - ".clients > span:last-child { text-align: right; }\n" - ".clients > a {\n" - " display: grid;\n" - " grid-column: 1 / -1;\n" - " grid-template-columns: subgrid;\n" - " align-items: center;\n" - " padding: 11px 12px;\n" - " border-bottom: 1px solid var(--bor);\n" - " text-decoration: none;\n" - " color: var(--txt);\n" - " transition: background .1s;\n" - "}\n" - ".clients > a:hover { background: var(--sur); }\n" - ".clients > a > :first-child { color: var(--hi); }\n" - ".clients > a > :last-child { text-align: right; }\n" - ".clients > a.stale > :first-child { border-left: 2px solid var(--purple); padding-left: 8px; }\n" - ".clients > a.stale > :nth-child(3) { color: var(--purple); }\n" - ".clients > a > code { font-size: 12px; color: var(--dim); letter-spacing: 1px; }\n" - "@media (max-width: 580px) {\n" - " .clients > span { display: none; }\n" - " .clients > a {\n" - " grid-template-columns: 1fr auto;\n" - " grid-template-rows: auto auto;\n" - " padding: 13px 14px;\n" - " }\n" - " .clients > a > :nth-child(1) { grid-column: 1; grid-row: 1; }\n" - " .clients > a > :nth-child(2) { display: none; }\n" - " .clients > a > :nth-child(3) { grid-column: 1; grid-row: 2; font-size: 11px; color: var(--dim); }\n" - " .clients > a > :nth-child(4) { grid-column: 2; grid-row: 1 / 3; align-self: center; }\n" - " .clients > a > :nth-child(5) { display: none; }\n" - " .clients > a.stale { border-left: 3px solid var(--purple); padding-left: 11px; }\n" - " .clients > a.stale > :first-child { border-left: none; padding-left: 0; }\n" - "}\n" - "/* BADGES */\n" - ".badge { border: 1px solid; padding: 2px 7px; font-size: 10px; letter-spacing: 1px; justify-self: center; }" - ".badge.ok { color: var(--green); background: #0a2e1a; border-color: #1a5c34; }\n" - ".badge.run { color: var(--yellow); background: #2e200a; border-color: #5c400e; }\n" - ".badge.fail { color: var(--red); background: #2e0a0a; border-color: #5c1a1a; }\n" - ".badge.stale { color: var(--purple); background: #1e0a2e; border-color: #5c1a8c; }\n" - "@keyframes blink { 50%% { opacity: .4; } }\n" - "/* ADD FORM */\n" - "#add-form {\n" - " background: var(--sur);\n" - " border: 1px solid var(--bor);\n" - " padding: 16px 20px;\n" - " margin-bottom: 20px;\n" - " display: none;\n" - " gap: 12px;\n" - " flex-direction: column;\n" - "}\n" - "#add-form.open { display: flex; }\n" - "#add-form label { color: var(--dim); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; display: block; margin-bottom: 5px; }\n" - "#add-form input {\n" - " background: var(--bg);\n" - " border: 1px solid var(--bor);\n" - " color: var(--hi);\n" - " font-family: var(--mono);\n" - " font-size: 16px;\n" - " padding: 8px 10px;\n" - " outline: none;\n" - " width: 100%%;\n" - " max-width: 280px;\n" - "}\n" - "#add-form input:focus { border-color: var(--green); }\n" - "/* DETAIL HEADER */\n" - "article {\n" - " background: var(--sur);\n" - " border: 1px solid var(--bor);\n" - " padding: 16px 20px;\n" - " margin-bottom: 24px;\n" - "}\n" - "article.stale { border-left: 3px solid var(--purple); }\n" - "article h3 { color: var(--green); font-size: 17px; letter-spacing: 1px; margin-bottom: 4px; }\n" - "article p { color: var(--dim); font-size: 12px; }\n" - "article p.warning {\n" - " color: var(--purple);\n" - " background: #1e0a2e;\n" - " border: 1px solid #5c1a8c;\n" - " padding: 7px 10px;\n" - " margin-top: 10px;\n" - " font-size: 12px;\n" - "}\n" - "article code {\n" - " display: block;\n" - " margin-top: 10px;\n" - " padding-top: 10px;\n" - " border-top: 1px solid var(--bor);\n" - " font-size: 12px;\n" - " color: var(--txt);\n" - " word-break: break-all;\n" - "}\n" - "/* BACK LINK */\n" - "main > a { display: inline-block; color: var(--dim); font-size: 12px; margin-bottom: 20px; text-decoration: none; }\n" - "main > a:hover { color: var(--green); }\n" - "/* SNIPPETS */\n" - "details {\n" - " margin-bottom: 28px;\n" - " border-bottom: 1px solid var(--bor);\n" - "}\n" - "details summary {\n" - " font-size: 11px;\n" - " letter-spacing: 2px;\n" - " text-transform: uppercase;\n" - " color: var(--dim);\n" - " padding-bottom: 10px;\n" - " cursor: pointer;\n" - " list-style: none;\n" - " display: flex;\n" - " justify-content: space-between;\n" - "}\n" - "details summary:hover { color: var(--txt); }\n" - "details summary::after { content: '>'; transition: transform .2s; }\n" - "details[open] summary::after { transform: rotate(90deg); }\n" - "details summary::-webkit-details-marker { display: none; }\n" - "details > div { padding-top: 14px; display: flex; flex-direction: column; gap: 10px; }\n" - "details h4 { font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: var(--dim); margin-bottom: 6px; font-weight: normal; }\n" - "details pre {\n" - " background: var(--bg);\n" - " border: 1px solid var(--bor);\n" - " padding: 12px 60px 12px 14px;\n" - " font-family: var(--mono);\n" - " font-size: 12px;\n" - " overflow-x: auto;\n" - " line-height: 1.6;\n" - " position: relative;\n" - " white-space: pre;\n" - "}\n" - "details pre .cmd { color: var(--yellow); }\n" - "details pre .flag { color: var(--dim); }\n" - "details pre .url { color: var(--green); }\n" - "details pre .key { color: #7dd3fc; }\n" - "details pre .val { color: #f9a8d4; }\n" - "details pre .secret { color: var(--purple); }\n" - "details pre button {\n" - " position: absolute;\n" - " top: 8px; right: 8px;\n" - " background: var(--sur);\n" - " border-color: var(--bor);\n" - " color: var(--dim);\n" - " font-size: 10px;\n" - "}\n" - "details pre button:hover { border-color: var(--green); color: var(--green); background: transparent; }\n" - "/* BACKUP LIST */\n" - "main > h4 { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--dim); margin-bottom: 12px; font-weight: normal; }\n" - ".backups { display: flex; flex-direction: column; gap: 8px; }\n" - ".backups > div {\n" - " background: var(--sur);\n" - " border: 1px solid var(--bor);\n" - " padding: 10px 16px;\n" - " display: flex;\n" - " align-items: center;\n" - " gap: 12px;\n" - "}\n" - ".backups > div > time { color: var(--hi); font-size: 13px; flex: 1; }\n" - ".backups > div > span { color: var(--dim); font-size: 12px; min-width: 70px; text-align: right; }\n" - "</style>\n" - ); -} - -static void mask_secret(const char *secret, char *out, size_t out_len) { - size_t len = strlen(secret); - if (len >= 8 && out_len >= 13) { - snprintf(out, out_len, "%.4s....%.4s", secret, secret + len - 4); - } else if (len > 0) { - snprintf(out, out_len, "****"); - } else { - out[0] = '\0'; - } -} - -static const char *status_to_badge_class(const char *status) { - if (strcmp(status, "OK") == 0) return "ok"; - if (strcmp(status, "RUNNING") == 0) return "run"; - if (strcmp(status, "STALE") == 0) return "stale"; - if (strcmp(status, "INTERRUPTED") == 0) return "fail"; - if (strcmp(status, "FINISHED") == 0) return "ok"; - return "ok"; -} - -static const char *status_to_display(const char *status) { - if (strcmp(status, "OK") == 0) return "OK"; - if (strcmp(status, "RUNNING") == 0) return "Running"; - if (strcmp(status, "STALE") == 0) return "Stale"; - if (strcmp(status, "INTERRUPTED") == 0) return "Failed"; - if (strcmp(status, "FINISHED") == 0) return "Done"; - return status; -} - -static char *render_clients_page(struct Request *request, size_t *out_len) { - int ret; - struct ListClientResult *result = malloc(sizeof(struct ListClientResult) + sizeof(struct ListClientResultClient) * 16); - ret = list_clients(request, result); - if(ret != 0) { - free(result); - return NULL; - } - - // Count stale clients - int stale_count = 0; - for (int i = 0; i < result->clients_len; i++) { - if (result->clients[i].is_stale) stale_count++; - } - - char *buf = NULL; - size_t buf_len = 0; - FILE *f = open_memstream(&buf, &buf_len); - if (!f) { - free(result); - return NULL; - } - - fprintf(f, - "<!DOCTYPE html>\n" - "<html lang=\"en\">\n" - "<head>\n" - " <meta charset=\"UTF-8\">\n" - " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" - " <title>BorgFlag - Clients</title>\n"); - emit_stylesheet(f); - fprintf(f, - "</head>\n" - "<body>\n" - " <header>\n" - " <b>BorgFlag</b>\n" - " <small><form method=\"POST\" action=\"/logout\" style=\"display:inline\"><button type=\"submit\" style=\"border:none;background:none;color:var(--dim);font-size:12px;padding:0;cursor:pointer;text-transform:none;letter-spacing:0\">Sign out</button></form></small>\n" - " </header>\n" - " <main>\n" - " <h2>\n" - " Clients\n" - " <small>%d registered", result->clients_len); - - if (stale_count > 0) { - fprintf(f, "<span style=\"color:var(--purple)\">%d stale</span>", stale_count); - } - - fprintf(f, - "</small>\n" - " <button onclick=\"document.getElementById('add-form').classList.toggle('open')\">+ Add</button>\n" - " </h2>\n" - " <form id=\"add-form\" method=\"POST\" action=\"/clients\">\n" - " <div>\n" - " <label>Name</label>\n" - " <input type=\"text\" name=\"name\" maxlength=\"31\" placeholder=\"prod-web-02\" required>\n" - " </div>\n" - " <button type=\"submit\">Create</button>\n" - " </form>\n" - " <div class=\"clients\">\n" - " <span>Name</span>\n" - " <span>Secret</span>\n" - " <span>Last Backup</span>\n" - " <span>Status</span>\n" - " <span></span>\n" - ); - - for (int i = 0; i < result->clients_len; i++) { - char masked_secret[16]; - mask_secret(result->clients[i].secret, masked_secret, sizeof(masked_secret)); - - fprintf(f, - " <a href=\"/client/%ld\"%s>\n" - " <span>%s</span>\n" - " <code>%s</code>\n" - " <span>%s</span>\n" - " <b class=\"badge %s\">%s</b>\n" - " <button onclick=\"event.stopPropagation();window.location='/client/%ld'\">View →</button>\n" - " </a>\n", - result->clients[i].id, - result->clients[i].is_stale ? " class=\"stale\"" : "", - result->clients[i].name, - masked_secret, - result->clients[i].last_backup.tv_sec > 0.0 ? format_time(result->clients[i].last_backup).buf : "-", - status_to_badge_class(result->clients[i].status), - status_to_display(result->clients[i].status), - result->clients[i].id - ); - } - - fprintf(f, - " </div>\n" - " </main>\n" - "</body>\n" - "</html>\n"); - - fclose(f); - *out_len = buf_len; - free(result); - return buf; -} - -static char *render_client_detail_page(struct Request *request, int64_t client_id, size_t *out_len) { - struct GetClientOnlyResult client_result; - int ret = get_client(request, client_id, &client_result); - if(ret != 0) { - return NULL; - } - - if(!client_result.found) { - return NULL; - } - - struct GetBackupsResult *backups_result = malloc(sizeof(struct GetBackupsResult) + sizeof(struct BackupRecord) * MAX_BACKUPS_DISPLAY); - backups_result->backups_len = MAX_BACKUPS_DISPLAY; - ret = get_client_backups(request, client_id, backups_result); - if(ret != 0) { - free(backups_result); - return NULL; - } - - // Determine status and staleness - int is_stale = 0; - const char *current_status = "OK"; - if (backups_result->backups_len > 0) { - const char *last_status = backups_result->backups[0].status; - if (strcmp(last_status, "RUNNING") == 0) { - current_status = "RUNNING"; - } else if (strcmp(last_status, "INTERRUPTED") == 0) { - current_status = "INTERRUPTED"; - } - } - - char *buf = NULL; - size_t buf_len = 0; - FILE *f = open_memstream(&buf, &buf_len); - if (!f) { - free(backups_result); - return NULL; - } - - fprintf(f, - "<!DOCTYPE html>\n" - "<html lang=\"en\">\n" - "<head>\n" - " <meta charset=\"UTF-8\">\n" - " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" - " <title>BorgFlag - %s</title>\n", client_result.name); - emit_stylesheet(f); - fprintf(f, - "<script>\n" - "function copy(btn) {\n" - " navigator.clipboard.writeText(btn.closest('pre').innerText.replace('Copy','').trim()).then(() => {\n" - " btn.textContent = 'Copied!';\n" - " setTimeout(() => btn.textContent = 'Copy', 2000);\n" - " });\n" - "}\n" - "</script>\n" - "</head>\n" - "<body>\n" - " <header>\n" - " <b>BorgFlag</b>\n" - " <small><form method=\"POST\" action=\"/logout\" style=\"display:inline\"><button type=\"submit\" style=\"border:none;background:none;color:var(--dim);font-size:12px;padding:0;cursor:pointer;text-transform:none;letter-spacing:0\">Sign out</button></form></small>\n" - " </header>\n" - " <main>\n" - " <a href=\"/clients\">← Back to clients</a>\n" - " <article%s>\n" - " <h3>%s</h3>\n" - " <p>%d backups <b class=\"badge %s\">%s</b></p>\n", - is_stale ? " class=\"stale\"" : "", - client_result.name, - backups_result->backups_len, - status_to_badge_class(current_status), - status_to_display(current_status) - ); - - if (is_stale) { - fprintf(f, - " <p class=\"warning\">Warning: Last backup was more than 24 hours ago. Expected every 24 hours.</p>\n" - ); - } - - fprintf(f, - " <code>Secret: %s</code>\n" - " </article>\n" - " <details>\n" - " <summary>API Snippets</summary>\n" - " <div>\n" - " <div>\n" - " <h4>Start backup</h4>\n" - " <pre><span class=\"cmd\">curl</span> <span class=\"flag\">-X POST</span> <span class=\"url\">http://%s/api/report</span> \\\n" - " <span class=\"flag\">-H</span> <span class=\"val\">\"Content-Type: application/json\"</span> \\\n" - " <span class=\"flag\">-d</span> <span class=\"val\">'{</span>\n" - " <span class=\"key\">\"client\"</span>: <span class=\"val\">\"%s\"</span>,\n" - " <span class=\"key\">\"secret\"</span>: <span class=\"secret\">\"%s\"</span>,\n" - " <span class=\"key\">\"event\"</span>: <span class=\"val\">\"begin_backup\"</span>\n" - " <span class=\"val\">}'</span><button onclick=\"copy(this)\">Copy</button></pre>\n" - " </div>\n" - " <div>\n" - " <h4>End backup</h4>\n" - " <pre><span class=\"cmd\">curl</span> <span class=\"flag\">-X POST</span> <span class=\"url\">http://%s/api/report</span> \\\n" - " <span class=\"flag\">-H</span> <span class=\"val\">\"Content-Type: application/json\"</span> \\\n" - " <span class=\"flag\">-d</span> <span class=\"val\">'{</span>\n" - " <span class=\"key\">\"client\"</span>: <span class=\"val\">\"%s\"</span>,\n" - " <span class=\"key\">\"secret\"</span>: <span class=\"secret\">\"%s\"</span>,\n" - " <span class=\"key\">\"event\"</span>: <span class=\"val\">\"end_backup\"</span>\n" - " <span class=\"val\">}'</span><button onclick=\"copy(this)\">Copy</button></pre>\n" - " </div>\n" - " </div>\n" - " </details>\n", - client_result.secret, - request->host ? request->host : "localhost", - client_result.name, - client_result.secret, - request->host ? request->host : "localhost", - client_result.name, - client_result.secret - ); - - fprintf(f, " <h4>Recent Backups</h4>\n"); - - if(backups_result->backups_len == 0) { - fprintf(f, " <p style=\"color:var(--dim)\">No backups recorded.</p>\n"); - } else { - fprintf(f, " <div class=\"backups\">\n"); - - for (int i = 0; i < backups_result->backups_len; i++) { - const char *badge_class; - const char *badge_text; - if (strcmp(backups_result->backups[i].status, "RUNNING") == 0) { - badge_class = "run"; - badge_text = "Running"; - } else if (strcmp(backups_result->backups[i].status, "INTERRUPTED") == 0) { - badge_class = "fail"; - badge_text = "Failed"; - } else { - badge_class = "ok"; - badge_text = "Done"; - } - - struct timespec duration = time_sub(&backups_result->backups[i].completed, &backups_result->backups[i].started); - fprintf(f, - " <div><time>%s</time><span>%s</span><b class=\"badge %s\">%s</b></div>\n", - format_time(backups_result->backups[i].started).buf, - format_duration(duration).buf, - badge_class, - badge_text - ); - } - - fprintf(f, " </div>\n"); - } - - fprintf(f, - " </main>\n" - "</body>\n" - "</html>\n"); - - fclose(f); - *out_len = buf_len; - free(backups_result); - return buf; -} - -static char *render_login_page(const char *error_msg, size_t *out_len) { - char *buf = NULL; - size_t buf_len = 0; - FILE *f = open_memstream(&buf, &buf_len); - if (!f) return NULL; - - fprintf(f, - "<!DOCTYPE html>\n" - "<html lang=\"en\">\n" - "<head>\n" - " <meta charset=\"UTF-8\">\n" - " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" - " <title>BorgFlag - Login</title>\n"); - emit_stylesheet(f); - fprintf(f, - "</head>\n" - "<body>\n" - " <div class=\"login-page\">\n" - " <form method=\"POST\" action=\"/login\">\n" - " <h1>BorgFlag</h1>\n"); - - if (error_msg) { - fprintf(f, " <div class=\"error\">%s</div>\n", error_msg); - } - - fprintf(f, - " <div>\n" - " <label>Username</label>\n" - " <input type=\"text\" name=\"username\" autocomplete=\"username\" required>\n" - " </div>\n" - " <div>\n" - " <label>Password</label>\n" - " <input type=\"password\" name=\"password\" autocomplete=\"current-password\" required>\n" - " </div>\n" - " <button type=\"submit\">Sign In</button>\n" - " </form>\n" - " </div>\n" - "</body>\n" - "</html>\n"); - - fclose(f); - *out_len = buf_len; - return buf; -} - static enum MHD_Result create_post_collector(struct PostCollector *c) { c->data = malloc(512); c->size = 0; @@ -1438,7 +190,7 @@ static enum MHD_Result handler( if (!request->form.username_set || !request->form.password_set) { size_t html_len; - char *html = render_login_page("Invalid credentials", &html_len); + char *html = html_render_login_page("Invalid credentials", &html_len); if (!html) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( html_len, html, MHD_RESPMEM_MUST_FREE); @@ -1451,7 +203,7 @@ static enum MHD_Result handler( if (!validate_credentials(server->app, request->form.username, request->form.password)) { size_t html_len; - char *html = render_login_page("Invalid credentials", &html_len); + char *html = html_render_login_page("Invalid credentials", &html_len); if (!html) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( html_len, html, MHD_RESPMEM_MUST_FREE); @@ -1495,7 +247,7 @@ static enum MHD_Result handler( } struct CreateClientResult result; - ret = create_client(request, request->form.name, &result); + ret = create_client(server->app, request->form.name, &result); if(ret != 0) return MHD_NO; // Post-Redirect-Get: redirect back to GET /clients @@ -1524,7 +276,7 @@ static enum MHD_Result handler( return MHD_NO; } - ret = delete_client(request, request->form.client_id); + ret = delete_client(server->app, request->form.client_id); if(ret != 0) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( @@ -1552,17 +304,17 @@ static enum MHD_Result handler( if(!cJSON_IsString(eventJson) || eventJson->valuestring == NULL) { abort(); } - enum ReportEvent event; + int is_finish; if(strcmp(eventJson->valuestring, "begin_backup") == 0) { - event = REV_BEGIN; + is_finish = 0; } else if(strcmp(eventJson->valuestring, "end_backup") == 0) { - event = REV_FINISH; + is_finish = 1; } else { abort(); } - ret = submit_report(server->app, clientJson->valuestring, secretJson->valuestring, event); + ret = submit_report(server->app, clientJson->valuestring, secretJson->valuestring, is_finish); if(ret == -2) { cJSON_Delete(rootJson); destroy_post_collector(&request->pp); @@ -1614,7 +366,7 @@ static enum MHD_Result handler( } struct CreateClientResult result; - ret = create_client(request, nameJson->valuestring, &result); + ret = create_client(server->app, nameJson->valuestring, &result); if(ret != 0) { // Should actually return an error page, but this will do for now return MHD_NO; @@ -1668,7 +420,7 @@ static enum MHD_Result handler( if(strcmp(url, "/login") == 0) { size_t html_len; - char *html = render_login_page(NULL, &html_len); + char *html = html_render_login_page(NULL, &html_len); if (!html) return MHD_NO; struct MHD_Response *response = MHD_create_response_from_buffer( html_len, html, MHD_RESPMEM_MUST_FREE); @@ -1688,8 +440,18 @@ static enum MHD_Result handler( return ret; } + // Fetch clients from app layer + struct ListClientResult *clients = malloc(sizeof(struct ListClientResult) + sizeof(struct ListClientResultClient) * 16); + ret = list_clients(server->app, clients); + if(ret != 0) { + free(clients); + return MHD_NO; + } + + // Render HTML size_t html_len; - char *html = render_clients_page(request, &html_len); + char *html = html_render_clients_page(request->host, clients, &html_len); + free(clients); if (!html) { return MHD_NO; @@ -1730,8 +492,34 @@ static enum MHD_Result handler( return ret; } + // Fetch client data from app layer + struct GetClientOnlyResult client_result; + ret = get_client(server->app, client_id, &client_result); + if(ret != 0) { + return MHD_NO; + } + + if(!client_result.found) { + // Not Found + struct MHD_Response *response = MHD_create_response_from_buffer( + 0, "", MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, response); + MHD_destroy_response(response); + return ret; + } + + struct GetBackupsResult *backups_result = malloc(sizeof(struct GetBackupsResult) + sizeof(struct BackupRecord) * MAX_BACKUPS_DISPLAY); + backups_result->backups_len = MAX_BACKUPS_DISPLAY; + ret = get_client_backups(server->app, client_id, backups_result); + if(ret != 0) { + free(backups_result); + return MHD_NO; + } + + // Render HTML size_t html_len; - char *html = render_client_detail_page(request, client_id, &html_len); + char *html = html_render_client_detail_page(request->host, &client_result, backups_result, &html_len); + free(backups_result); if (!html) { // Not Found |
