summaryrefslogtreecommitdiff
path: root/src/web.c
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2026-02-21 11:14:42 +0100
committerJesper Jensen <jesper@jnsn.dev>2026-02-21 11:14:42 +0100
commit1b19afaa339f2f68520d61b634600ddeb04effb7 (patch)
tree232ec9fdc03a8955a8c847b88a688306689d2633 /src/web.c
parent0b6e8026a6b530c7d88607bfdf79b9692987abaa (diff)
Split up the rendering from the logic
Diffstat (limited to 'src/web.c')
-rw-r--r--src/web.c1312
1 files changed, 50 insertions, 1262 deletions
diff --git a/src/web.c b/src/web.c
index 6dd18e1..9c0789b 100644
--- a/src/web.c
+++ b/src/web.c
@@ -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 &rarr;</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\">&larr; 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