diff --git a/src/api/lib.zig b/src/api/lib.zig index fa8990d..ad272fa 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -13,14 +13,12 @@ const services = struct { const notes = @import("./services/notes.zig"); }; -pub const RegistrationRequest = struct { - username: []const u8, - password: []const u8, - invite_code: []const u8, +pub const RegistrationOptions = struct { + invite_code: ?[]const u8 = null, email: ?[]const u8 = null, }; -pub const InviteRequest = struct { +pub const InviteOptions = struct { pub const Kind = services.invites.Kind; name: ?[]const u8 = null, @@ -227,7 +225,7 @@ fn ApiConn(comptime DbConn: type) type { return community; } - pub fn createInvite(self: *Self, options: InviteRequest) !services.invites.Invite { + pub fn createInvite(self: *Self, options: InviteOptions) !services.invites.Invite { // Only logged in users can make invites const user_id = self.user_id orelse return error.TokenRequired; @@ -251,26 +249,33 @@ fn ApiConn(comptime DbConn: type) type { return try services.invites.get(self.db, invite_id, self.arena.allocator()); } - pub fn register(self: *Self, request: RegistrationRequest) !UserResponse { - std.log.debug("registering user {s} with code {s}", .{ request.username, request.invite_code }); - const invite = try services.invites.getByCode(self.db, request.invite_code, self.community.id, self.arena.allocator()); + pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse { + std.log.debug("registering user {s} with code {?s}", .{ username, opt.invite_code }); + const maybe_invite = if (opt.invite_code) |code| + try services.invites.getByCode(self.db, code, self.community.id, self.arena.allocator()) + else + null; - if (!Uuid.eql(invite.community_id, self.community.id)) return error.NotFound; - if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return error.InviteExpired; - if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return error.InviteExpired; + if (maybe_invite) |invite| { + if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; + if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return error.InviteExpired; + if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return error.InviteExpired; + } + + const invite_kind = if (maybe_invite) |inv| inv.kind else .user; if (self.community.kind == .admin) @panic("Unimplmented"); const user_id = try services.auth.register( self.db, - request.username, - request.password, + username, + password, self.community.id, - .{ .invite_id = invite.id, .email = request.email }, + .{ .invite_id = if (maybe_invite) |inv| inv.id else null, .email = opt.email }, self.arena.allocator(), ); - switch (invite.kind) { + switch (invite_kind) { .user => {}, .system => @panic("System user invites unimplemented"), .community_owner => { diff --git a/src/main/controllers.zig b/src/main/controllers.zig index e8749c7..a963521 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -13,40 +13,26 @@ pub const invites = @import("./controllers/invites.zig"); pub const users = @import("./controllers/users.zig"); pub const notes = @import("./controllers/notes.zig"); -pub fn routeRequest(api_source: anytype, request: http.Request, response: http.Response, alloc: std.mem.Allocator) void { +pub fn routeRequest(api_source: anytype, ctx: http.server.Context, alloc: std.mem.Allocator) void { // TODO: hashmaps? inline for (routes) |route| { - if (Context(route).matchAndHandle(api_source, request, response, alloc)) return; + if (Context(route).matchAndHandle(api_source, ctx, alloc)) return; } - // todo 404 + var response = Response{ .headers = http.Headers.init(alloc), .ctx = ctx }; + defer response.headers.deinit(); + + response.status(.not_found) catch {}; } -const routes = .{ sample_api, invites.create }; - -pub const sample_api = struct { - const Self = @This(); - - pub const method = .POST; - pub const path = "/notes/:id/reacts"; - pub const content_type = "application/json"; - - pub const Args = struct { - id: []const u8, - }; - - pub const Body = struct { - content: util.Uuid, - }; - - pub const Query = struct { - arg: []const u8 = "", - }; - - pub fn handler(ctx: Context(Self), response: *Response, _: api.ApiSource.Conn) !void { - std.log.debug("{}", .{ctx.body.content}); - try response.writeJson(.created, ctx.query); - } +const routes = .{ + auth.login, + auth.verify_login, + communities.create, + invites.create, + users.create, + notes.create, + //notes.get, }; pub fn Context(comptime Route: type) type { @@ -74,7 +60,7 @@ pub fn Context(comptime Route: type) type { query: Query, fn parseArgs(path: []const u8) ?Args { - var args: Route.Args = undefined; + var args: Args = undefined; var path_iter = util.PathIter.from(path); comptime var route_iter = util.PathIter.from(Route.path); inline while (comptime route_iter.next()) |route_segment| { @@ -93,7 +79,7 @@ pub fn Context(comptime Route: type) type { const req = ctx.request; if (req.method != Route.method) return false; var path = std.mem.sliceTo(std.mem.sliceTo(req.path, '#'), '?'); - var args: Route.Args = parseArgs(path) orelse return false; + var args: Args = parseArgs(path) orelse return false; var response = Response{ .headers = http.Headers.init(alloc), .ctx = ctx }; defer response.headers.deinit(); @@ -116,7 +102,7 @@ pub fn Context(comptime Route: type) type { } fn errorHandler(response: *Response, status: http.Status) void { - response.writeStatus(status) catch unreachable; + response.status(status) catch unreachable; } fn prepareAndHandle(self: *Self, api_source: anytype, req: http.Request, response: *Response) void { @@ -128,7 +114,7 @@ pub fn Context(comptime Route: type) type { var api_conn = self.getApiConn(api_source) catch return errorHandler(response, .internal_server_error); // TODO defer api_conn.close(); - self.handle(response, api_conn); + self.handle(response, &api_conn); } fn parseBody(self: *Self, req: http.Request) !void { @@ -179,16 +165,16 @@ pub const Response = struct { headers: http.Headers, ctx: http.server.Context, - pub fn writeStatus(self: *Self, status: http.Status) !void { - var stream = try self.ctx.openResponse(&self.headers, status); + pub fn status(self: *Self, status_code: http.Status) !void { + var stream = try self.ctx.openResponse(&self.headers, status_code); defer stream.close(); try stream.finish(); } - pub fn writeJson(self: *Self, status: http.Status, response_body: anytype) !void { + pub fn json(self: *Self, status_code: http.Status, response_body: anytype) !void { try self.headers.put("Content-Type", "application/json"); - var stream = try self.ctx.openResponse(&self.headers, status); + var stream = try self.ctx.openResponse(&self.headers, status_code); defer stream.close(); const writer = stream.writer(); @@ -196,6 +182,13 @@ pub const Response = struct { try stream.finish(); } + + pub fn err(self: *Self, status_code: http.Status, message: []const u8, details: anytype) !void { + return self.json(status_code, .{ + .message = message, + .details = details, + }); + } }; const json_options = if (builtin.mode == .Debug) diff --git a/src/main/controllers/actors.zig b/src/main/controllers/actors.zig deleted file mode 100644 index 3663972..0000000 --- a/src/main/controllers/actors.zig +++ /dev/null @@ -1,19 +0,0 @@ -const root = @import("root"); -const http = @import("http"); -const Uuid = @import("util").Uuid; - -const utils = @import("../controllers.zig").utils; - -const RequestServer = root.RequestServer; -const RouteArgs = http.RouteArgs; - -pub fn get(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void { - const id_str = args.get("id") orelse return error.NotFound; - const id = Uuid.parse(id_str) catch return utils.respondError(ctx, .bad_request, "Invalid UUID"); - var api = try utils.getApiConn(srv, ctx); - defer api.close(); - - const user = (try api.getActor(id)) orelse return utils.respondError(ctx, .not_found, "Note not found"); - - try utils.respondJson(ctx, .ok, user); -} diff --git a/src/main/controllers/auth.zig b/src/main/controllers/auth.zig index f07aa88..95be918 100644 --- a/src/main/controllers/auth.zig +++ b/src/main/controllers/auth.zig @@ -1,42 +1,28 @@ -const std = @import("std"); -const root = @import("root"); -const builtin = @import("builtin"); -const http = @import("http"); -const Uuid = @import("util").Uuid; - -const utils = @import("../controllers.zig").utils; - -const RequestServer = root.RequestServer; -const RouteArgs = http.RouteArgs; +const api = @import("api"); pub const login = struct { - pub const path = "/auth/login"; pub const method = .POST; - pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { - const credentials = try utils.parseRequestBody(struct { username: []const u8, password: []const u8 }, ctx); - defer utils.freeRequestBody(credentials, ctx.alloc); + pub const path = "/auth/login"; - var api = try utils.getApiConn(srv, ctx); - defer api.close(); + pub const Body = struct { + username: []const u8, + password: []const u8, + }; - const token = try api.login(credentials.username, credentials.password); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const token = try srv.login(req.body.username, req.body.password); - try utils.respondJson(ctx, .ok, token); + try res.json(.ok, token); } }; pub const verify_login = struct { - pub const path = "/auth/login"; pub const method = .GET; + pub const path = "/auth/login"; - pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { - var api = try utils.getApiConn(srv, ctx); - defer api.close(); + pub fn handler(_: anytype, res: anytype, srv: anytype) !void { + const info = try srv.verifyAuthorization(); - // The self-hosted compiler doesn't like inferring this error set. - // do this for now - const info = try api.verifyAuthorization(); - - try utils.respondJson(ctx, .ok, info); + try res.json(.ok, info); } }; diff --git a/src/main/controllers/communities.zig b/src/main/controllers/communities.zig index 952fd90..e108ce8 100644 --- a/src/main/controllers/communities.zig +++ b/src/main/controllers/communities.zig @@ -1,33 +1,16 @@ -const root = @import("root"); -const http = @import("http"); - -const utils = @import("../controllers.zig").utils; - -const RequestServer = root.RequestServer; -const RouteArgs = http.RouteArgs; +const api = @import("api"); pub const create = struct { pub const method = .POST; pub const path = "/communities"; - pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { - const opt = try utils.parseRequestBody(struct { origin: []const u8 }, ctx); - defer utils.freeRequestBody(opt, ctx.alloc); - var api = try utils.getApiConn(srv, ctx); - defer api.close(); + pub const Body = struct { + origin: []const u8, + }; - const invite = try api.createCommunity(opt.origin); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const invite = try srv.createCommunity(req.body.origin); - try utils.respondJson(ctx, .created, invite); + try res.json(.created, invite); } }; - -pub fn get(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void { - const host = args.get("host") orelse return error.NotFound; - var api = try utils.getApiConn(srv, ctx); - defer api.close(); - - const invite = (try api.getCommunity(host)) orelse return utils.respondError(ctx, .not_found, "Community not found"); - - try utils.respondJson(ctx, .ok, invite); -} diff --git a/src/main/controllers/invites.zig b/src/main/controllers/invites.zig index 4084a68..0355bcf 100644 --- a/src/main/controllers/invites.zig +++ b/src/main/controllers/invites.zig @@ -1,15 +1,16 @@ const api = @import("api"); pub const create = struct { - pub const path = "/invites"; pub const method = .POST; - pub const Body = api.InviteRequest; + pub const path = "/invites"; - pub fn handler(req: anytype, res: anytype, srv: api.ApiSource.Conn) !void { + pub const Body = api.InviteOptions; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { // No need to free because it will be freed when the api conn // is closed - const invite = srv.createInvite(req.body); + const invite = try srv.createInvite(req.body); - try res.writeJson(.created, invite); + try res.json(.created, invite); } }; diff --git a/src/main/controllers/notes.zig b/src/main/controllers/notes.zig index edc59dc..11da65c 100644 --- a/src/main/controllers/notes.zig +++ b/src/main/controllers/notes.zig @@ -1,40 +1,32 @@ -const root = @import("root"); -const http = @import("http"); -const Uuid = @import("util").Uuid; - -const utils = @import("../controllers.zig").utils; -const NoteCreateInfo = @import("api").NoteCreateInfo; - -const RequestServer = root.RequestServer; -const RouteArgs = http.RouteArgs; +const api = @import("api"); +const util = @import("util"); pub const create = struct { pub const method = .POST; pub const path = "/notes"; - pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { - const info = try utils.parseRequestBody(struct { content: []const u8 }, ctx); - defer utils.freeRequestBody(info, ctx.alloc); - var api = try utils.getApiConn(srv, ctx); - defer api.close(); + pub const Body = struct { + content: []const u8, + }; - const note = try api.createNote(info.content); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const note = try srv.createNote(req.body.content); - try utils.respondJson(ctx, .created, note); + try res.json(.created, note); } }; pub const get = struct { pub const method = .GET; pub const path = "/notes/:id"; - pub fn handler(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void { - const id_str = args.get("id") orelse return error.NotFound; - const id = Uuid.parse(id_str) catch return utils.respondError(ctx, .bad_request, "Invalid UUID"); - var api = try utils.getApiConn(srv, ctx); - defer api.close(); - const note = try api.getNote(id); + pub const Args = struct { + id: util.Uuid, + }; - try utils.respondJson(ctx, .ok, note); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const note = try srv.getNote(req.args.id); + + try res.json(.ok, note); } }; diff --git a/src/main/controllers/notes/reacts.zig b/src/main/controllers/notes/reacts.zig deleted file mode 100644 index 0d13b8a..0000000 --- a/src/main/controllers/notes/reacts.zig +++ /dev/null @@ -1,30 +0,0 @@ -const root = @import("root"); -const http = @import("http"); -const Uuid = @import("util").Uuid; - -const utils = @import("../../controllers.zig").utils; - -const RequestServer = root.RequestServer; -const RouteArgs = http.RouteArgs; - -pub fn create(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void { - var api = try utils.getApiConn(srv, ctx); - defer api.close(); - - const note_id = args.get("id") orelse return error.NotFound; - const id = Uuid.parse(note_id) catch return utils.respondError(ctx, .bad_request, "Invalid UUID"); - - try api.react(id); - try utils.respondJson(ctx, .created, .{}); -} - -pub fn list(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void { - var api = try utils.getApiConn(srv, ctx); - defer api.close(); - - const note_id = args.get("id") orelse return error.NotFound; - const id = Uuid.parse(note_id) catch return utils.respondError(ctx, .bad_request, "Invalid UUID"); - - const reacts = try api.listReacts(id); - try utils.respondJson(ctx, .ok, .{ .items = reacts }); -} diff --git a/src/main/controllers/users.zig b/src/main/controllers/users.zig index 239bfb0..fdc683e 100644 --- a/src/main/controllers/users.zig +++ b/src/main/controllers/users.zig @@ -1,29 +1,26 @@ -const std = @import("std"); -const root = @import("root"); -const http = @import("http"); -const Uuid = @import("util").Uuid; - -const RegistrationRequest = @import("api").RegistrationRequest; -const utils = @import("../controllers.zig").utils; - -const RequestServer = root.RequestServer; -const RouteArgs = http.RouteArgs; +const api = @import("api"); pub const create = struct { pub const method = .POST; pub const path = "/users"; - pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { - const info = try utils.parseRequestBody(RegistrationRequest, ctx); - defer utils.freeRequestBody(info, ctx.alloc); - var api = try utils.getApiConn(srv, ctx); - defer api.close(); + pub const Body = struct { + username: []const u8, + password: []const u8, + invite_code: ?[]const u8 = null, + email: ?[]const u8 = null, + }; - const user = api.register(info) catch |err| switch (err) { - error.UsernameTaken => return utils.respondError(ctx, .bad_request, "Username Unavailable"), + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const options = .{ + .invite_code = req.body.invite_code, + .email = req.body.email, + }; + const user = srv.register(req.body.username, req.body.password, options) catch |err| switch (err) { + error.UsernameTaken => return res.err(.unprocessable_entity, "Username Unavailable", {}), else => return err, }; - try utils.respondJson(ctx, .created, user); + try res.json(.created, user); } }; diff --git a/src/main/main.zig b/src/main/main.zig index 4eb0a71..dc96f3c 100644 --- a/src/main/main.zig +++ b/src/main/main.zig @@ -26,16 +26,13 @@ pub const RequestServer = struct { defer srv.shutdown(); while (true) { - const buf = try self.alloc.alloc(u8, 1 << 28); // 4mb - defer self.alloc.free(buf); - var fba = std.heap.FixedBufferAllocator.init(buf); - const alloc = fba.allocator(); + var arena = std.heap.ArenaAllocator.init(self.alloc); + defer arena.deinit(); - var ctx = try srv.accept(alloc); + var ctx = try srv.accept(arena.allocator()); defer ctx.close(); - _ = c.Context(c.sample_api).matchAndHandle(self.api, ctx, self.alloc); - if (true) continue; + c.routeRequest(self.api, ctx, arena.allocator()); } } };