diff options
Diffstat (limited to 'zig-comments/src/main.zig')
-rw-r--r-- | zig-comments/src/main.zig | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/zig-comments/src/main.zig b/zig-comments/src/main.zig new file mode 100644 index 0000000..951f5f0 --- /dev/null +++ b/zig-comments/src/main.zig @@ -0,0 +1,183 @@ +const std = @import("std"); +const zws = @import("zws"); +const pq = @import("pq.zig"); +const mustache = @import("mustache"); + +const Params = zws.Params; + +const Err = error{ Overflow, InvalidCharacter, StreamTooLong, ColumnNotFound } || pq.PqError || std.http.Server.Response.WaitError || std.http.Server.Response.DoError || std.http.Server.Response.ReadError || std.http.Server.Response.FinishError || zws.Path.ParseError; +const Ctx = struct { + db: pq.Db, + pub fn clone(self: @This()) Ctx { + return Ctx{ + .db = self.db, + }; + } + pub fn deinit(self: @This()) void { + _ = self; + } +}; + +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const Rtr = zws.Router(Ctx, Err); +const router = Rtr{ + .handlers = &[_]Rtr.Handler{ + .{ + .method = .GET, + .pattern = "/api/comments", + .handle_fn = get_comments, + }, + .{ + .method = .POST, + .pattern = "/api/comments", + .handle_fn = post_comments, + }, + .{ + .method = .GET, + .pattern = "/api/form/*", + .handle_fn = get_form, + }, + }, + .notfound = notfound, +}; + +pub fn main() !void { + var db = try pq.Db.init("postgresql://comments@localhost/comments"); + try db.exec(@embedFile("migrations/0_init.sql")); + try db.exec(@embedFile("migrations/1_capcha.sql")); + defer db.deinit(); + const server = zws.Server(Ctx, Rtr){ + .allocator = gpa.allocator(), + .address = std.net.Address{ .in = std.net.Ip4Address.init(.{ 127, 0, 0, 1 }, 8080) }, + .context = Ctx{ .db = db }, + .handler = router, + }; + + try server.serve(); +} + +fn notfound(res: *std.http.Server.Response, _: Ctx) Err!void { + const rr = @embedFile("templates/notfound.html"); + try constresponse(res, rr, .not_found); +} + +fn badrequest(res: *std.http.Server.Response, _: Ctx) Err!void { + const rr = @embedFile("templates/badrequest.html"); + try constresponse(res, rr, .bad_request); +} + +fn constresponse(res: *std.http.Server.Response, rr: []const u8, status: std.http.Status) Err!void { + res.status = status; + res.transfer_encoding = .{ .content_length = rr.len }; + try res.headers.append("content-type", "text/html"); + try res.do(); + try res.writeAll(rr); + try res.finish(); +} + +fn get_comments(res: *std.http.Server.Response, ctx: Ctx, _: Params) Err!void { + _ = ctx; + // Run SQL + // Render comments template + //zws.parse + + const rr = @embedFile("templates/comments.html"); + res.transfer_encoding = .{ .content_length = rr.len }; + try res.headers.append("content-type", "text/html"); + try res.do(); + try res.writeAll(rr); + try res.finish(); +} + +fn post_comments(res: *std.http.Server.Response, ctx: Ctx, _: Params) Err!void { + var body_aa = std.ArrayList(u8).init(res.allocator); + try res.reader().readAllArrayList(&body_aa, 1_000_000); + var body = try body_aa.toOwnedSlice(); + var form = try zws.Form.parse(res.allocator, body); + const Form = struct { + url: []const u8, + capcha_id: []const u8, + author: []const u8, + comment: []const u8, + capcha_answer: []const u8, + }; + const form_val = form.form_to_struct(Form) catch { + try badrequest(res, ctx); + return; + }; + + // Validate the capcha + { + var stmt = try ctx.db.prepare_statement(res.allocator, "select answer from capchas where id = $1"); + defer stmt.deinit(); + try stmt.bind(0, form_val.capcha_id); + if (!try stmt.step()) { + std.log.err("missing capcha_id {s}", .{form_val.capcha_id}); + try badrequest(res, ctx); + return; + } + const ans = try stmt.read_column(0, []const u8); + if (!std.mem.eql(u8, ans, form_val.capcha_answer)) { + try constresponse(res, @embedFile("templates/capchainvalid.html"), std.http.Status.unauthorized); + return; + } + } + + // Add the comment... + { + var stmt = try ctx.db.prepare_statement(res.allocator, "insert into comments(url,author,comment) values($1, $2, $3)"); + defer stmt.deinit(); + try stmt.bind(0, form_val.url); + try stmt.bind(1, form_val.author); + try stmt.bind(2, form_val.comment); + _ = try stmt.step(); + } + + // And redirect! + res.transfer_encoding = .none; + res.status = .found; + try res.headers.append("location", form_val.url); + try res.do(); + try res.finish(); +} + +fn get_form(res: *std.http.Server.Response, ctx: Ctx, _: Params) Err!void { + var p = try zws.Path.parse(res.allocator, res.request.target); + defer p.deinit(); + const url: []const u8 = try p.get_query_param("url") orelse { + try badrequest(res, ctx); + return; + }; + + var stmt = try ctx.db.prepare_statement(res.allocator, "select id, question from capchas order by random() limit 1"); + defer stmt.deinit(); + if (!try stmt.step()) { + std.log.err("no capcha!", .{}); + try badrequest(res, ctx); + return; + } + const Capcha = struct { + id: []const u8, + question: []const u8, + }; + const capcha = try stmt.read_struct(Capcha); + + const rr = @embedFile("templates/form.html"); + const tt = comptime mustache.parseComptime(rr, .{}, .{}); + + res.transfer_encoding = .chunked; + try res.headers.append("content-type", "text/html"); + try res.do(); + // For some reason, mustache.render won't work with anonymous struct + const data = struct { + capcha_id: []const u8, + capcha_question: []const u8, + url: []const u8, + }; + try mustache.render(tt, data{ + .capcha_id = capcha.id, + .capcha_question = capcha.question, + .url = url, + }, res.writer()); + try res.finish(); +} |