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");
};
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 => {

View File

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

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

View File

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

View File

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

View File

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

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

View File

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