aboutsummaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/access.zig6
-rw-r--r--src/routes/api/auth/first-login.zig5
-rw-r--r--src/routes/api/auth/login.zig18
-rw-r--r--src/routes/api/auth/root.zig2
-rw-r--r--src/routes/api/root.zig1
-rw-r--r--src/routes/api/session/current.zig23
-rw-r--r--src/routes/api/session/drop.zig8
-rw-r--r--src/routes/api/session/renew.zig8
-rw-r--r--src/routes/api/session/root.zig5
-rw-r--r--src/routes/context.zig2
-rw-r--r--src/routes/handler-info.zig201
-rw-r--r--src/routes/root.zig45
-rw-r--r--src/routes/static.zig (renamed from src/routes/fallback.zig)5
13 files changed, 251 insertions, 78 deletions
diff --git a/src/routes/access.zig b/src/routes/access.zig
new file mode 100644
index 0000000..39872b8
--- /dev/null
+++ b/src/routes/access.zig
@@ -0,0 +1,6 @@
+
+pub const Access = enum {
+ everyone,
+ users,
+ admins,
+};
diff --git a/src/routes/api/auth/first-login.zig b/src/routes/api/auth/first-login.zig
index 2fb8c02..34f04d8 100644
--- a/src/routes/api/auth/first-login.zig
+++ b/src/routes/api/auth/first-login.zig
@@ -1,14 +1,13 @@
const std = @import("std");
const Context = @import("../../context.zig");
-pub const needs_auth = true;
-pub const method = .POST;
+pub const access = .everyone;
const Result = struct {
is_first: bool,
};
-pub fn handler(ctx: *Context) anyerror!Result {
+pub fn post(ctx: *Context) anyerror!Result {
_ = ctx;
return .{ .is_first = false };
}
diff --git a/src/routes/api/auth/login.zig b/src/routes/api/auth/login.zig
index c3f2bef..76efcf4 100644
--- a/src/routes/api/auth/login.zig
+++ b/src/routes/api/auth/login.zig
@@ -3,9 +3,6 @@ const std = @import("std");
const Context = @import("../../context.zig");
const Storage = @import("../../../storage/root.zig");
-pub const needs_auth = false;
-pub const method = .POST;
-
const Body = struct {
user: []const u8,
password: []const u8,
@@ -15,11 +12,22 @@ const Result = struct {
success: bool,
};
-pub fn handler(ctx: *Context, body: Body) anyerror!Result {
+pub const access = .everyone;
+
+pub fn post(ctx: *Context, body: Body) anyerror!Result {
var user = Storage.User.open(ctx.storage, body.user, ctx.allocator) catch return .{
.success = false
};
defer user.deinit();
- return .{ .success = user.check_password(body.password) };
+ const result = Result {
+ .success = user.check_password(body.password)
+ };
+
+ if (result.success) {
+ const session = try ctx.storage.sessions.add(ctx.storage, user.info);
+ ctx.response.headers.fingerprint = session.fingerprint;
+ }
+
+ return result;
}
diff --git a/src/routes/api/auth/root.zig b/src/routes/api/auth/root.zig
index 785271e..5f45891 100644
--- a/src/routes/api/auth/root.zig
+++ b/src/routes/api/auth/root.zig
@@ -1,4 +1,4 @@
const HandlerInfo = @import("../../handler-info.zig");
pub const login: HandlerInfo = .from_type(@import("login.zig"));
-pub const first_login: HandlerInfo = .from_type(@import("first-login.zig"));
+pub const @"first-login": HandlerInfo = .from_type(@import("first-login.zig"));
diff --git a/src/routes/api/root.zig b/src/routes/api/root.zig
index 19c4074..e3c5d6f 100644
--- a/src/routes/api/root.zig
+++ b/src/routes/api/root.zig
@@ -1 +1,2 @@
pub const auth = @import("auth/root.zig");
+pub const session = @import("session/root.zig");
diff --git a/src/routes/api/session/current.zig b/src/routes/api/session/current.zig
new file mode 100644
index 0000000..31c8476
--- /dev/null
+++ b/src/routes/api/session/current.zig
@@ -0,0 +1,23 @@
+const std = @import("std");
+const Context = @import("../../context.zig");
+
+pub const access = .users;
+
+const Result = struct {
+ name: []const u8,
+ full_name: []const u8,
+ birthday: []const u8,
+};
+
+pub fn get(ctx: *Context) !Result {
+ const session = ctx.storage.sessions.get(
+ ctx.storage,
+ ctx.fingerprint,
+ ) orelse return error.UserDoesNotExist;
+
+ return .{
+ .name = session.info.name,
+ .full_name = session.info.full_name,
+ .birthday = session.info.birthday,
+ };
+}
diff --git a/src/routes/api/session/drop.zig b/src/routes/api/session/drop.zig
new file mode 100644
index 0000000..eacf8ce
--- /dev/null
+++ b/src/routes/api/session/drop.zig
@@ -0,0 +1,8 @@
+const Context = @import("../../context.zig");
+
+pub const access = .users;
+
+pub fn get(ctx: *Context) !void {
+ ctx.storage.sessions.remove(ctx.storage, ctx.fingerprint);
+ ctx.response.headers.fingerprint = "";
+}
diff --git a/src/routes/api/session/renew.zig b/src/routes/api/session/renew.zig
new file mode 100644
index 0000000..7dfa491
--- /dev/null
+++ b/src/routes/api/session/renew.zig
@@ -0,0 +1,8 @@
+const Context = @import("../../context.zig");
+
+pub const access = .users;
+
+pub fn get(ctx: *Context) !void {
+ const new = try ctx.storage.sessions.renew(ctx.storage, ctx.fingerprint);
+ ctx.response.headers.fingerprint = new.fingerprint;
+}
diff --git a/src/routes/api/session/root.zig b/src/routes/api/session/root.zig
new file mode 100644
index 0000000..ea155e5
--- /dev/null
+++ b/src/routes/api/session/root.zig
@@ -0,0 +1,5 @@
+const HandlerInfo = @import("../../handler-info.zig");
+
+pub const current: HandlerInfo = .from_type(@import("current.zig"));
+pub const drop: HandlerInfo = .from_type(@import("renew.zig"));
+pub const renew: HandlerInfo = .from_type(@import("drop.zig"));
diff --git a/src/routes/context.zig b/src/routes/context.zig
index bc3e710..7c0ca4e 100644
--- a/src/routes/context.zig
+++ b/src/routes/context.zig
@@ -6,9 +6,11 @@ const Self = @This();
allocator: std.mem.Allocator,
request: *std.http.Server.Request,
storage: *Storage,
+fingerprint: []const u8,
user: ?[]const u8 = null,
response: struct {
headers: struct {
content_type: []const u8 = "application/json",
+ fingerprint: ?[]const u8 = null,
} = .{},
} = .{},
diff --git a/src/routes/handler-info.zig b/src/routes/handler-info.zig
index bac7612..7b744d2 100644
--- a/src/routes/handler-info.zig
+++ b/src/routes/handler-info.zig
@@ -2,24 +2,65 @@ const std = @import("std");
const Context = @import("context.zig");
const Storage = @import("../storage/root.zig");
+const Access = @import("access.zig").Access;
+
const log = std.log.scoped(.handler_info);
-handler: *const fn (*Context) anyerror![]const u8,
-needs_auth: bool,
-method: std.http.Method,
+const Self = @This();
+
+const Handler = *const fn (*Context) anyerror![]const u8;
+
+get: ?Handler,
+head: ?Handler,
+post: ?Handler,
+put: ?Handler,
+delete: ?Handler,
+connect: ?Handler,
+options: ?Handler,
+trace: ?Handler,
+patch: ?Handler,
+access: Access,
+
+inline fn handler_from_method(self: *const Self, method: std.http.Method) ?Handler {
+ return switch (method) {
+ .GET => self.get,
+ .HEAD => self.head,
+ .POST => self.post,
+ .PUT => self.put,
+ .DELETE => self.delete,
+ .CONNECT => self.connect,
+ .OPTIONS => self.options,
+ .TRACE => self.trace,
+ .PATCH => self.patch,
+ };
+}
+
+inline fn get_fingerprint(cookie: []const u8) []const u8 {
+ const start = std.mem.indexOf(u8, cookie, "fingerprint=") orelse return "";
+ const end = std.mem.indexOf(u8, cookie[start + 12..], " ") orelse return cookie[start + 12..];
+ return cookie[start..end + 1];
+}
pub fn handle(
- self: *const @This(),
+ self: *const Self,
request: *std.http.Server.Request,
storage: *Storage,
allocator: std.mem.Allocator,
) !void {
- if (request.head.method != self.method) {
- return request.respond(
- "{ \"error\": \"Bad Request\" }",
- .{ .status = .bad_request }
- );
- }
+ const handler = self.handler_from_method(request.head.method) orelse return request.respond(
+ "{ \"error\": \"Bad Request\" }",
+ .{ .status = .bad_request }
+ );
+
+ const cookie = cookie: {
+ var iterator = request.iterateHeaders();
+ while (iterator.next()) |header| {
+ if (std.ascii.eqlIgnoreCase(header.name, "cookie")) {
+ break :cookie header.value;
+ }
+ }
+ break :cookie "";
+ };
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
@@ -27,9 +68,28 @@ pub fn handle(
.request = request,
.storage = storage,
.allocator = arena.allocator(),
+ .fingerprint = get_fingerprint(cookie),
};
- const response = self.handler(&context) catch |err| {
+ const allowed = switch (self.access) {
+ .everyone => true,
+ .users => storage.sessions.get(storage, context.fingerprint) != null,
+ .admins => admin: {
+ if (storage.sessions.get(storage, context.fingerprint)) |session| {
+ break :admin session.info.is_admin;
+ }
+ break :admin false;
+ },
+ };
+
+ if (!allowed) {
+ return request.respond(
+ "{ \"error\": \"Forbidden\" }",
+ .{ .status = .forbidden }
+ );
+ }
+
+ const response = handler(&context) catch |err| {
const response, const status_code: std.http.Status = switch (err) {
error.BadRequest => .{ "{ \"error\": \"Bad Request\" }", .bad_request },
error.Unauthorized => .{ "{ \"error\": \"Unauthorized\" }", .unauthorized },
@@ -44,70 +104,91 @@ pub fn handle(
return request.respond(response, .{ .status = status_code });
};
+ var headers: std.ArrayList(std.http.Header) = .empty;
+ defer headers.deinit(allocator);
+
+ try headers.append(allocator, .{
+ .name = "Content-Type",
+ .value = context.response.headers.content_type
+ });
+
+ if (context.response.headers.fingerprint) |auth_token| {
+ var value = std.Io.Writer.Allocating.init(arena.allocator());
+
+ try value.writer.print("fingerprint={s}; Secure; Path=/", .{auth_token});
+
+ try headers.append(allocator, .{
+ .name = "Set-Cookie",
+ .value = value.written(),
+ });
+ }
+
try request.respond(response, .{
- .extra_headers = &.{
- .{
- .name = "Content-Type",
- .value = context.response.headers.content_type
- },
- },
+ .extra_headers = headers.items
});
}
-pub fn from_type(T: type) @This() {
- const info = @typeInfo(@TypeOf(T.handler));
+fn HandlerWrapper(T: type, name: []const u8) type {
+ const info = @typeInfo(@TypeOf(@field(T, name)));
const return_type = info.@"fn".return_type orelse void;
const payload_type = @typeInfo(return_type).error_union.payload;
- const Handler = type: {
- break :type struct {
- pub fn call(ctx: *Context) anyerror![]const u8 {
- const args = args: {
- const tuple = std.meta.fields(std.meta.ArgsTuple(@TypeOf(T.handler)));
+ return struct {
+ pub fn call(ctx: *Context) anyerror![]const u8 {
+ const args = args: {
+ const tuple = std.meta.fields(std.meta.ArgsTuple(@TypeOf(@field(T, name))));
- if (tuple.len == 1) {
- break :args .{ ctx };
- } else if (tuple.len == 2) {
- const Body = tuple[1].@"type";
+ if (tuple.len == 1) {
+ break :args .{ ctx };
+ } else if (tuple.len == 2) {
+ const Body = tuple[1].@"type";
- var writer = std.Io.Writer.Allocating.init(ctx.allocator);
- const interface = &writer.writer;
+ var writer = std.Io.Writer.Allocating.init(ctx.allocator);
+ const interface = &writer.writer;
- var buffer: [1024]u8 = undefined;
- const reader = try ctx.request.readerExpectContinue(&buffer);
+ var buffer: [1024]u8 = undefined;
+ const reader = try ctx.request.readerExpectContinue(&buffer);
- try reader.streamExact64(interface, ctx.request.head.content_length orelse 0);
+ try reader.streamExact64(interface, ctx.request.head.content_length orelse 0);
- const body = std.json.parseFromSliceLeaky(
- Body,
- ctx.allocator,
- writer.written(),
- .{}
- ) catch return error.BadRequest;
- break :args .{ ctx, body };
- } else {
- @compileError("invalid amount of arguments for request function");
- }
- };
-
- if (payload_type == []const u8) {
- return @call(.auto, T.handler, args);
- } else if (payload_type == void) {
- try @call(.auto, T.handler, args);
- return "";
- } else {
- var writer = std.Io.Writer.Allocating.init(ctx.allocator);
- var stringify = std.json.Stringify { .writer = &writer.writer };
- try stringify.write(try @call(.auto, T.handler, args));
- return writer.written();
- }
+ const body = std.json.parseFromSliceLeaky(
+ Body,
+ ctx.allocator,
+ writer.written(),
+ .{}
+ ) catch return error.BadRequest;
+ break :args .{ ctx, body };
+ } else {
+ @compileError("invalid amount of arguments for request function");
}
};
+
+ if (payload_type == []const u8) {
+ return @call(.auto, @field(T, name), args);
+ } else if (payload_type == void) {
+ try @call(.auto, @field(T, name), args);
+ return "";
+ } else {
+ var writer = std.Io.Writer.Allocating.init(ctx.allocator);
+ var stringify = std.json.Stringify { .writer = &writer.writer };
+ try stringify.write(try @call(.auto, @field(T, name), args));
+ return writer.written();
+ }
+ }
};
+}
- return @This() {
- .handler = Handler.call,
- .needs_auth = T.needs_auth,
- .method = T.method,
+pub fn from_type(T: type) @This() {
+ return Self {
+ .get = if (@hasDecl(T, "get")) HandlerWrapper(T, "get").call else null,
+ .head = if (@hasDecl(T, "head")) HandlerWrapper(T, "head").call else null,
+ .post = if (@hasDecl(T, "post")) HandlerWrapper(T, "post").call else null,
+ .put = if (@hasDecl(T, "put")) HandlerWrapper(T, "put").call else null,
+ .delete = if (@hasDecl(T, "delete")) HandlerWrapper(T, "delete").call else null,
+ .connect = if (@hasDecl(T, "connect")) HandlerWrapper(T, "connect").call else null,
+ .options = if (@hasDecl(T, "options")) HandlerWrapper(T, "options").call else null,
+ .trace = if (@hasDecl(T, "trace")) HandlerWrapper(T, "trace").call else null,
+ .patch = if (@hasDecl(T, "patch")) HandlerWrapper(T, "patch").call else null,
+ .access = T.access,
};
}
diff --git a/src/routes/root.zig b/src/routes/root.zig
index dbfce32..878b9f2 100644
--- a/src/routes/root.zig
+++ b/src/routes/root.zig
@@ -4,14 +4,47 @@ pub const HandlerInfo = @import("handler-info.zig");
pub const Context = @import("context.zig");
pub const api = @import("api/root.zig");
-pub const fallback: HandlerInfo = .from_type(@import("fallback.zig"));
+pub const static: HandlerInfo = .from_type(@import("static.zig"));
-pub const handlers = std.StaticStringMap(HandlerInfo).initComptime(.{
- .{ "", fallback },
- .{ "/api/auth/login", api.auth.login },
- .{ "/api/auth/first-login", api.auth.first_login },
-});
+const routes = (Routes {})
+ .with("", static)
+ .with_module("/api", api);
+
+pub const handlers = std.StaticStringMap(HandlerInfo).initComptime(routes.items);
pub fn get(path: []const u8) HandlerInfo {
return (handlers.getLongestPrefix(std.mem.trimEnd(u8, path, "/")) orelse unreachable).value;
}
+
+const Routes = struct {
+ const Self = @This();
+
+ const Route = struct{ []const u8, HandlerInfo };
+
+ items: []const Route = &[0]Route{},
+
+ pub fn with(self: Self, comptime route: []const u8, handler: HandlerInfo) Self {
+ var next = self;
+ const tail: []const Route = &[1]Route { .{ route, handler } };
+ next.items = self.items ++ tail;
+ return next;
+ }
+
+ pub fn with_module(self: Self, comptime prefix: []const u8, module: type) Self {
+ var next = self;
+
+ if (@typeInfo(module) != .@"struct") return self;
+
+ inline for (@typeInfo(module).@"struct".decls) |decl| {
+ const field = @field(module, decl.name);
+ const route = prefix ++ "/" ++ decl.name;
+ switch (@TypeOf(field)) {
+ type => next = next.with_module(route, field),
+ HandlerInfo => next = next.with(route, field),
+ else => {},
+ }
+ }
+
+ return next;
+ }
+};
diff --git a/src/routes/fallback.zig b/src/routes/static.zig
index 25e1927..8719cc9 100644
--- a/src/routes/fallback.zig
+++ b/src/routes/static.zig
@@ -4,10 +4,9 @@ const Context = @import("context.zig");
const log = std.log.scoped(.fallback);
-pub const needs_auth = false;
-pub const method = .GET;
+pub const access = .everyone;
-pub fn handler(ctx: *Context) anyerror![]const u8 {
+pub fn get(ctx: *Context) anyerror![]const u8 {
var static = try std.fs.cwd().openDir("static", .{});
defer static.close();