fediglam/src/main/controllers/web.zig

383 lines
12 KiB
Zig

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 {
mkcol,
};
pub const Body = struct {
action: Action,
data: union(Action) {
mkcol: 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) {
.mkcol => |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();
}
};