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.context.userId() == 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.value, .{}); 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.value, .{}); 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, }); } }; 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"); const file_tmpl = @embedFile("./web/drive/file.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); } // TODO: put this into the db layer const FileClass = enum { image, video, audio, other, }; 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", }), .file => |file| try res.template(.ok, srv, file_tmpl, .{ .file = file, .breadcrumbs = breadcrumbs.items, .mount_path = req.mount_path, .base_drive_path = "drive", .class = if (std.mem.eql(u8, file.meta.content_type orelse "", "image/jpeg")) FileClass.image else FileClass.other, }), } } 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, delete, upload, }; pub const body_tag_from_query_param = "action"; pub const Body = union(Action) { mkdir: struct { name: []const u8, }, delete: struct {}, upload: struct { file: http.FormFile, sensitive: bool = false, description: []const u8 = "", }, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const trimmed_path = std.mem.trim(u8, req.args.path, "/"); switch (req.body) { .mkdir => |body| { _ = try srv.driveMkdir(req.args.path, body.name); // TODO try servePage(req, res, srv); }, .delete => { _ = try srv.driveDelete(trimmed_path); const dir = trimmed_path[0 .. std.mem.lastIndexOfScalar(u8, trimmed_path, '/') orelse trimmed_path.len]; const url = try std.fmt.allocPrint(srv.allocator, "{s}/drive/{s}", .{ req.mount_path, dir, }); defer srv.allocator.free(url); try res.headers.put("Location", url); return res.status(.see_other); }, .upload => |body| { const entry = try srv.driveUpload( .{ .filename = body.file.filename, .dir = trimmed_path, .description = body.description, .content_type = body.file.content_type, .sensitive = body.sensitive, }, body.file.data, ); defer util.deepFree(srv.allocator, entry); const url = try std.fmt.allocPrint(srv.allocator, "{s}/drive/{s}", .{ req.mount_path, std.mem.trim(u8, entry.file.path, "/"), }); defer srv.allocator.free(url); try res.headers.put("Location", url); return res.status(.see_other); }, } } }; }; 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"), .{ .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(); } };