const std = @import("std"); const util = @import("util"); const http = @import("http"); const controllers = @import("../controllers.zig"); pub const routes = .{ controllers.apiEndpoint(index), controllers.apiEndpoint(about), controllers.apiEndpoint(login), controllers.apiEndpoint(global_timeline), controllers.apiEndpoint(user_details), controllers.apiEndpoint(media), controllers.apiEndpoint(static), controllers.apiEndpoint(signup.page), controllers.apiEndpoint(signup.with_invite), controllers.apiEndpoint(signup.submit), controllers.apiEndpoint(cluster.overview), controllers.apiEndpoint(cluster.communities.create.page), controllers.apiEndpoint(cluster.communities.create.submit), controllers.apiEndpoint(drive.details), controllers.apiEndpoint(drive.form), }; const static = struct { pub const path = "/static/:path*"; pub const method = .GET; pub const Args = struct { path: []const u8, }; pub fn handler(req: anytype, res: anytype, _: anytype) !void { if (std.mem.indexOf(u8, req.args.path, "..") != null) return error.NotFound; std.log.debug("opening {s}", .{req.args.path}); var dir = try std.fs.cwd().openDir("static", .{}); defer dir.close(); var file = dir.openFile(req.args.path, .{}) catch |err| switch (err) { error.FileNotFound => return error.NotFound, else => |e| return e, }; defer file.close(); var stream = try res.open(.ok); defer stream.close(); var buf: [1 << 16]u8 = undefined; while (true) { const count = try file.reader().readAll(&buf); if (count == 0) break; std.log.debug("writing {} bytes from {s}", .{ count, req.args.path }); try stream.writer().writeAll(buf[0..count]); } try stream.finish(); } }; const index = struct { pub const path = "/"; pub const method = .GET; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { if (srv.user_id == null) { try res.headers.put("Location", about.path); return res.status(.see_other); } try res.template(.ok, srv, "Hello", .{}); } }; const about = struct { pub const path = "/about"; pub const method = .GET; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { try res.template(.ok, srv, tmpl, .{}); } const tmpl = @embedFile("./web/about.tmpl.html"); }; const login = struct { pub const path = "/login"; pub const method = .POST; pub const Body = struct { username: []const u8, password: []const u8, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const token = try srv.login(req.body.username, req.body.password); try res.headers.put("Location", index.path); var buf: [64]u8 = undefined; const cookie_name = try std.fmt.bufPrint(&buf, "token.{s}", .{req.body.username}); try res.headers.setCookie(cookie_name, token.token, .{}); try res.headers.setCookie("active_account", req.body.username, .{ .HttpOnly = false }); try res.status(.see_other); } }; const signup = struct { const tmpl = @embedFile("./web/signup.tmpl.html"); fn servePage( invite_code: ?[]const u8, error_msg: ?[]const u8, status: http.Status, res: anytype, srv: anytype, ) !void { const invite = if (invite_code) |code| srv.validateInvite(code) catch |err| switch (err) { error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv), else => |e| return e, } else null; defer util.deepFree(srv.allocator, invite); try res.template(status, srv, tmpl, .{ .error_msg = error_msg, .invite = invite, }); } const page = struct { pub const path = "/signup"; pub const method = .GET; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { try servePage(null, null, .ok, res, srv); } }; const with_invite = struct { pub const path = "/invite/:code"; pub const method = .GET; pub const Args = struct { code: []const u8, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { std.log.debug("{s}", .{req.args.code}); try servePage(req.args.code, null, .ok, res, srv); } }; const submit = struct { pub const path = "/signup"; pub const method = .POST; pub const Body = struct { username: []const u8, password: []const u8, email: ?[]const u8 = null, invite_code: ?[]const u8 = null, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const user = srv.register(req.body.username, req.body.password, .{ .email = req.body.email, .invite_code = req.body.invite_code, }) catch |err| { var status: http.Status = .bad_request; const err_msg = switch (err) { error.UsernameEmpty => "Username cannot be empty", error.UsernameContainsInvalidChar => "Username must be composed of alphanumeric characters and underscore", error.UsernameTooLong => "Username too long", error.PasswordTooShort => "Password too short, must be at least 12 chars", error.UsernameTaken => blk: { status = .unprocessable_entity; break :blk "Username is already registered"; }, else => blk: { status = .internal_server_error; break :blk "an internal error occurred"; }, }; return servePage(req.body.invite_code, err_msg, status, res, srv); }; defer util.deepFree(srv.allocator, user); const token = try srv.login(req.body.username, req.body.password); try res.headers.put("Location", index.path); var buf: [64]u8 = undefined; const cookie_name = try std.fmt.bufPrint(&buf, "token.{s}", .{req.body.username}); try res.headers.setCookie(cookie_name, token.token, .{}); try res.headers.setCookie("active_account", req.body.username, .{ .HttpOnly = false }); try res.status(.see_other); } }; }; const global_timeline = struct { pub const path = "/timelines/global"; pub const method = .GET; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { _ = req; const timeline = try srv.globalTimeline(.{}); try res.template(.ok, srv, @embedFile("./web/timelines/global.tmpl.html"), .{ .notes = timeline.items, .community = srv.community, }); } }; const user_details = struct { pub const path = "/users/:id"; pub const method = .GET; pub const tmpl = @embedFile("./web/user.tmpl.html"); pub const Args = struct { id: util.Uuid, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const user = try srv.getUser(req.args.id); defer util.deepFree(srv.allocator, user); try res.template(.ok, srv, tmpl, user); } }; const drive = struct { const dir_tmpl = @embedFile("./web/drive/directory.tmpl.html"); fn servePage(req: anytype, res: anytype, srv: anytype) !void { const info = try srv.driveGet(req.args.path); defer util.deepFree(srv.allocator, info); var breadcrumbs = std.ArrayList([]const u8).init(srv.allocator); defer breadcrumbs.deinit(); var iter = util.PathIter.from(req.args.path); while (iter.next()) |p| { std.log.debug("breadcrumb: {s}", .{p}); try breadcrumbs.append(if (p.len != 0) p else continue); } switch (info) { .dir => |dir| try res.template(.ok, srv, dir_tmpl, .{ .dir = dir, .breadcrumbs = breadcrumbs.items, .mount_path = req.mount_path, .base_drive_path = "drive", }), else => unreachable, } } const details = struct { pub const path = "/drive/:path*"; pub const method = .GET; pub const Args = struct { path: []const u8, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { try servePage(req, res, srv); } }; const form = struct { pub const path = "/drive/:path*"; pub const method = .POST; pub const Args = struct { path: []const u8, }; const Action = enum { mkdir, }; pub const Body = struct { action: Action, data: union(Action) { mkdir: struct { name: []const u8, }, }, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { if (req.body.action != req.body.data) return error.BadRequest; switch (req.body.data) { .mkdir => |data| { _ = try srv.driveMkdir(req.args.path, data.name); // TODO try servePage(req, res, srv); }, } } }; }; const cluster = struct { const overview = struct { pub const path = "/cluster/overview"; pub const method = .GET; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { const meta = try srv.getClusterMeta(); try res.template(.ok, srv, @embedFile("./web/cluster/overview.tmpl.html"), .{ .community = srv.community, .meta = meta, }); } }; const communities = struct { const create = struct { const tmpl = @embedFile("./web/cluster/community-create.tmpl.html"); const success_tmpl = @embedFile("./web/cluster/community-create-success.tmpl.html"); const page = struct { pub const path = "/cluster/communities/create"; pub const method = .GET; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { try res.template(.ok, srv, tmpl, .{}); } }; const submit = struct { pub const path = "/cluster/communities/create"; pub const method = .POST; pub const Body = struct { origin: []const u8, name: ?[]const u8, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const community = try srv.createCommunity(req.body.origin, req.body.name); defer util.deepFree(srv.allocator, community); const invite = try srv.createInvite(.{ .max_uses = 1, .kind = .community_owner, .to_community = community.id, }); defer util.deepFree(srv.allocator, invite); try res.template(.ok, srv, success_tmpl, .{ .community = community, .invite = invite }); } }; }; }; }; const media = struct { pub const path = "/media/:id"; pub const method = .GET; pub const Args = struct { id: util.Uuid, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const result = try srv.fileDereference(req.args.id); defer util.deepFree(srv.allocator, result); try res.headers.put("Content-Type", result.meta.content_type orelse "application/octet-stream"); var stream = try res.open(.ok); defer stream.close(); try stream.writer().writeAll(result.data); try stream.finish(); } };