diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a1a630 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.dub +docs.json +__dummy.html +docs/ +/web-logbook +web-logbook.so +web-logbook.dylib +web-logbook.dll +web-logbook.a +web-logbook.lib +web-logbook-test-* +*.exe +*.pdb +*.o +*.obj +*.lst +logbook.sqlite diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..51dee3b --- /dev/null +++ b/dub.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "Andrew Lalis" + ], + "copyright": "Copyright © 2023, Andrew Lalis", + "dependencies": { + "d-properties": "~>1.0.4", + "d2sqlite3": "~>1.0.0", + "handy-httpd": "~>7.6.4" + }, + "subConfigurations": { + "d2sqlite3": "all-included" + }, + "description": "A minimal D application.", + "license": "MIT", + "name": "web-logbook" +} \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..d31581d --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,11 @@ +{ + "fileVersion": 1, + "versions": { + "d-properties": "1.0.4", + "d2sqlite3": "1.0.0", + "handy-httpd": "7.6.4", + "httparsed": "1.2.1", + "slf4d": "2.4.2", + "streams": "3.5.0" + } +} diff --git a/source/app.d b/source/app.d new file mode 100644 index 0000000..8059366 --- /dev/null +++ b/source/app.d @@ -0,0 +1,128 @@ +import handy_httpd; +import handy_httpd.handlers.path_delegating_handler; +import slf4d; +import d_properties; +import d2sqlite3; + +import std.file; +import std.conv; +import std.json; +import std.datetime; + +void main() { + ServerConfig config = ServerConfig.defaultValues(); + if (exists("application.properties")) { + Properties props = Properties("application.properties"); + if (props.has("hostname")) { + config.hostname = props.get("hostname"); + } + if (props.has("port")) { + config.port = props.get("port").to!ushort; + } + } + initDb(); + HttpServer server = new HttpServer((ref HttpRequestContext ctx) { + ctx.response.addHeader("Access-Control-Allow-Origin", "*"); + ctx.response.addHeader("Access-Control-Allow-Headers", "*"); + if (ctx.request.method == Method.GET) { + handleLogbookRequest(ctx); + } else if (ctx.request.method == Method.POST) { + handleVisitorLog(ctx); + } else if (ctx.request.method == Method.OPTIONS) { + ctx.response.setStatus(HttpStatus.OK); + } else { + ctx.response.setStatus(HttpStatus.METHOD_NOT_ALLOWED); + } + }, config); + server.start(); +} + +void handleVisitorLog(ref HttpRequestContext ctx) { + infoF!"Got visitor log from %s"(ctx.request.remoteAddress); + JSONValue logBody = ctx.request.readBodyAsJson(); + string name = logBody.object["name"].str; + string message = logBody.object["message"].str; + string remoteAddress = "UNKNOWN"; + if (ctx.request.remoteAddress !is null) { + remoteAddress = ctx.request.remoteAddress.toString(); + } + insertLogEntry(remoteAddress, name, message); +} + +void handleLogbookRequest(ref HttpRequestContext ctx) { + LogEntry[] entries = getRecentLogEntries(); + JSONValue entriesJson = JSONValue(JSONValue[].init); + foreach (LogEntry entry; entries) { + entriesJson.array ~= entry.toJson(); + } + ctx.response.writeBodyString(entriesJson.toString(), "application/json"); +} + + + +struct LogEntry { + ulong id; + SysTime createdAt; + string remoteAddress; + string name; + string message; + + JSONValue toJson() const { + JSONValue obj = JSONValue(string[string].init); + obj["id"] = JSONValue(id); + obj["createdAt"] = JSONValue(createdAt.toISOString()); + obj["remoteAddress"] = JSONValue(remoteAddress); + obj["name"] = JSONValue(name); + obj["message"] = JSONValue(message); + return obj; + } +} + +void initDb() { + Database db = Database("logbook.sqlite"); + db.run(q"SQL +CREATE TABLE IF NOT EXISTS log_entry ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + remote_address TEXT NOT NULL, + name TEXT NOT NULL, + message TEXT NOT NULL +); +SQL"); + db.close(); + info("Initialized database."); +} + +void insertLogEntry(string remoteAddress, string name, string message) { + Database db = Database("logbook.sqlite"); + Statement stmt = db.prepare(q"SQL +INSERT INTO log_entry (remote_address, name, message) +VALUES (:addr, :name, :msg); +SQL"); + stmt.bind(1, remoteAddress); + stmt.bind(2, name); + stmt.bind(3, message); + stmt.execute(); + stmt.finalize(); + db.close(); + infoF!"Added log entry for %s @ %s"(name, remoteAddress); +} + +LogEntry[] getRecentLogEntries() { + Database db = Database("logbook.sqlite"); + ResultRange results = db.execute("SELECT * FROM log_entry ORDER BY created_at DESC LIMIT 5"); + LogEntry[] entries; + foreach (Row row; results) { + LogEntry entry; + entry.id = row.peek!ulong(0); + string createdAtStr = row.peek!string(1); + string isoCreatedAt = createdAtStr[0 .. 4] ~ createdAtStr[5 .. 7] ~ createdAtStr[8 .. 10] ~ 'T' ~ + createdAtStr[11 .. 13] ~ createdAtStr[14 .. 16] ~ createdAtStr[17 .. $]; + entry.createdAt = SysTime.fromISOString(isoCreatedAt); + entry.remoteAddress = row.peek!string(2); + entry.name = row.peek!string(3); + entry.message = row.peek!string(4); + entries ~= entry; + } + return entries; +}