Use new controller layout

This commit is contained in:
jaina heartles 2022-10-10 20:28:23 -07:00
parent 228b9490ef
commit add17c0a0a
10 changed files with 110 additions and 205 deletions

View file

@ -13,14 +13,12 @@ const services = struct {
const notes = @import("./services/notes.zig"); const notes = @import("./services/notes.zig");
}; };
pub const RegistrationRequest = struct { pub const RegistrationOptions = struct {
username: []const u8, invite_code: ?[]const u8 = null,
password: []const u8,
invite_code: []const u8,
email: ?[]const u8 = null, email: ?[]const u8 = null,
}; };
pub const InviteRequest = struct { pub const InviteOptions = struct {
pub const Kind = services.invites.Kind; pub const Kind = services.invites.Kind;
name: ?[]const u8 = null, name: ?[]const u8 = null,
@ -227,7 +225,7 @@ fn ApiConn(comptime DbConn: type) type {
return community; 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 // Only logged in users can make invites
const user_id = self.user_id orelse return error.TokenRequired; 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()); return try services.invites.get(self.db, invite_id, self.arena.allocator());
} }
pub fn register(self: *Self, request: RegistrationRequest) !UserResponse { pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse {
std.log.debug("registering user {s} with code {s}", .{ request.username, request.invite_code }); std.log.debug("registering user {s} with code {?s}", .{ username, opt.invite_code });
const invite = try services.invites.getByCode(self.db, request.invite_code, self.community.id, self.arena.allocator()); 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 (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.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 (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"); if (self.community.kind == .admin) @panic("Unimplmented");
const user_id = try services.auth.register( const user_id = try services.auth.register(
self.db, self.db,
request.username, username,
request.password, password,
self.community.id, 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(), self.arena.allocator(),
); );
switch (invite.kind) { switch (invite_kind) {
.user => {}, .user => {},
.system => @panic("System user invites unimplemented"), .system => @panic("System user invites unimplemented"),
.community_owner => { .community_owner => {

View file

@ -13,40 +13,26 @@ pub const invites = @import("./controllers/invites.zig");
pub const users = @import("./controllers/users.zig"); pub const users = @import("./controllers/users.zig");
pub const notes = @import("./controllers/notes.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? // TODO: hashmaps?
inline for (routes) |route| { 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 }; const routes = .{
auth.login,
pub const sample_api = struct { auth.verify_login,
const Self = @This(); communities.create,
invites.create,
pub const method = .POST; users.create,
pub const path = "/notes/:id/reacts"; notes.create,
pub const content_type = "application/json"; //notes.get,
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);
}
}; };
pub fn Context(comptime Route: type) type { pub fn Context(comptime Route: type) type {
@ -74,7 +60,7 @@ pub fn Context(comptime Route: type) type {
query: Query, query: Query,
fn parseArgs(path: []const u8) ?Args { fn parseArgs(path: []const u8) ?Args {
var args: Route.Args = undefined; var args: Args = undefined;
var path_iter = util.PathIter.from(path); var path_iter = util.PathIter.from(path);
comptime var route_iter = util.PathIter.from(Route.path); comptime var route_iter = util.PathIter.from(Route.path);
inline while (comptime route_iter.next()) |route_segment| { inline while (comptime route_iter.next()) |route_segment| {
@ -93,7 +79,7 @@ pub fn Context(comptime Route: type) type {
const req = ctx.request; const req = ctx.request;
if (req.method != Route.method) return false; if (req.method != Route.method) return false;
var path = std.mem.sliceTo(std.mem.sliceTo(req.path, '#'), '?'); 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 }; var response = Response{ .headers = http.Headers.init(alloc), .ctx = ctx };
defer response.headers.deinit(); defer response.headers.deinit();
@ -116,7 +102,7 @@ pub fn Context(comptime Route: type) type {
} }
fn errorHandler(response: *Response, status: http.Status) void { 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 { 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 var api_conn = self.getApiConn(api_source) catch return errorHandler(response, .internal_server_error); // TODO
defer api_conn.close(); defer api_conn.close();
self.handle(response, api_conn); self.handle(response, &api_conn);
} }
fn parseBody(self: *Self, req: http.Request) !void { fn parseBody(self: *Self, req: http.Request) !void {
@ -179,16 +165,16 @@ pub const Response = struct {
headers: http.Headers, headers: http.Headers,
ctx: http.server.Context, ctx: http.server.Context,
pub fn writeStatus(self: *Self, status: http.Status) !void { pub fn status(self: *Self, status_code: http.Status) !void {
var stream = try self.ctx.openResponse(&self.headers, status); var stream = try self.ctx.openResponse(&self.headers, status_code);
defer stream.close(); defer stream.close();
try stream.finish(); 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"); 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(); defer stream.close();
const writer = stream.writer(); const writer = stream.writer();
@ -196,6 +182,13 @@ pub const Response = struct {
try stream.finish(); 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) const json_options = if (builtin.mode == .Debug)

View file

@ -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);
}

View file

@ -1,42 +1,28 @@
const std = @import("std"); const api = @import("api");
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;
pub const login = struct { pub const login = struct {
pub const path = "/auth/login";
pub const method = .POST; pub const method = .POST;
pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub const path = "/auth/login";
const credentials = try utils.parseRequestBody(struct { username: []const u8, password: []const u8 }, ctx);
defer utils.freeRequestBody(credentials, ctx.alloc);
var api = try utils.getApiConn(srv, ctx); pub const Body = struct {
defer api.close(); 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 verify_login = struct {
pub const path = "/auth/login";
pub const method = .GET; pub const method = .GET;
pub const path = "/auth/login";
pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void { pub fn handler(_: anytype, res: anytype, srv: anytype) !void {
var api = try utils.getApiConn(srv, ctx); const info = try srv.verifyAuthorization();
defer api.close();
// The self-hosted compiler doesn't like inferring this error set. try res.json(.ok, info);
// do this for now
const info = try api.verifyAuthorization();
try utils.respondJson(ctx, .ok, info);
} }
}; };

View file

@ -1,33 +1,16 @@
const root = @import("root"); const api = @import("api");
const http = @import("http");
const utils = @import("../controllers.zig").utils;
const RequestServer = root.RequestServer;
const RouteArgs = http.RouteArgs;
pub const create = struct { pub const create = struct {
pub const method = .POST; pub const method = .POST;
pub const path = "/communities"; 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); pub const Body = struct {
defer api.close(); 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);
}

View file

@ -1,15 +1,16 @@
const api = @import("api"); const api = @import("api");
pub const create = struct { pub const create = struct {
pub const path = "/invites";
pub const method = .POST; 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 // No need to free because it will be freed when the api conn
// is closed // 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);
} }
}; };

View file

@ -1,40 +1,32 @@
const root = @import("root"); const api = @import("api");
const http = @import("http"); const util = @import("util");
const Uuid = @import("util").Uuid;
const utils = @import("../controllers.zig").utils;
const NoteCreateInfo = @import("api").NoteCreateInfo;
const RequestServer = root.RequestServer;
const RouteArgs = http.RouteArgs;
pub const create = struct { pub const create = struct {
pub const method = .POST; pub const method = .POST;
pub const path = "/notes"; 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); pub const Body = struct {
defer api.close(); 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 get = struct {
pub const method = .GET; pub const method = .GET;
pub const path = "/notes/:id"; 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);
} }
}; };

View file

@ -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 });
}

View file

@ -1,29 +1,26 @@
const std = @import("std"); const api = @import("api");
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;
pub const create = struct { pub const create = struct {
pub const method = .POST; pub const method = .POST;
pub const path = "/users"; 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); pub const Body = struct {
defer api.close(); username: []const u8,
password: []const u8,
invite_code: ?[]const u8 = null,
email: ?[]const u8 = null,
};
const user = api.register(info) catch |err| switch (err) { pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
error.UsernameTaken => return utils.respondError(ctx, .bad_request, "Username Unavailable"), 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, else => return err,
}; };
try utils.respondJson(ctx, .created, user); try res.json(.created, user);
} }
}; };

View file

@ -26,16 +26,13 @@ pub const RequestServer = struct {
defer srv.shutdown(); defer srv.shutdown();
while (true) { while (true) {
const buf = try self.alloc.alloc(u8, 1 << 28); // 4mb var arena = std.heap.ArenaAllocator.init(self.alloc);
defer self.alloc.free(buf); defer arena.deinit();
var fba = std.heap.FixedBufferAllocator.init(buf);
const alloc = fba.allocator();
var ctx = try srv.accept(alloc); var ctx = try srv.accept(arena.allocator());
defer ctx.close(); defer ctx.close();
_ = c.Context(c.sample_api).matchAndHandle(self.api, ctx, self.alloc); c.routeRequest(self.api, ctx, arena.allocator());
if (true) continue;
} }
} }
}; };