From 4c661672c260e1e6188efa9f4e403f272d9a7470 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 19 Dec 2022 04:31:18 -0800 Subject: [PATCH 01/22] parseToken --- src/http/request/parser.zig | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/http/request/parser.zig b/src/http/request/parser.zig index a11c315..54ca6bd 100644 --- a/src/http/request/parser.zig +++ b/src/http/request/parser.zig @@ -265,3 +265,87 @@ fn parseEncoding(encoding: ?[]const u8) !Encoding { if (std.mem.eql(u8, encoding.?, "chunked")) return .chunked; return error.UnsupportedMediaType; } + +fn isTokenChar(ch: u8) bool { + switch (ch) { + '"', '(', ')', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' => return false, + + '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~' => return true, + else => return std.ascii.isAlphanumeric(ch), + } +} + +fn parseToken(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { + var data = std.ArrayList(u8).init(alloc); + errdefer data.deinit(); + + const reader = peek_stream.reader(); + while (reader.readByte()) |ch| { + if (!isTokenChar(ch)) { + try peek_stream.putBackByte(ch); + break; + } + try data.append(ch); + } else |err| if (err != error.EndOfStream) return err; + + return data.toOwnedSlice(); +} + +test "parseToken" { + const testCase = struct { + fn func(data: []const u8, err: ?anyerror, expected: anyerror![]const u8, remaining: []const u8) !void { + var fbs = std.io.fixedBufferStream(data); + var stream = errorReader(err orelse error.EndOfStream, fbs.reader()); + var peeker = std.io.peekStream(1, stream.reader()); + + const result = parseToken(std.testing.allocator, &peeker); + defer if (result) |v| std.testing.allocator.free(v) else |_| {}; + + if (expected) |val| + try std.testing.expectEqualStrings(val, try result) + else |expected_err| + try std.testing.expectError(expected_err, result); + + try std.testing.expect(try peeker.reader().isBytes(remaining)); + try std.testing.expectError(err orelse error.EndOfStream, peeker.reader().readByte()); + } + }.func; + + try testCase("abcdefg", null, "abcdefg", ""); + try testCase("abc defg", null, "abc", " defg"); + try testCase("abc;defg", null, "abc", ";defg"); + try testCase("abc%defg$; ", null, "abc%defg$", "; "); + + try testCase(" ", null, "", " "); + try testCase(";", null, "", ";"); + + try testCase("abcdefg", error.ClosedPipe, error.ClosedPipe, ""); +} + +fn ErrorReader(comptime E: type, comptime ReaderType: type) type { + return struct { + inner_reader: ReaderType, + err: E, + + pub const Error = ReaderType.Error || E; + pub const Reader = std.io.Reader(*@This(), Error, read); + + pub fn read(self: *@This(), dest: []u8) Error!usize { + const count = try self.inner_reader.readAll(dest); + if (count == 0) return self.err; + return dest.len; + } + + pub fn reader(self: *@This()) Reader { + return .{ .context = self }; + } + }; +} + +/// Returns the given error after the underlying stream is finished +fn errorReader(err: anytype, reader: anytype) ErrorReader(@TypeOf(err), @TypeOf(reader)) { + return .{ + .inner_reader = reader, + .err = err, + }; +} From 7f689c70303fa22bb65e8e3be60f199ddcd6e90c Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 19 Dec 2022 05:41:35 -0800 Subject: [PATCH 02/22] parseQuotedString --- src/http/request/parser.zig | 130 ++++++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 5 deletions(-) diff --git a/src/http/request/parser.zig b/src/http/request/parser.zig index 54ca6bd..f10b72d 100644 --- a/src/http/request/parser.zig +++ b/src/http/request/parser.zig @@ -275,6 +275,76 @@ fn isTokenChar(ch: u8) bool { } } +// Parses a quoted-string (rfc 9110) off the stream. Backslash-tokens are unescaped. +// The caller takes responsibility for deallocating the memory returned. +fn parseQuotedString(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { + const reader = peek_stream.reader(); + + var data = std.ArrayList(u8).init(alloc); + errdefer data.deinit(); + + { + const start = try reader.readByte(); + if (start != '"') { + try peek_stream.putBackByte(start); + return error.MissingStartQuote; + } + } + + while (true) { + const ch = switch (try reader.readByte()) { + '\t', ' ', '!', 0x23...0x5b, 0x5d...0x7e, 0x80...0xff => |c| c, + + '\\' => switch (try reader.readByte()) { + '\t', ' ', 0x21...0x7e, 0x80...0xff => |c| c, + else => return error.UnexpectedChar, + }, + + '"' => break, + else => return error.UnexpectedChar, + }; + + try data.append(ch); + } + + return data.toOwnedSlice(); +} + +test "parseQuotedString" { + const testCase = struct { + fn func(data: []const u8, stream_error: ?anyerror, expected: anyerror![]const u8, remaining: []const u8) !void { + var fbs = std.io.fixedBufferStream(data); + var stream = errorReader(stream_error orelse error.EndOfStream, fbs.reader()); + var peeker = std.io.peekStream(1, stream.reader()); + + const result = parseQuotedString(std.testing.allocator, &peeker); + defer if (result) |v| std.testing.allocator.free(v) else |_| {}; + + if (expected) |val| + try std.testing.expectEqualStrings(val, try result) + else |expected_err| + try std.testing.expectError(expected_err, result); + + try std.testing.expect(try peeker.reader().isBytes(remaining)); + try std.testing.expectError(stream_error orelse error.EndOfStream, peeker.reader().readByte()); + } + }.func; + + try testCase("\"abcdefg\"", null, "abcdefg", ""); + try testCase("\"abcdefg\"abcd", null, "abcdefg", "abcd"); + try testCase("\"xyz\\\"z\"", null, "xyz\"z", ""); + try testCase("\"xyz\\\\z\"", null, "xyz\\z", ""); + try testCase("\"💯\"", null, "💯", ""); + + try testCase("abcdefg\"abcd", null, error.MissingStartQuote, "abcdefg\"abcd"); + try testCase("\"abcdefg", null, error.EndOfStream, ""); + + try testCase("\"abcdefg", error.ClosedPipe, error.ClosedPipe, ""); +} + +// Attempts to parse a token (rfc 9110) off the stream. It stops at the first non-token +// char. Said char reamins on the stream. If the token is empty, returns error.EmptyToken; +// The caller takes responsibility for deallocating the memory returned. fn parseToken(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { var data = std.ArrayList(u8).init(alloc); errdefer data.deinit(); @@ -288,14 +358,16 @@ fn parseToken(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { try data.append(ch); } else |err| if (err != error.EndOfStream) return err; + if (data.items.len == 0) return error.EmptyToken; + return data.toOwnedSlice(); } test "parseToken" { const testCase = struct { - fn func(data: []const u8, err: ?anyerror, expected: anyerror![]const u8, remaining: []const u8) !void { + fn func(data: []const u8, stream_error: ?anyerror, expected: anyerror![]const u8, remaining: []const u8) !void { var fbs = std.io.fixedBufferStream(data); - var stream = errorReader(err orelse error.EndOfStream, fbs.reader()); + var stream = errorReader(stream_error orelse error.EndOfStream, fbs.reader()); var peeker = std.io.peekStream(1, stream.reader()); const result = parseToken(std.testing.allocator, &peeker); @@ -307,7 +379,7 @@ test "parseToken" { try std.testing.expectError(expected_err, result); try std.testing.expect(try peeker.reader().isBytes(remaining)); - try std.testing.expectError(err orelse error.EndOfStream, peeker.reader().readByte()); + try std.testing.expectError(stream_error orelse error.EndOfStream, peeker.reader().readByte()); } }.func; @@ -316,12 +388,60 @@ test "parseToken" { try testCase("abc;defg", null, "abc", ";defg"); try testCase("abc%defg$; ", null, "abc%defg$", "; "); - try testCase(" ", null, "", " "); - try testCase(";", null, "", ";"); + try testCase(" ", null, error.EmptyToken, " "); + try testCase(";", null, error.EmptyToken, ";"); try testCase("abcdefg", error.ClosedPipe, error.ClosedPipe, ""); } +// Parses a token or quoted string (rfc 9110) off the stream, as appropriate. +// The caller takes responsibility for deallocating the memory returned. +fn parseTokenOrQuotedString(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { + return parseToken(alloc, peek_stream) catch |err| switch (err) { + error.EmptyToken => return try parseQuotedString(alloc, peek_stream), + else => |e| return e, + }; +} + +test "parseTokenOrQuotedString" { + const testCase = struct { + fn func(data: []const u8, stream_error: ?anyerror, expected: anyerror![]const u8, remaining: []const u8) !void { + var fbs = std.io.fixedBufferStream(data); + var stream = errorReader(stream_error orelse error.EndOfStream, fbs.reader()); + var peeker = std.io.peekStream(1, stream.reader()); + + const result = parseTokenOrQuotedString(std.testing.allocator, &peeker); + defer if (result) |v| std.testing.allocator.free(v) else |_| {}; + + if (expected) |val| + try std.testing.expectEqualStrings(val, try result) + else |expected_err| + try std.testing.expectError(expected_err, result); + + try std.testing.expect(try peeker.reader().isBytes(remaining)); + try std.testing.expectError(stream_error orelse error.EndOfStream, peeker.reader().readByte()); + } + }.func; + + try testCase("abcdefg", null, "abcdefg", ""); + try testCase("abc defg", null, "abc", " defg"); + try testCase("abc;defg", null, "abc", ";defg"); + try testCase("abc%defg$; ", null, "abc%defg$", "; "); + + try testCase("\"abcdefg\"", null, "abcdefg", ""); + try testCase("\"abcdefg\"abcd", null, "abcdefg", "abcd"); + try testCase("\"xyz\\\"z\"", null, "xyz\"z", ""); + try testCase("\"xyz\\\\z\"", null, "xyz\\z", ""); + try testCase("\"💯\"", null, "💯", ""); + + try testCase(" ", null, error.MissingStartQuote, " "); + try testCase(";", null, error.MissingStartQuote, ";"); + try testCase("\"abcdefg", null, error.EndOfStream, ""); + + try testCase("abcdefg", error.ClosedPipe, error.ClosedPipe, ""); + try testCase("\"abcdefg", error.ClosedPipe, error.ClosedPipe, ""); +} + fn ErrorReader(comptime E: type, comptime ReaderType: type) type { return struct { inner_reader: ReaderType, From b0db514adceb9869fd1c6cd6299bc8fc1286e092 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 19 Dec 2022 05:42:26 -0800 Subject: [PATCH 03/22] Make helper functions public --- src/http/request/parser.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/http/request/parser.zig b/src/http/request/parser.zig index f10b72d..114b6c0 100644 --- a/src/http/request/parser.zig +++ b/src/http/request/parser.zig @@ -277,7 +277,7 @@ fn isTokenChar(ch: u8) bool { // Parses a quoted-string (rfc 9110) off the stream. Backslash-tokens are unescaped. // The caller takes responsibility for deallocating the memory returned. -fn parseQuotedString(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { +pub fn parseQuotedString(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { const reader = peek_stream.reader(); var data = std.ArrayList(u8).init(alloc); @@ -345,7 +345,7 @@ test "parseQuotedString" { // Attempts to parse a token (rfc 9110) off the stream. It stops at the first non-token // char. Said char reamins on the stream. If the token is empty, returns error.EmptyToken; // The caller takes responsibility for deallocating the memory returned. -fn parseToken(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { +pub fn parseToken(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { var data = std.ArrayList(u8).init(alloc); errdefer data.deinit(); @@ -396,7 +396,7 @@ test "parseToken" { // Parses a token or quoted string (rfc 9110) off the stream, as appropriate. // The caller takes responsibility for deallocating the memory returned. -fn parseTokenOrQuotedString(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { +pub fn parseTokenOrQuotedString(alloc: std.mem.Allocator, peek_stream: anytype) ![]const u8 { return parseToken(alloc, peek_stream) catch |err| switch (err) { error.EmptyToken => return try parseQuotedString(alloc, peek_stream), else => |e| return e, From d8e4d6c82b9254f819585b58d1e0bd4e3f833eb4 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Wed, 21 Dec 2022 00:57:36 -0800 Subject: [PATCH 04/22] comptime dependency injection for ApiConn --- src/api/lib.zig | 104 ++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 0b37618..cfc7b45 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -265,7 +265,7 @@ pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password pub const ApiSource = struct { db_conn_pool: *sql.ConnPool, - pub const Conn = ApiConn(sql.Db); + pub const Conn = ApiConn(sql.Db, services); const root_username = "root"; @@ -310,14 +310,14 @@ pub const ApiSource = struct { } }; -fn ApiConn(comptime DbConn: type) type { +fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return struct { const Self = @This(); db: DbConn, - token_info: ?services.auth.TokenInfo = null, + token_info: ?models.auth.TokenInfo = null, user_id: ?Uuid = null, - community: services.communities.Community, + community: models.communities.Community, allocator: std.mem.Allocator, pub fn close(self: *Self) void { @@ -332,7 +332,7 @@ fn ApiConn(comptime DbConn: type) type { } pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResponse { - return services.auth.login( + return models.auth.login( self.db, username, self.community.id, @@ -351,7 +351,7 @@ fn ApiConn(comptime DbConn: type) type { }; pub fn verifyAuthorization(self: *Self) !AuthorizationInfo { if (self.token_info) |info| { - const user = try services.actors.get(self.db, info.user_id, self.allocator); + const user = try models.actors.get(self.db, info.user_id, self.allocator); defer util.deepFree(self.allocator, user); const username = try util.deepClone(self.allocator, user.username); @@ -370,21 +370,21 @@ fn ApiConn(comptime DbConn: type) type { return error.TokenRequired; } - pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !services.communities.Community { + pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !models.communities.Community { if (!self.isAdmin()) { return error.PermissionDenied; } const tx = try self.db.begin(); errdefer tx.rollback(); - const community_id = try services.communities.create( + const community_id = try models.communities.create( tx, origin, .{ .name = name }, self.allocator, ); - const community = services.communities.get( + const community = models.communities.get( tx, community_id, self.allocator, @@ -412,18 +412,18 @@ fn ApiConn(comptime DbConn: type) type { // Users can only make user invites if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied; - const invite_id = try services.invites.create(self.db, user_id, community_id, .{ + const invite_id = try models.invites.create(self.db, user_id, community_id, .{ .name = options.name, .lifespan = options.lifespan, .max_uses = options.max_uses, .kind = options.kind, }, self.allocator); - const invite = try services.invites.get(self.db, invite_id, self.allocator); + const invite = try models.invites.get(self.db, invite_id, self.allocator); errdefer util.deepFree(self.allocator, invite); const url = if (options.to_community) |cid| blk: { - const community = try services.communities.get(self.db, cid, self.allocator); + const community = try models.communities.get(self.db, cid, self.allocator); defer util.deepFree(self.allocator, community); break :blk try std.fmt.allocPrint( @@ -454,7 +454,7 @@ fn ApiConn(comptime DbConn: type) type { }; } - fn isInviteValid(invite: services.invites.Invite) bool { + fn isInviteValid(invite: models.invites.Invite) bool { if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false; if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false; return true; @@ -463,7 +463,7 @@ fn ApiConn(comptime DbConn: type) type { pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse { const tx = try self.db.beginOrSavepoint(); const maybe_invite = if (opt.invite_code) |code| - try services.invites.getByCode(tx, code, self.community.id, self.allocator) + try models.invites.getByCode(tx, code, self.community.id, self.allocator) else null; defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); @@ -477,7 +477,7 @@ fn ApiConn(comptime DbConn: type) type { if (self.community.kind == .admin) @panic("Unimplmented"); - const user_id = try services.auth.register( + const user_id = try models.auth.register( tx, username, password, @@ -493,7 +493,7 @@ fn ApiConn(comptime DbConn: type) type { .user => {}, .system => @panic("System user invites unimplemented"), .community_owner => { - try services.communities.transferOwnership(tx, self.community.id, user_id); + try models.communities.transferOwnership(tx, self.community.id, user_id); }, } @@ -508,7 +508,7 @@ fn ApiConn(comptime DbConn: type) type { } fn getUserUnchecked(self: *Self, db: anytype, user_id: Uuid) !UserResponse { - const user = try services.actors.get(db, user_id, self.allocator); + const user = try models.actors.get(db, user_id, self.allocator); const avatar_url = if (user.avatar_file_id) |fid| try std.fmt.allocPrint( @@ -573,7 +573,7 @@ fn ApiConn(comptime DbConn: type) type { // Only authenticated users can post const user_id = self.user_id orelse return error.TokenRequired; - const note_id = try services.notes.create(self.db, user_id, content, self.allocator); + const note_id = try models.notes.create(self.db, user_id, content, self.allocator); return self.getNote(note_id) catch |err| switch (err) { error.NotFound => error.Unexpected, @@ -582,8 +582,8 @@ fn ApiConn(comptime DbConn: type) type { } pub fn getNote(self: *Self, note_id: Uuid) !NoteResponse { - const note = try services.notes.get(self.db, note_id, self.allocator); - const user = try services.actors.get(self.db, note.author_id, self.allocator); + const note = try models.notes.get(self.db, note_id, self.allocator); + const user = try models.actors.get(self.db, note.author_id, self.allocator); // Only serve community-specific notes on unauthenticated requests if (self.user_id == null) { @@ -602,14 +602,14 @@ fn ApiConn(comptime DbConn: type) type { }; } - pub fn queryCommunities(self: *Self, args: services.communities.QueryArgs) !CommunityQueryResult { + pub fn queryCommunities(self: *Self, args: models.communities.QueryArgs) !CommunityQueryResult { if (!self.isAdmin()) return error.PermissionDenied; - return try services.communities.query(self.db, args, self.allocator); + return try models.communities.query(self.db, args, self.allocator); } pub fn globalTimeline(self: *Self, args: TimelineArgs) !TimelineResult { const all_args = std.mem.zeroInit(NoteQueryArgs, args); - const result = try services.notes.query(self.db, all_args, self.allocator); + const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ .items = result.items, .prev_page = TimelineArgs.from(result.prev_page), @@ -620,7 +620,7 @@ fn ApiConn(comptime DbConn: type) type { pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult { var all_args = std.mem.zeroInit(NoteQueryArgs, args); all_args.community_id = self.community.id; - const result = try services.notes.query(self.db, all_args, self.allocator); + const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ .items = result.items, .prev_page = TimelineArgs.from(result.prev_page), @@ -631,9 +631,9 @@ fn ApiConn(comptime DbConn: type) type { pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult { if (self.user_id == null) return error.NoToken; - var all_args = std.mem.zeroInit(services.notes.QueryArgs, args); + var all_args = std.mem.zeroInit(models.notes.QueryArgs, args); all_args.followed_by = self.user_id; - const result = try services.notes.query(self.db, all_args, self.allocator); + const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ .items = result.items, .prev_page = TimelineArgs.from(result.prev_page), @@ -642,9 +642,9 @@ fn ApiConn(comptime DbConn: type) type { } pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult { - var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); + var all_args = std.mem.zeroInit(models.follows.QueryArgs, args); all_args.followee_id = user_id; - const result = try services.follows.query(self.db, all_args, self.allocator); + const result = try models.follows.query(self.db, all_args, self.allocator); return FollowerQueryResult{ .items = result.items, .prev_page = FollowQueryArgs.from(result.prev_page), @@ -653,9 +653,9 @@ fn ApiConn(comptime DbConn: type) type { } pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult { - var all_args = std.mem.zeroInit(services.follows.QueryArgs, args); + var all_args = std.mem.zeroInit(models.follows.QueryArgs, args); all_args.followed_by_id = user_id; - const result = try services.follows.query(self.db, all_args, self.allocator); + const result = try models.follows.query(self.db, all_args, self.allocator); return FollowingQueryResult{ .items = result.items, .prev_page = FollowQueryArgs.from(result.prev_page), @@ -664,12 +664,12 @@ fn ApiConn(comptime DbConn: type) type { } pub fn follow(self: *Self, followee: Uuid) !void { - const result = try services.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); + const result = try models.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); defer util.deepFree(self.allocator, result); } pub fn unfollow(self: *Self, followee: Uuid) !void { - const result = try services.follows.delete(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); + const result = try models.follows.delete(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); defer util.deepFree(self.allocator, result); } @@ -690,7 +690,7 @@ fn ApiConn(comptime DbConn: type) type { ); } - fn backendDriveEntryToFrontend(self: *Self, entry: services.drive.Entry, recurse: bool) !DriveEntry { + fn backendDriveEntryToFrontend(self: *Self, entry: models.drive.Entry, recurse: bool) !DriveEntry { return if (entry.file_id) |file_id| .{ .file = .{ .id = entry.id, @@ -699,7 +699,7 @@ fn ApiConn(comptime DbConn: type) type { .path = entry.path, .parent_directory_id = entry.parent_directory_id, - .meta = try services.files.get(self.db, file_id, self.allocator), + .meta = try models.files.get(self.db, file_id, self.allocator), }, } else .{ .dir = .{ @@ -712,7 +712,7 @@ fn ApiConn(comptime DbConn: type) type { .children = blk: { if (!recurse) break :blk null; - const children = try services.drive.list(self.db, entry.id, self.allocator); + const children = try models.drive.list(self.db, entry.id, self.allocator); const result = self.allocator.alloc(DriveEntry, children.len) catch |err| { util.deepFree(self.allocator, children); @@ -741,7 +741,7 @@ fn ApiConn(comptime DbConn: type) type { pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; - const file_id = try services.files.create(self.db, user_id, .{ + const file_id = try models.files.create(self.db, user_id, .{ .filename = meta.filename, .description = meta.description, .content_type = meta.content_type, @@ -749,11 +749,11 @@ fn ApiConn(comptime DbConn: type) type { }, body, self.allocator); const entry = entry: { - errdefer services.files.delete(self.db, file_id, self.allocator) catch |err| { + errdefer models.files.delete(self.db, file_id, self.allocator) catch |err| { std.log.err("Unable to delete file {}: {}", .{ file_id, err }); }; - break :entry services.drive.create( + break :entry models.drive.create( self.db, user_id, meta.dir, @@ -768,7 +768,7 @@ fn ApiConn(comptime DbConn: type) type { const name = split.rest(); const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext }); - break :entry try services.drive.create( + break :entry try models.drive.create( self.db, user_id, meta.dir, @@ -787,63 +787,63 @@ fn ApiConn(comptime DbConn: type) type { pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; - const entry = try services.drive.create(self.db, user_id, parent_path, name, null, self.allocator); + const entry = try models.drive.create(self.db, user_id, parent_path, name, null, self.allocator); errdefer util.deepFree(self.allocator, entry); return try self.backendDriveEntryToFrontend(entry, true); } pub fn driveDelete(self: *Self, path: []const u8) !void { const user_id = self.user_id orelse return error.NoToken; - const entry = try services.drive.stat(self.db, user_id, path, self.allocator); + const entry = try models.drive.stat(self.db, user_id, path, self.allocator); defer util.deepFree(self.allocator, entry); - try services.drive.delete(self.db, entry.id, self.allocator); - if (entry.file_id) |file_id| try services.files.delete(self.db, file_id, self.allocator); + try models.drive.delete(self.db, entry.id, self.allocator); + if (entry.file_id) |file_id| try models.files.delete(self.db, file_id, self.allocator); } pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; - try services.drive.move(self.db, user_id, src, dest, self.allocator); + try models.drive.move(self.db, user_id, src, dest, self.allocator); return try self.driveGet(dest); } pub fn driveGet(self: *Self, path: []const u8) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; - const entry = try services.drive.stat(self.db, user_id, path, self.allocator); + const entry = try models.drive.stat(self.db, user_id, path, self.allocator); errdefer util.deepFree(self.allocator, entry); return try self.backendDriveEntryToFrontend(entry, true); } - pub fn driveUpdate(self: *Self, path: []const u8, meta: services.files.PartialMeta) !DriveEntry { + pub fn driveUpdate(self: *Self, path: []const u8, meta: models.files.PartialMeta) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; std.log.debug("{s}", .{path}); - const entry = try services.drive.stat(self.db, user_id, path, self.allocator); + const entry = try models.drive.stat(self.db, user_id, path, self.allocator); defer util.deepFree(self.allocator, entry); std.log.debug("{}", .{entry.id}); - try services.files.update(self.db, entry.file_id orelse return error.NotAFile, meta, self.allocator); + try models.files.update(self.db, entry.file_id orelse return error.NotAFile, meta, self.allocator); return try self.driveGet(path); } pub fn fileDereference(self: *Self, id: Uuid) !FileResult { - const meta = try services.files.get(self.db, id, self.allocator); + const meta = try models.files.get(self.db, id, self.allocator); errdefer util.deepFree(self.allocator, meta); return FileResult{ .meta = meta, - .data = try services.files.deref(self.allocator, id), + .data = try models.files.deref(self.allocator, id), }; } pub fn updateUserProfile(self: *Self, id: Uuid, data: PartialUserProfile) !void { if (!Uuid.eql(id, self.user_id orelse return error.NoToken)) return error.AccessDenied; - try services.actors.updateProfile(self.db, id, data, self.allocator); + try models.actors.updateProfile(self.db, id, data, self.allocator); } pub fn validateInvite(self: *Self, code: []const u8) !InviteResponse { - const invite = services.invites.getByCode( + const invite = models.invites.getByCode( self.db, code, self.community.id, From f52b82b5066adbf110de75a80d56a2cfe298a522 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Wed, 21 Dec 2022 07:19:13 -0800 Subject: [PATCH 05/22] Move types into shared file --- src/api/lib.zig | 133 ++++++--- src/api/services/actors.zig | 56 +--- src/api/services/auth.zig | 24 +- src/api/services/communities.zig | 91 +----- src/api/services/files.zig | 59 +--- src/api/services/invites.zig | 41 +-- src/api/services/notes.zig | 93 +++--- src/api/services/types.zig | 362 +++++++++++++++++++++++ src/main/controllers/api/communities.zig | 10 +- src/main/controllers/api/users.zig | 2 +- src/main/controllers/web.zig | 4 +- 11 files changed, 533 insertions(+), 342 deletions(-) create mode 100644 src/api/services/types.zig diff --git a/src/api/lib.zig b/src/api/lib.zig index cfc7b45..9da0674 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -18,6 +18,17 @@ const services = struct { const follows = @import("./services/follows.zig"); }; +const types = @import("./services/types.zig"); + +pub const QueryResult = types.QueryResult; + +pub const Account = types.Account; +pub const Actor = types.Actor; +pub const Community = types.Community; +pub const Invite = types.Invite; +pub const Note = types.Note; +pub const Token = types.Token; + pub const ClusterMeta = struct { community_count: usize, user_count: usize, @@ -30,7 +41,7 @@ pub const RegistrationOptions = struct { }; pub const InviteOptions = struct { - pub const Kind = services.invites.Kind; + pub const Kind = Invite.Kind; name: ?[]const u8 = null, lifespan: ?DateTime.Duration = null, @@ -41,9 +52,6 @@ pub const InviteOptions = struct { to_community: ?Uuid = null, }; -pub const LoginResponse = services.auth.LoginResult; - -pub const ProfileField = services.actors.ProfileField; pub const UserResponse = struct { id: Uuid, @@ -59,14 +67,13 @@ pub const UserResponse = struct { header_file_id: ?Uuid, header_url: ?[]const u8, - profile_fields: []const ProfileField, + profile_fields: []const Actor.ProfileField, community_id: Uuid, created_at: DateTime, updated_at: DateTime, }; -pub const PartialUserProfile = services.actors.PartialProfile; pub const NoteResponse = struct { id: Uuid, @@ -80,15 +87,9 @@ pub const NoteResponse = struct { created_at: DateTime, }; -pub const Community = services.communities.Community; -pub const CommunityQueryArgs = services.communities.QueryArgs; -pub const CommunityQueryResult = services.communities.QueryResult; - -pub const NoteQueryArgs = services.notes.QueryArgs; - pub const TimelineArgs = struct { - pub const PageDirection = NoteQueryArgs.PageDirection; - pub const Prev = NoteQueryArgs.Prev; + pub const PageDirection = Note.QueryArgs.PageDirection; + pub const Prev = Note.QueryArgs.Prev; max_items: usize = 20, @@ -99,7 +100,7 @@ pub const TimelineArgs = struct { page_direction: PageDirection = .forward, - fn from(args: NoteQueryArgs) TimelineArgs { + fn from(args: Note.QueryArgs) TimelineArgs { return .{ .max_items = args.max_items, .created_before = args.created_before, @@ -111,7 +112,7 @@ pub const TimelineArgs = struct { }; pub const TimelineResult = struct { - items: []services.notes.NoteDetailed, + items: []Note, prev_page: TimelineArgs, next_page: TimelineArgs, @@ -191,7 +192,7 @@ pub const DriveEntry = union(enum) { }, }; -pub const FileUpload = services.files.FileUpload; +pub const FileUpload = types.FileUpload; pub const DriveGetResult = union(services.drive.Kind) { dir: struct { @@ -205,13 +206,13 @@ pub const DriveGetResult = union(services.drive.Kind) { }; pub const FileResult = struct { - meta: services.files.FileUpload, + meta: FileUpload, data: []const u8, }; pub const InviteResponse = struct { code: []const u8, - kind: services.invites.Kind, + kind: Invite.Kind, name: []const u8, creator: UserResponse, @@ -315,9 +316,9 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { const Self = @This(); db: DbConn, - token_info: ?models.auth.TokenInfo = null, + token_info: ?Token.Info = null, user_id: ?Uuid = null, - community: models.communities.Community, + community: Community, allocator: std.mem.Allocator, pub fn close(self: *Self) void { @@ -331,7 +332,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return self.user_id != null and self.community.kind == .admin; } - pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResponse { + pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { return models.auth.login( self.db, username, @@ -370,7 +371,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return error.TokenRequired; } - pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !models.communities.Community { + pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Community { if (!self.isAdmin()) { return error.PermissionDenied; } @@ -412,8 +413,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { // Users can only make user invites if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied; - const invite_id = try models.invites.create(self.db, user_id, community_id, .{ - .name = options.name, + const invite_id = try models.invites.create(self.db, user_id, community_id, options.name orelse "", .{ .lifespan = options.lifespan, .max_uses = options.max_uses, .kind = options.kind, @@ -454,7 +454,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { }; } - fn isInviteValid(invite: models.invites.Invite) bool { + fn isInviteValid(invite: Invite) bool { if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false; if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false; return true; @@ -567,7 +567,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return user; } - pub fn createNote(self: *Self, content: []const u8) !NoteResponse { + pub fn createNote(self: *Self, content: []const u8) !Note { // You cannot post on admin accounts if (self.community.kind == .admin) return error.WrongCommunity; @@ -581,34 +581,25 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { }; } - pub fn getNote(self: *Self, note_id: Uuid) !NoteResponse { + pub fn getNote(self: *Self, note_id: Uuid) !Note { const note = try models.notes.get(self.db, note_id, self.allocator); - const user = try models.actors.get(self.db, note.author_id, self.allocator); + errdefer util.deepFree(self.allocator, note); // Only serve community-specific notes on unauthenticated requests if (self.user_id == null) { - if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound; + if (!Uuid.eql(self.community.id, note.author.community_id)) return error.NotFound; } - return NoteResponse{ - .id = note.id, - .author = .{ - .id = user.id, - .username = user.username, - .host = user.host, - }, - .content = note.content, - .created_at = note.created_at, - }; + return note; } - pub fn queryCommunities(self: *Self, args: models.communities.QueryArgs) !CommunityQueryResult { + pub fn queryCommunities(self: *Self, args: Community.QueryArgs) !QueryResult(Community) { if (!self.isAdmin()) return error.PermissionDenied; return try models.communities.query(self.db, args, self.allocator); } pub fn globalTimeline(self: *Self, args: TimelineArgs) !TimelineResult { - const all_args = std.mem.zeroInit(NoteQueryArgs, args); + const all_args = std.mem.zeroInit(Note.QueryArgs, args); const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ .items = result.items, @@ -618,7 +609,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult { - var all_args = std.mem.zeroInit(NoteQueryArgs, args); + var all_args = std.mem.zeroInit(Note.QueryArgs, args); all_args.community_id = self.community.id; const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ @@ -631,7 +622,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult { if (self.user_id == null) return error.NoToken; - var all_args = std.mem.zeroInit(models.notes.QueryArgs, args); + var all_args = std.mem.zeroInit(Note.QueryArgs, args); all_args.followed_by = self.user_id; const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ @@ -815,7 +806,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return try self.backendDriveEntryToFrontend(entry, true); } - pub fn driveUpdate(self: *Self, path: []const u8, meta: models.files.PartialMeta) !DriveEntry { + pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry { const user_id = self.user_id orelse return error.NoToken; std.log.debug("{s}", .{path}); const entry = try models.drive.stat(self.db, user_id, path, self.allocator); @@ -837,7 +828,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { }; } - pub fn updateUserProfile(self: *Self, id: Uuid, data: PartialUserProfile) !void { + pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void { if (!Uuid.eql(id, self.user_id orelse return error.NoToken)) return error.AccessDenied; try models.actors.updateProfile(self.db, id, data, self.allocator); } @@ -888,3 +879,53 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } }; } + +// test "register" { +// const TestDb = void; +// const exp_code = "abcd"; +// const exp_community = Uuid.parse("a210c035-c9e1-4361-82a2-aaeac8e40dc6") catch unreachable; +// var conn = ApiConn(TestDb, struct { +// const invites = struct { +// fn getByCode(_: TestDb, code: []const u8, community_id: Uuid, alloc: std.mem.Allocator) !services.invites.Invite { +// try std.testing.expectEqualStrings(exp_code, code); +// try std.testing.expectEqual(exp_community, community_id); + +// return try util.deepClone(alloc, services.invites.Invite{ +// .id = Uuid.parse("eac18f43-4dcc-489f-9fb5-4c1633e7b4e0") catch unreachable, + +// .created_by = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable, +// .community_id = exp_community, +// .name = "test invite", +// .code = exp_code, + +// .created_at = DateTime.parse("2022-12-21T09:05:50Z") catch unreachable, +// .times_used = 0, + +// .expires_at = null, +// .max_uses = null, +// }); +// } +// }; +// const auth = struct { +// fn register( +// _: TestDb, +// username: []const u8, +// password: []const u8, +// community_id: Uuid, +// _: RegistrationOptions, +// _: std.mem.Allocator, +// ) !Uuid { +// try std.testing.expectEqualStrings("root", username); +// try std.testing.expectEqualStrings("password", password); +// try std.testing.expectEqual(exp_community, community_id); + +// return Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable; +// } +// }; +// }){}; +// defer conn.close(); + +// const result = try conn.register("root", "password", .{}); +// try std.allocator. + +// } diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 0244336..48567da 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -1,13 +1,13 @@ const std = @import("std"); const util = @import("util"); const sql = @import("sql"); -const auth = @import("./auth.zig"); const common = @import("./common.zig"); const files = @import("./files.zig"); +const types = @import("./types.zig"); -const Partial = common.Partial; const Uuid = util.Uuid; const DateTime = util.DateTime; +const Actor = types.Actor; pub const CreateError = error{ UsernameTaken, @@ -17,19 +17,6 @@ pub const CreateError = error{ DatabaseFailure, }; -pub const ActorDetailed = struct { - id: Uuid, - username: []const u8, - host: []const u8, - display_name: ?[]const u8, - bio: []const u8, - avatar_file_id: ?Uuid, - header_file_id: ?Uuid, - profile_fields: ProfileField, - created_at: DateTime, - updated_at: DateTime, -}; - pub const LookupError = error{ DatabaseFailure, }; @@ -101,35 +88,6 @@ pub fn create( return id; } -pub const ProfileField = struct { - key: []const u8, - value: []const u8, -}; - -pub const Actor = struct { - id: Uuid, - - username: []const u8, - host: []const u8, - - display_name: ?[]const u8, - bio: []const u8, - - avatar_file_id: ?Uuid, - header_file_id: ?Uuid, - - profile_fields: []const ProfileField, - - community_id: Uuid, - - created_at: DateTime, - updated_at: DateTime, - - pub const sql_serialize = struct { - pub const profile_fields = .json; - }; -}; - pub const GetError = error{ NotFound, DatabaseFailure }; pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor { return db.queryRow( @@ -162,19 +120,11 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor { }; } -pub const PartialProfile = Partial(Profile); -pub const Profile = struct { - display_name: ?[]const u8, - bio: []const u8, - avatar_file_id: ?Uuid, - header_file_id: ?Uuid, - profile_fields: []const ProfileField, -}; pub const max_fields = 32; pub const max_display_name_len = 128; pub const max_bio = 1 << 16; -pub fn updateProfile(db: anytype, id: Uuid, new: PartialProfile, alloc: std.mem.Allocator) !void { +pub fn updateProfile(db: anytype, id: Uuid, new: Actor.ProfileUpdateArgs, alloc: std.mem.Allocator) !void { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); diff --git a/src/api/services/auth.zig b/src/api/services/auth.zig index 4307cab..9cf1734 100644 --- a/src/api/services/auth.zig +++ b/src/api/services/auth.zig @@ -1,6 +1,8 @@ const std = @import("std"); const util = @import("util"); const actors = @import("./actors.zig"); +const types = @import("./types.zig"); +const Token = types.Token; const Uuid = util.Uuid; const DateTime = util.DateTime; @@ -85,7 +87,7 @@ pub fn login( community_id: Uuid, password: []const u8, alloc: std.mem.Allocator, -) LoginError!LoginResult { +) LoginError!Token { std.log.debug("user: {s}, community_id: {}", .{ username, community_id }); const info = db.queryRow( struct { account_id: Uuid, hash: []const u8 }, @@ -136,36 +138,36 @@ pub fn login( if (!std.mem.eql(u8, info.hash, updated_info.hash)) return error.InvalidLogin; } + const now = DateTime.now(); tx.insert("token", .{ .account_id = info.account_id, .hash = token_hash, - .issued_at = DateTime.now(), + .issued_at = now, }, alloc) catch return error.DatabaseFailure; tx.commit() catch return error.DatabaseFailure; - return LoginResult{ - .token = token, - .user_id = info.account_id, + return Token{ + .value = token, + .info = .{ + .user_id = info.account_id, + .issued_at = now, + }, }; } pub const VerifyTokenError = error{ InvalidToken, DatabaseFailure, OutOfMemory }; -pub const TokenInfo = struct { - user_id: Uuid, - issued_at: DateTime, -}; pub fn verifyToken( db: anytype, token: []const u8, community_id: Uuid, alloc: std.mem.Allocator, -) VerifyTokenError!TokenInfo { +) VerifyTokenError!Token.Info { const hash = try hashToken(token, alloc); defer alloc.free(hash); return db.queryRow( - TokenInfo, + Token.Info, \\SELECT token.account_id as user_id, token.issued_at \\FROM token \\ JOIN account diff --git a/src/api/services/communities.zig b/src/api/services/communities.zig index 81ae8b4..8bc3869 100644 --- a/src/api/services/communities.zig +++ b/src/api/services/communities.zig @@ -2,41 +2,15 @@ const std = @import("std"); const builtin = @import("builtin"); const util = @import("util"); const sql = @import("sql"); -const common = @import("./common.zig"); const actors = @import("./actors.zig"); +const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; - -pub const Community = struct { - pub const Kind = enum { - admin, - local, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - }; - - pub const Scheme = enum { - https, - http, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - }; - id: Uuid, - - owner_id: ?Uuid, - host: []const u8, - name: []const u8, - - scheme: Scheme, - kind: Kind, - created_at: DateTime, -}; - -pub const CreateOptions = struct { - name: ?[]const u8 = null, - kind: Community.Kind = .local, -}; +const Community = types.Community; +const CreateOptions = Community.CreateOptions; +const QueryArgs = Community.QueryArgs; +const QueryResult = types.QueryResult(Community); pub const CreateError = error{ UnsupportedScheme, @@ -165,61 +139,6 @@ pub fn transferOwnership(db: anytype, community_id: Uuid, new_owner: Uuid) !void ) catch return error.DatabaseFailure; } -pub const QueryArgs = struct { - pub const OrderBy = enum { - name, - host, - created_at, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - }; - - pub const Direction = common.Direction; - pub const PageDirection = common.PageDirection; - pub const Prev = std.meta.Child(std.meta.fieldInfo(QueryArgs, .prev).field_type); - pub const OrderVal = std.meta.fieldInfo(Prev, .order_val).field_type; - - // Max items to fetch - max_items: usize = 20, - - // Selection filters - owner_id: ?Uuid = null, // searches for communities owned by this user - like: ?[]const u8 = null, // searches for communities with host or name LIKE '%?%' - created_before: ?DateTime = null, - created_after: ?DateTime = null, - - // Ordering parameter - order_by: OrderBy = .created_at, - direction: Direction = .ascending, - - // Page start parameter - // This struct is a reference to the last value scanned - // If prev is present, then prev.order_val must have the same tag as order_by - // "prev" here refers to it being the previous value returned. It may be that - // prev refers to the item directly after the results you are about to recieve, - // if you are querying the previous page. - prev: ?struct { - id: Uuid, - order_val: union(OrderBy) { - name: []const u8, - host: []const u8, - created_at: DateTime, - }, - } = null, - - // What direction to scan the page window - // If "forward", then "prev" is interpreted as the item directly before the items - // to query, in the direction of "direction" above. If "backward", then the opposite - page_direction: PageDirection = .forward, -}; - -pub const QueryResult = struct { - items: []const Community, - - prev_page: QueryArgs, - next_page: QueryArgs, -}; - const max_max_items = 100; pub const QueryError = error{ diff --git a/src/api/services/files.zig b/src/api/services/files.zig index 21ac8e8..ca8361d 100644 --- a/src/api/services/files.zig +++ b/src/api/services/files.zig @@ -1,41 +1,11 @@ const std = @import("std"); const sql = @import("sql"); const util = @import("util"); +const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; - -pub const FileStatus = enum { - uploading, - uploaded, - external, - deleted, - pub const jsonStringify = util.jsonSerializeEnumAsString; -}; - -pub const FileUpload = struct { - id: Uuid, - - owner_id: Uuid, - size: usize, - - filename: []const u8, - description: ?[]const u8, - content_type: ?[]const u8, - sensitive: bool, - - status: FileStatus, - - created_at: DateTime, - updated_at: DateTime, -}; - -pub const FileMeta = struct { - filename: []const u8, - description: ?[]const u8, - content_type: ?[]const u8, - sensitive: bool, -}; +const FileUpload = types.FileUpload; pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload { return try db.queryRow( @@ -60,26 +30,7 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload { ); } -pub const PartialMeta = Partial(FileMeta); -pub fn Partial(comptime T: type) type { - const t_fields = std.meta.fields(T); - var fields: [t_fields.len]std.builtin.Type.StructField = undefined; - for (std.meta.fields(T)) |f, i| fields[i] = .{ - .name = f.name, - .field_type = ?f.field_type, - .default_value = &@as(?f.field_type, null), - .is_comptime = false, - .alignment = @alignOf(?f.field_type), - }; - return @Type(.{ .Struct = .{ - .layout = .Auto, - .fields = &fields, - .decls = &.{}, - .is_tuple = false, - } }); -} - -pub fn update(db: anytype, id: Uuid, meta: PartialMeta, alloc: std.mem.Allocator) !void { +pub fn update(db: anytype, id: Uuid, meta: FileUpload.UpdateArgs, alloc: std.mem.Allocator) !void { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); @@ -106,7 +57,7 @@ pub fn update(db: anytype, id: Uuid, meta: PartialMeta, alloc: std.mem.Allocator }, alloc); } -pub fn create(db: anytype, owner_id: Uuid, meta: FileMeta, data: []const u8, alloc: std.mem.Allocator) !Uuid { +pub fn create(db: anytype, owner_id: Uuid, meta: FileUpload.CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid { const id = Uuid.randV4(util.getThreadPrng()); const now = DateTime.now(); try db.insert("file_upload", .{ @@ -120,7 +71,7 @@ pub fn create(db: anytype, owner_id: Uuid, meta: FileMeta, data: []const u8, all .content_type = meta.content_type, .sensitive = meta.sensitive, - .status = FileStatus.uploading, + .status = FileUpload.Status.uploading, .created_at = now, .updated_at = now, diff --git a/src/api/services/invites.zig b/src/api/services/invites.zig index 72684e7..dff5401 100644 --- a/src/api/services/invites.zig +++ b/src/api/services/invites.zig @@ -1,9 +1,11 @@ const std = @import("std"); const builtin = @import("builtin"); const util = @import("util"); +const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; +const Invite = types.Invite; // 9 random bytes = 12 random b64 const rand_len = 8; @@ -12,40 +14,14 @@ const code_len = 12; const Encoder = std.base64.url_safe.Encoder; const Decoder = std.base64.url_safe.Decoder; -pub const Kind = enum { - system, - community_owner, - user, - - pub const jsonStringify = util.jsonSerializeEnumAsString; -}; - -const InviteCount = usize; -pub const Invite = struct { - id: Uuid, - - created_by: Uuid, // User ID +pub fn create( + db: anytype, + created_by: Uuid, community_id: Uuid, name: []const u8, - code: []const u8, - - created_at: DateTime, - times_used: InviteCount, - - expires_at: ?DateTime, - max_uses: ?InviteCount, - - kind: Kind, -}; - -pub const InviteOptions = struct { - name: ?[]const u8 = null, - max_uses: ?InviteCount = null, - lifespan: ?DateTime.Duration = null, - kind: Kind = .user, -}; - -pub fn create(db: anytype, created_by: Uuid, community_id: ?Uuid, options: InviteOptions, alloc: std.mem.Allocator) !Uuid { + options: Invite.InternalCreateOptions, + alloc: std.mem.Allocator, +) !Uuid { const id = Uuid.randV4(util.getThreadPrng()); var code_bytes: [rand_len]u8 = undefined; @@ -55,7 +31,6 @@ pub fn create(db: anytype, created_by: Uuid, community_id: ?Uuid, options: Invit defer alloc.free(code); _ = Encoder.encode(code, &code_bytes); - const name = options.name orelse code; const created_at = DateTime.now(); try db.insert( diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index f413f76..59782d4 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -2,28 +2,13 @@ const std = @import("std"); const util = @import("util"); const sql = @import("sql"); const common = @import("./common.zig"); +const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; - -pub const Note = struct { - id: Uuid, - - author_id: Uuid, - content: []const u8, - created_at: DateTime, -}; - -pub const NoteDetailed = struct { - id: Uuid, - - author: struct { - id: Uuid, - username: []const u8, - }, - content: []const u8, - created_at: DateTime, -}; +const Note = types.Note; +const QueryArgs = Note.QueryArgs; +const QueryResult = types.QueryResult(Note); pub const CreateError = error{ DatabaseFailure, @@ -58,10 +43,27 @@ const selectStarFromNote = std.fmt.comptimePrint( pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Note { return db.queryRow( Note, - selectStarFromNote ++ - \\WHERE id = $1 - \\LIMIT 1 - , + \\SELECT + \\ note.id, + \\ note.content, + \\ note.created_at, + \\ actor.id AS "author.id", + \\ actor.username AS "author.username", + \\ community.host AS "author.host", + \\ actor.display_name AS "author.display_name", + \\ actor.bio AS "author.bio", + \\ actor.avatar_file_id AS "author.avatar_file_id", + \\ actor.header_file_id AS "author.header_file_id", + \\ actor.profile_fields AS "author.profile_fields", + \\ actor.community_id AS "author.community_id", + \\ actor.created_at AS "author.created_at", + \\ actor.updated_at AS "author.updated_at" + \\FROM note + \\ JOIN actor ON actor.id = note.author_id + \\ JOIN community ON community.id = actor.community_id + \\WHERE id = $1 + \\LIMIT 1 + , .{id}, alloc, ) catch |err| switch (err) { @@ -72,40 +74,29 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Note { const max_max_items = 100; -pub const QueryArgs = struct { - pub const PageDirection = common.PageDirection; - pub const Prev = std.meta.Child(std.meta.fieldInfo(@This(), .prev).field_type); - - max_items: usize = 20, - - created_before: ?DateTime = null, - created_after: ?DateTime = null, - community_id: ?Uuid = null, - followed_by: ?Uuid = null, - - prev: ?struct { - id: Uuid, - created_at: DateTime, - } = null, - - page_direction: PageDirection = .forward, -}; - -pub const QueryResult = struct { - items: []NoteDetailed, - - prev_page: QueryArgs, - next_page: QueryArgs, -}; - pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); try builder.appendSlice( - \\SELECT note.id, note.content, note.created_at, actor.id AS "author.id", actor.username AS "author.username" + \\SELECT + \\ note.id, + \\ note.content, + \\ note.created_at, + \\ actor.id AS "author.id", + \\ actor.username AS "author.username", + \\ community.host AS "author.host", + \\ actor.display_name AS "author.display_name", + \\ actor.bio AS "author.bio", + \\ actor.avatar_file_id AS "author.avatar_file_id", + \\ actor.header_file_id AS "author.header_file_id", + \\ actor.profile_fields AS "author.profile_fields", + \\ actor.community_id AS "author.community_id", + \\ actor.created_at AS "author.created_at", + \\ actor.updated_at AS "author.updated_at" \\FROM note \\ JOIN actor ON actor.id = note.author_id + \\ JOIN community ON community.id = actor.community_id \\ ); @@ -153,7 +144,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul }; const results = try db.queryRowsWithOptions( - NoteDetailed, + Note, try builder.terminate(), query_args, max_items, diff --git a/src/api/services/types.zig b/src/api/services/types.zig new file mode 100644 index 0000000..7bd3a13 --- /dev/null +++ b/src/api/services/types.zig @@ -0,0 +1,362 @@ +const util = @import("util"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; + +const common = struct { + const Direction = enum { + ascending, + descending, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + const PageDirection = enum { + forward, + backward, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; +}; + +pub fn QueryResult(comptime T: type) type { + return QueryResultArguments(T, T.QueryArgs); +} + +pub fn QueryResultArguments(comptime T: type, comptime A: type) type { + return struct { + items: []T, + + next_page: A, + prev_page: A, + }; +} + +pub const Account = struct { + pub const Auth = struct { + password_hash: []const u8, + updated_at: DateTime, + }; + pub const Kind = enum { + user, + admin, + }; + + id: Uuid, + invite_id: ?Uuid, + email: ?[]const u8, + kind: Kind, +}; + +pub const Actor = struct { + pub const ProfileField = struct { + key: []const u8, + value: []const u8, + }; + + id: Uuid, + + username: []const u8, + host: []const u8, + community_id: Uuid, + + display_name: ?[]const u8, + bio: []const u8, + + avatar_file_id: ?Uuid, + header_file_id: ?Uuid, + + profile_fields: []const ProfileField, + + created_at: DateTime, + updated_at: DateTime, + + // TODO: get rid of this + pub const Profile = struct { + display_name: ?[]const u8, + bio: []const u8, + avatar_file_id: ?Uuid, + header_file_id: ?Uuid, + profile_fields: []const ProfileField, + + pub const sql_serialize = struct { + pub const profile_fields = .json; + }; + }; + + pub const ProfileUpdateArgs = struct { + display_name: ??[]const u8, + bio: ?[]const u8, + avatar_file_id: ??Uuid, + header_file_id: ??Uuid, + profile_fields: ?[]const ProfileField, + + pub const sql_serialize = struct { + pub const profile_fields = .json; + }; + }; + + pub const sql_serialize = struct { + pub const profile_fields = .json; + }; +}; + +pub const Community = struct { + pub const Kind = enum { + admin, + local, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + pub const Scheme = enum { + https, + http, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + id: Uuid, + + owner_id: ?Uuid, + host: []const u8, + name: []const u8, + + scheme: Scheme, + kind: Kind, + created_at: DateTime, + + pub const CreateOptions = struct { + name: ?[]const u8 = null, + kind: Kind = .local, + }; + + pub const QueryArgs = struct { + pub const OrderBy = enum { + name, + host, + created_at, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + pub const Direction = common.Direction; + pub const PageDirection = common.PageDirection; + pub const Prev = struct { + id: Uuid, + order_val: OrderVal, + }; + pub const OrderVal = union(OrderBy) { + name: []const u8, + host: []const u8, + created_at: DateTime, + }; + + // Max items to fetch + max_items: usize = 20, + + // Selection filters + owner_id: ?Uuid = null, // searches for communities owned by this user + like: ?[]const u8 = null, // searches for communities with host or name LIKE '%?%' + created_before: ?DateTime = null, + created_after: ?DateTime = null, + + // Ordering parameter + order_by: OrderBy = .created_at, + direction: Direction = .ascending, + + // Page start parameter(s) + // This struct is a reference to the last value scanned + // If prev is present, then prev.order_val must have the same tag as order_by + // "prev" here refers to it being the previous value returned. It may be that + // prev refers to the item directly after the results you are about to recieve, + // if you are querying the previous page. + prev: ?Prev = null, + + // What direction to scan the page window + // If "forward", then "prev" is interpreted as the item directly before the items + // to query, in the direction of "direction" above. If "backward", then the opposite + page_direction: PageDirection = .forward, + }; +}; + +pub const DriveEntry = struct { + pub const Kind = enum { + dir, + file, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + id: Uuid, + owner_id: Uuid, + + name: ?[]const u8, + + path: []const u8, + parent_directory_id: ?Uuid, + + file_id: ?Uuid, + kind: Kind, +}; + +pub const FileUpload = struct { + pub const Status = enum { + uploading, + uploaded, + external, + deleted, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + id: Uuid, + + owner_id: Uuid, + size: usize, + + filename: []const u8, + description: ?[]const u8, + content_type: ?[]const u8, + sensitive: bool, + + status: Status, + + created_at: DateTime, + updated_at: DateTime, + + pub const CreateOptions = struct { + filename: []const u8, + description: ?[]const u8, + content_type: ?[]const u8, + sensitive: bool, + }; + + pub const UpdateArgs = struct { + filename: ?[]const u8, + description: ?[]const u8, + content_type: ?[]const u8, + sensitive: ?bool, + }; +}; + +pub const Invite = struct { + const UseCount = usize; + + pub const Kind = enum { + system, + community_owner, + user, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + id: Uuid, + + created_by: Uuid, // User ID + community_id: Uuid, + name: []const u8, + code: []const u8, + + created_at: DateTime, + times_used: UseCount, + + expires_at: ?DateTime, + max_uses: ?UseCount, + + kind: Kind, + + pub const CreateOptions = struct { + name: ?[]const u8 = null, + max_uses: ?UseCount = null, + lifespan: ?DateTime.Duration = null, + kind: Kind = .user, + to_community: ?Uuid = null, + }; + + pub const InternalCreateOptions = struct { + name: ?[]const u8 = null, + max_uses: ?UseCount = null, + lifespan: ?DateTime.Duration = null, + kind: Kind = .user, + }; +}; + +pub const Follow = struct { + id: Uuid, + + followed_by_id: Uuid, + followee_id: Uuid, + + created_at: DateTime, + + pub const QueryArgs = struct { + pub const OrderBy = enum { + created_at, + }; + + pub const Direction = common.Direction; + pub const PageDirection = common.PageDirection; + pub const Prev = struct { + id: Uuid, + order_val: union(OrderBy) { + created_at: DateTime, + }, + }; + + max_items: usize = 20, + + followed_by_id: ?Uuid = null, + followee_id: ?Uuid = null, + + order_by: OrderBy = .created_at, + + direction: Direction = .descending, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + }; +}; + +pub const Note = struct { + id: Uuid, + + author: Actor, + content: []const u8, + created_at: DateTime, + + pub const QueryArgs = struct { + pub const PageDirection = common.PageDirection; + pub const Prev = struct { + id: Uuid, + created_at: DateTime, + }; + + max_items: usize = 20, + + created_before: ?DateTime = null, + created_after: ?DateTime = null, + community_id: ?Uuid = null, + followed_by: ?Uuid = null, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + }; + + // TODO: This sucks + pub const sql_serialize = struct { + pub const @"author.profile_fields" = .json; + }; +}; + +pub const Token = struct { + pub const Info = struct { + user_id: Uuid, + issued_at: DateTime, + }; + + value: []const u8, + info: Info, +}; diff --git a/src/main/controllers/api/communities.zig b/src/main/controllers/api/communities.zig index 161fbb0..6c79355 100644 --- a/src/main/controllers/api/communities.zig +++ b/src/main/controllers/api/communities.zig @@ -2,7 +2,7 @@ const api = @import("api"); const util = @import("util"); const controller_utils = @import("../../controllers.zig").helpers; -const QueryArgs = api.CommunityQueryArgs; +const QueryArgs = api.Community.QueryArgs; pub const create = struct { pub const method = .POST; @@ -25,9 +25,9 @@ pub const query = struct { pub const path = "/communities"; pub const Query = struct { - const OrderBy = api.CommunityQueryArgs.OrderBy; - const Direction = api.CommunityQueryArgs.Direction; - const PageDirection = api.CommunityQueryArgs.PageDirection; + const OrderBy = api.Community.QueryArgs.OrderBy; + const Direction = api.Community.QueryArgs.Direction; + const PageDirection = api.Community.QueryArgs.PageDirection; // Max items to fetch max_items: usize = 20, @@ -80,7 +80,7 @@ pub const query = struct { }); const convert = struct { - fn func(args: api.CommunityQueryArgs) Query { + fn func(args: api.Community.QueryArgs) Query { return .{ .max_items = args.max_items, .owner_id = args.owner_id, diff --git a/src/main/controllers/api/users.zig b/src/main/controllers/api/users.zig index 0a629a0..450c652 100644 --- a/src/main/controllers/api/users.zig +++ b/src/main/controllers/api/users.zig @@ -50,7 +50,7 @@ pub const update_profile = struct { id: util.Uuid, }; - pub const Body = api.PartialUserProfile; + pub const Body = api.Actor.ProfileUpdateArgs; // TODO: I don't like that the request body dn response body are substantially different pub fn handler(req: anytype, res: anytype, srv: anytype) !void { diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index d176239..72bc1bd 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -96,7 +96,7 @@ const login = struct { 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(cookie_name, token.value, .{}); try res.headers.setCookie("active_account", req.body.username, .{ .HttpOnly = false }); try res.status(.see_other); @@ -190,7 +190,7 @@ const signup = struct { 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(cookie_name, token.value, .{}); try res.headers.setCookie("active_account", req.body.username, .{ .HttpOnly = false }); try res.status(.see_other); From 62d47d9d2f5bc201b34913723582f740fa569c97 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 26 Dec 2022 08:05:26 -0800 Subject: [PATCH 06/22] start moving api methods --- build.zig | 8 +- src/api/lib.zig | 153 +++++++++++------------------ src/api/methods/auth.zig | 162 +++++++++++++++++++++++++++++++ src/api/services/actors.zig | 2 +- src/api/services/auth.zig | 2 +- src/api/services/communities.zig | 2 +- src/api/services/files.zig | 2 +- src/api/services/invites.zig | 2 +- src/api/services/notes.zig | 2 +- src/api/{services => }/types.zig | 0 10 files changed, 229 insertions(+), 106 deletions(-) create mode 100644 src/api/methods/auth.zig rename src/api/{services => }/types.zig (100%) diff --git a/build.zig b/build.zig index 7cc183b..26d44c5 100644 --- a/build.zig +++ b/build.zig @@ -116,7 +116,12 @@ pub fn build(b: *std.build.Builder) !void { const unittest_template_cmd = b.step("unit:template", "Run tests for template package"); const unittest_template = b.addTest("src/template/lib.zig"); unittest_template_cmd.dependOn(&unittest_template.step); - //unittest_template.addPackage(pkgs.util); + + const unittest_api_cmd = b.step("unit:api", "Run tests for api package"); + const unittest_api = b.addTest("src/api/lib.zig"); + unittest_api_cmd.dependOn(&unittest_api.step); + unittest_api.addPackage(pkgs.util); + unittest_api.addPackage(pkgs.sql); //const util_tests = b.addTest("src/util/lib.zig"); //const sql_tests = b.addTest("src/sql/lib.zig"); @@ -129,6 +134,7 @@ pub fn build(b: *std.build.Builder) !void { unittest_all.dependOn(unittest_util_cmd); unittest_all.dependOn(unittest_sql_cmd); unittest_all.dependOn(unittest_template_cmd); + unittest_all.dependOn(unittest_api_cmd); const api_integration = b.addTest("./tests/api_integration/lib.zig"); api_integration.addPackage(pkgs.opts); diff --git a/src/api/lib.zig b/src/api/lib.zig index 9da0674..80ebef3 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -8,17 +8,21 @@ const Uuid = util.Uuid; const default_avatar = "static/default_avi.png"; const services = struct { - const communities = @import("./services/communities.zig"); - const actors = @import("./services/actors.zig"); - const auth = @import("./services/auth.zig"); - const drive = @import("./services/drive.zig"); - const files = @import("./services/files.zig"); - const invites = @import("./services/invites.zig"); - const notes = @import("./services/notes.zig"); - const follows = @import("./services/follows.zig"); + pub const communities = @import("./services/communities.zig"); + pub const actors = @import("./services/actors.zig"); + pub const auth = @import("./services/auth.zig"); + pub const drive = @import("./services/drive.zig"); + pub const files = @import("./services/files.zig"); + pub const invites = @import("./services/invites.zig"); + pub const notes = @import("./services/notes.zig"); + pub const follows = @import("./services/follows.zig"); }; -const types = @import("./services/types.zig"); +test { + _ = @import("./methods/auth.zig"); +} + +const types = @import("./types.zig"); pub const QueryResult = types.QueryResult; @@ -460,52 +464,53 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return true; } - pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse { - const tx = try self.db.beginOrSavepoint(); - const maybe_invite = if (opt.invite_code) |code| - try models.invites.getByCode(tx, code, self.community.id, self.allocator) - else - null; - defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); + pub usingnamespace @import("./methods/auth.zig").methods(models); + // pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse { + // const tx = try self.db.beginOrSavepoint(); + // const maybe_invite = if (opt.invite_code) |code| + // try models.invites.getByCode(tx, code, self.community.id, self.allocator) + // else + // null; + // defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); - if (maybe_invite) |invite| { - if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; - if (!isInviteValid(invite)) return error.InvalidInvite; - } + // if (maybe_invite) |invite| { + // if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; + // if (!isInviteValid(invite)) return error.InvalidInvite; + // } - const invite_kind = if (maybe_invite) |inv| inv.kind else .user; + // const invite_kind = if (maybe_invite) |inv| inv.kind else .user; - if (self.community.kind == .admin) @panic("Unimplmented"); + // if (self.community.kind == .admin) @panic("Unimplmented"); - const user_id = try models.auth.register( - tx, - username, - password, - self.community.id, - .{ - .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, - .email = opt.email, - }, - self.allocator, - ); + // const user_id = try models.auth.register( + // tx, + // username, + // password, + // self.community.id, + // .{ + // .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, + // .email = opt.email, + // }, + // self.allocator, + // ); - switch (invite_kind) { - .user => {}, - .system => @panic("System user invites unimplemented"), - .community_owner => { - try models.communities.transferOwnership(tx, self.community.id, user_id); - }, - } + // switch (invite_kind) { + // .user => {}, + // .system => @panic("System user invites unimplemented"), + // .community_owner => { + // try models.communities.transferOwnership(tx, self.community.id, user_id); + // }, + // } - const user = self.getUserUnchecked(tx, user_id) catch |err| switch (err) { - error.NotFound => return error.Unexpected, - else => |e| return e, - }; - errdefer util.deepFree(self.allocator, user); + // const user = self.getUserUnchecked(tx, user_id) catch |err| switch (err) { + // error.NotFound => return error.Unexpected, + // else => |e| return e, + // }; + // errdefer util.deepFree(self.allocator, user); - try tx.commit(); - return user; - } + // try tx.commit(); + // return user; + // } fn getUserUnchecked(self: *Self, db: anytype, user_id: Uuid) !UserResponse { const user = try models.actors.get(db, user_id, self.allocator); @@ -720,7 +725,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { errdefer self.allocator.free(result); for (children) |child, i| { - result[i] = try self.backendDriveEntryToFrontend(child, false); + result[i] = try backendDriveEntryToFrontend(self, child, false); count += 1; } @@ -879,53 +884,3 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } }; } - -// test "register" { -// const TestDb = void; -// const exp_code = "abcd"; -// const exp_community = Uuid.parse("a210c035-c9e1-4361-82a2-aaeac8e40dc6") catch unreachable; -// var conn = ApiConn(TestDb, struct { -// const invites = struct { -// fn getByCode(_: TestDb, code: []const u8, community_id: Uuid, alloc: std.mem.Allocator) !services.invites.Invite { -// try std.testing.expectEqualStrings(exp_code, code); -// try std.testing.expectEqual(exp_community, community_id); - -// return try util.deepClone(alloc, services.invites.Invite{ -// .id = Uuid.parse("eac18f43-4dcc-489f-9fb5-4c1633e7b4e0") catch unreachable, - -// .created_by = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable, -// .community_id = exp_community, -// .name = "test invite", -// .code = exp_code, - -// .created_at = DateTime.parse("2022-12-21T09:05:50Z") catch unreachable, -// .times_used = 0, - -// .expires_at = null, -// .max_uses = null, -// }); -// } -// }; -// const auth = struct { -// fn register( -// _: TestDb, -// username: []const u8, -// password: []const u8, -// community_id: Uuid, -// _: RegistrationOptions, -// _: std.mem.Allocator, -// ) !Uuid { -// try std.testing.expectEqualStrings("root", username); -// try std.testing.expectEqualStrings("password", password); -// try std.testing.expectEqual(exp_community, community_id); - -// return Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable; -// } -// }; -// }){}; -// defer conn.close(); - -// const result = try conn.register("root", "password", .{}); -// try std.allocator. - -// } diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig new file mode 100644 index 0000000..3829851 --- /dev/null +++ b/src/api/methods/auth.zig @@ -0,0 +1,162 @@ +const std = @import("std"); +const util = @import("util"); +const types = @import("../types.zig"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const RegistrationOptions = @import("../lib.zig").RegistrationOptions; +const UserResponse = @import("../lib.zig").UserResponse; +const Invite = @import("../lib.zig").Invite; + +pub fn methods(comptime models: type) type { + return struct { + fn isInviteValid(invite: Invite) bool { + if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false; + if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false; + return true; + } + pub fn register(self: anytype, username: []const u8, password: []const u8, opt: RegistrationOptions) !types.Actor { + const tx = try self.db.beginOrSavepoint(); + const maybe_invite = if (opt.invite_code) |code| + try models.invites.getByCode(tx, code, self.community.id, self.allocator) + else + null; + defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); + + if (maybe_invite) |invite| { + if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; + if (!isInviteValid(invite)) return error.InvalidInvite; + } + + const invite_kind = if (maybe_invite) |inv| inv.kind else .user; + + if (self.community.kind == .admin) @panic("Unimplmented"); + + const user_id = try models.auth.register( + tx, + username, + password, + self.community.id, + .{ + .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, + .email = opt.email, + }, + self.allocator, + ); + + switch (invite_kind) { + .user => {}, + .system => @panic("System user invites unimplemented"), + .community_owner => { + try models.communities.transferOwnership(tx, self.community.id, user_id); + }, + } + + const user = models.actors.get(tx, user_id, self.allocator) catch |err| switch (err) { + error.NotFound => return error.Unexpected, + else => |e| return e, + }; + errdefer util.deepFree(self.allocator, user); + + try tx.commitOrRelease(); + return user; + } + }; +} + +const TestDb = struct { + tx_level: usize = 0, + rolled_back: bool = false, + committed: bool = false, + fn beginOrSavepoint(self: *TestDb) !*TestDb { + self.tx_level += 1; + return self; + } + + fn rollback(self: *TestDb) void { + self.rolled_back = true; + self.tx_level -= 1; + } + + fn commitOrRelease(self: *TestDb) !void { + self.committed = true; + self.tx_level -= 1; + } +}; + +test "register" { + comptime var exp_code = "code"; + comptime var exp_community = Uuid.parse("a210c035-c9e1-4361-82a2-aaeac8e40dc6") catch unreachable; + comptime var uid = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable; + + const MockSvc = struct { + const invites = struct { + fn getByCode(db: *TestDb, code: []const u8, community_id: Uuid, alloc: std.mem.Allocator) !Invite { + try std.testing.expectEqual(db.tx_level, 1); + try std.testing.expectEqualStrings(exp_code, code); + try std.testing.expectEqual(exp_community, community_id); + + return try util.deepClone(alloc, Invite{ + .id = Uuid.parse("eac18f43-4dcc-489f-9fb5-4c1633e7b4e0") catch unreachable, + + .created_by = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable, + .community_id = exp_community, + .name = "test invite", + .code = exp_code, + + .kind = .user, + + .created_at = DateTime.parse("2022-12-21T09:05:50Z") catch unreachable, + .times_used = 0, + + .expires_at = null, + .max_uses = null, + }); + } + }; + const auth = struct { + fn register( + db: *TestDb, + username: []const u8, + password: []const u8, + community_id: Uuid, + _: @import("../services/auth.zig").RegistrationOptions, + _: std.mem.Allocator, + ) anyerror!Uuid { + try std.testing.expectEqual(db.tx_level, 1); + try std.testing.expectEqualStrings("root", username); + try std.testing.expectEqualStrings("password", password); + try std.testing.expectEqual(exp_community, community_id); + + return uid; + } + }; + const actors = struct { + fn get(_: *TestDb, id: Uuid, alloc: std.mem.Allocator) anyerror!types.Actor { + try std.testing.expectEqual(uid, id); + return try util.deepClone(alloc, std.mem.zeroInit(types.Actor, .{ + .id = id, + .username = "root", + .host = "example.com", + .community_id = exp_community, + })); + } + }; + const communities = struct { + fn transferOwnership(_: *TestDb, _: Uuid, _: Uuid) anyerror!void {} + }; + }; + + var db = TestDb{}; + util.deepFree(std.testing.allocator, try methods(MockSvc).register(.{ + .db = &db, + .allocator = std.testing.allocator, + .community = .{ + .id = exp_community, + .kind = .local, + }, + }, "root", "password", .{})); + try std.testing.expectEqual(false, db.rolled_back); + try std.testing.expectEqual(true, db.committed); + try std.testing.expectEqual(@as(usize, 0), db.tx_level); +} diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 48567da..95bfe10 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -3,7 +3,7 @@ const util = @import("util"); const sql = @import("sql"); const common = @import("./common.zig"); const files = @import("./files.zig"); -const types = @import("./types.zig"); +const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; diff --git a/src/api/services/auth.zig b/src/api/services/auth.zig index 9cf1734..b0f8200 100644 --- a/src/api/services/auth.zig +++ b/src/api/services/auth.zig @@ -1,7 +1,7 @@ const std = @import("std"); const util = @import("util"); const actors = @import("./actors.zig"); -const types = @import("./types.zig"); +const types = @import("../types.zig"); const Token = types.Token; const Uuid = util.Uuid; diff --git a/src/api/services/communities.zig b/src/api/services/communities.zig index 8bc3869..6b575d5 100644 --- a/src/api/services/communities.zig +++ b/src/api/services/communities.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const util = @import("util"); const sql = @import("sql"); const actors = @import("./actors.zig"); -const types = @import("./types.zig"); +const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; diff --git a/src/api/services/files.zig b/src/api/services/files.zig index ca8361d..9b4412d 100644 --- a/src/api/services/files.zig +++ b/src/api/services/files.zig @@ -1,7 +1,7 @@ const std = @import("std"); const sql = @import("sql"); const util = @import("util"); -const types = @import("./types.zig"); +const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; diff --git a/src/api/services/invites.zig b/src/api/services/invites.zig index dff5401..fe0ce6f 100644 --- a/src/api/services/invites.zig +++ b/src/api/services/invites.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const util = @import("util"); -const types = @import("./types.zig"); +const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 59782d4..0cce202 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -2,7 +2,7 @@ const std = @import("std"); const util = @import("util"); const sql = @import("sql"); const common = @import("./common.zig"); -const types = @import("./types.zig"); +const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; diff --git a/src/api/services/types.zig b/src/api/types.zig similarity index 100% rename from src/api/services/types.zig rename to src/api/types.zig From 6f30696d30ba07dd5dcf59265c7c97ff5059f4a8 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 1 Jan 2023 15:58:17 -0800 Subject: [PATCH 07/22] Create API Context --- src/api/lib.zig | 103 +++++++++++++++++++---------------- src/api/methods/auth.zig | 10 ++-- src/main/controllers.zig | 4 +- src/main/controllers/web.zig | 2 +- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 80ebef3..774b1aa 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -287,8 +287,9 @@ pub const ApiSource = struct { return Conn{ .db = db, - .user_id = null, - .community = community, + .context = .{ + .community = community, + }, .allocator = alloc, }; } @@ -307,40 +308,48 @@ pub const ApiSource = struct { return Conn{ .db = db, - .token_info = token_info, - .user_id = token_info.user_id, - .community = community, + .context = .{ + .community = community, + .token_info = token_info, + }, .allocator = alloc, }; } }; +pub const ApiContext = struct { + token_info: ?Token.Info = null, + community: Community, + + pub fn userId(self: ApiContext) ?Uuid { + if (self.token_info) |t| return t.user_id else return null; + } +}; + fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return struct { const Self = @This(); db: DbConn, - token_info: ?Token.Info = null, - user_id: ?Uuid = null, - community: Community, + context: ApiContext, allocator: std.mem.Allocator, pub fn close(self: *Self) void { - util.deepFree(self.allocator, self.community); - if (self.token_info) |info| util.deepFree(self.allocator, info); + util.deepFree(self.allocator, self.context.community); + if (self.context.token_info) |info| util.deepFree(self.allocator, info); self.db.releaseConnection(); } fn isAdmin(self: *Self) bool { // TODO - return self.user_id != null and self.community.kind == .admin; + return self.context.userId() != null and self.context.community.kind == .admin; } pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { return models.auth.login( self.db, username, - self.community.id, + self.context.community.id, password, self.allocator, ); @@ -355,7 +364,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { issued_at: DateTime, }; pub fn verifyAuthorization(self: *Self) !AuthorizationInfo { - if (self.token_info) |info| { + if (self.context.token_info) |info| { const user = try models.actors.get(self.db, info.user_id, self.allocator); defer util.deepFree(self.allocator, user); @@ -365,8 +374,8 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return AuthorizationInfo{ .id = user.id, .username = username, - .community_id = self.community.id, - .host = try util.deepClone(self.allocator, self.community.host), + .community_id = self.context.community.id, + .host = try util.deepClone(self.allocator, self.context.community.host), .issued_at = info.issued_at, }; @@ -405,14 +414,14 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse { // Only logged in users can make invites - const user_id = self.user_id orelse return error.TokenRequired; + const user_id = self.context.userId() orelse return error.TokenRequired; const community_id = if (options.to_community) |id| blk: { // Only admins can send invites for other communities if (!self.isAdmin()) return error.PermissionDenied; break :blk id; - } else self.community.id; + } else self.context.community.id; // Users can only make user invites if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied; @@ -438,7 +447,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } else try std.fmt.allocPrint( self.allocator, "{s}://{s}/invite/{s}", - .{ @tagName(self.community.scheme), self.community.host, invite.code }, + .{ @tagName(self.context.community.scheme), self.context.community.host, invite.code }, ); errdefer util.deepFree(self.allocator, url); @@ -468,25 +477,25 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { // pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse { // const tx = try self.db.beginOrSavepoint(); // const maybe_invite = if (opt.invite_code) |code| - // try models.invites.getByCode(tx, code, self.community.id, self.allocator) + // try models.invites.getByCode(tx, code, self.context.community.id, self.allocator) // else // null; // defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); // if (maybe_invite) |invite| { - // if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; + // if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity; // if (!isInviteValid(invite)) return error.InvalidInvite; // } // const invite_kind = if (maybe_invite) |inv| inv.kind else .user; - // if (self.community.kind == .admin) @panic("Unimplmented"); + // if (self.context.community.kind == .admin) @panic("Unimplmented"); // const user_id = try models.auth.register( // tx, // username, // password, - // self.community.id, + // self.context.community.id, // .{ // .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, // .email = opt.email, @@ -498,7 +507,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { // .user => {}, // .system => @panic("System user invites unimplemented"), // .community_owner => { - // try models.communities.transferOwnership(tx, self.community.id, user_id); + // try models.communities.transferOwnership(tx, self.context.community.id, user_id); // }, // } @@ -519,20 +528,20 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { try std.fmt.allocPrint( self.allocator, "{s}://{s}/media/{}", - .{ @tagName(self.community.scheme), self.community.host, fid }, + .{ @tagName(self.context.community.scheme), self.context.community.host, fid }, ) else try std.fmt.allocPrint( self.allocator, "{s}://{s}/{s}", - .{ @tagName(self.community.scheme), self.community.host, default_avatar }, + .{ @tagName(self.context.community.scheme), self.context.community.host, default_avatar }, ); errdefer self.allocator.free(avatar_url); const header_url = if (user.header_file_id) |fid| try std.fmt.allocPrint( self.allocator, "{s}://{s}/media/{}", - .{ @tagName(self.community.scheme), self.community.host, fid }, + .{ @tagName(self.context.community.scheme), self.context.community.host, fid }, ) else null; @@ -565,8 +574,8 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { const user = try self.getUserUnchecked(self.db, user_id); errdefer util.deepFree(self.allocator, user); - if (self.user_id == null) { - if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound; + if (self.context.userId() == null) { + if (!Uuid.eql(self.context.community.id, user.community_id)) return error.NotFound; } return user; @@ -574,10 +583,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { pub fn createNote(self: *Self, content: []const u8) !Note { // You cannot post on admin accounts - if (self.community.kind == .admin) return error.WrongCommunity; + if (self.context.community.kind == .admin) return error.WrongCommunity; // Only authenticated users can post - const user_id = self.user_id orelse return error.TokenRequired; + const user_id = self.context.userId() orelse return error.TokenRequired; const note_id = try models.notes.create(self.db, user_id, content, self.allocator); return self.getNote(note_id) catch |err| switch (err) { @@ -591,8 +600,8 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { errdefer util.deepFree(self.allocator, note); // Only serve community-specific notes on unauthenticated requests - if (self.user_id == null) { - if (!Uuid.eql(self.community.id, note.author.community_id)) return error.NotFound; + if (self.context.userId() == null) { + if (!Uuid.eql(self.context.community.id, note.author.community_id)) return error.NotFound; } return note; @@ -615,7 +624,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult { var all_args = std.mem.zeroInit(Note.QueryArgs, args); - all_args.community_id = self.community.id; + all_args.community_id = self.context.community.id; const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ .items = result.items, @@ -625,10 +634,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult { - if (self.user_id == null) return error.NoToken; + if (self.context.userId() == null) return error.NoToken; var all_args = std.mem.zeroInit(Note.QueryArgs, args); - all_args.followed_by = self.user_id; + all_args.followed_by = self.context.userId(); const result = try models.notes.query(self.db, all_args, self.allocator); return TimelineResult{ .items = result.items, @@ -660,12 +669,12 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn follow(self: *Self, followee: Uuid) !void { - const result = try models.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); + const result = try models.follows.create(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator); defer util.deepFree(self.allocator, result); } pub fn unfollow(self: *Self, followee: Uuid) !void { - const result = try models.follows.delete(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); + const result = try models.follows.delete(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator); defer util.deepFree(self.allocator, result); } @@ -736,7 +745,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry { - const user_id = self.user_id orelse return error.NoToken; + const user_id = self.context.userId() orelse return error.NoToken; const file_id = try models.files.create(self.db, user_id, .{ .filename = meta.filename, .description = meta.description, @@ -782,14 +791,14 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry { - const user_id = self.user_id orelse return error.NoToken; + const user_id = self.context.userId() orelse return error.NoToken; const entry = try models.drive.create(self.db, user_id, parent_path, name, null, self.allocator); errdefer util.deepFree(self.allocator, entry); return try self.backendDriveEntryToFrontend(entry, true); } pub fn driveDelete(self: *Self, path: []const u8) !void { - const user_id = self.user_id orelse return error.NoToken; + const user_id = self.context.userId() orelse return error.NoToken; const entry = try models.drive.stat(self.db, user_id, path, self.allocator); defer util.deepFree(self.allocator, entry); try models.drive.delete(self.db, entry.id, self.allocator); @@ -797,14 +806,14 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry { - const user_id = self.user_id orelse return error.NoToken; + const user_id = self.context.userId() orelse return error.NoToken; try models.drive.move(self.db, user_id, src, dest, self.allocator); return try self.driveGet(dest); } pub fn driveGet(self: *Self, path: []const u8) !DriveEntry { - const user_id = self.user_id orelse return error.NoToken; + const user_id = self.context.userId() orelse return error.NoToken; const entry = try models.drive.stat(self.db, user_id, path, self.allocator); errdefer util.deepFree(self.allocator, entry); @@ -812,7 +821,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry { - const user_id = self.user_id orelse return error.NoToken; + const user_id = self.context.userId() orelse return error.NoToken; std.log.debug("{s}", .{path}); const entry = try models.drive.stat(self.db, user_id, path, self.allocator); defer util.deepFree(self.allocator, entry); @@ -834,7 +843,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { } pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void { - if (!Uuid.eql(id, self.user_id orelse return error.NoToken)) return error.AccessDenied; + if (!Uuid.eql(id, self.context.userId() orelse return error.NoToken)) return error.AccessDenied; try models.actors.updateProfile(self.db, id, data, self.allocator); } @@ -842,7 +851,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { const invite = models.invites.getByCode( self.db, code, - self.community.id, + self.context.community.id, self.allocator, ) catch |err| switch (err) { error.NotFound => return error.InvalidInvite, @@ -850,13 +859,13 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { }; errdefer util.deepFree(self.allocator, invite); - if (!Uuid.eql(invite.community_id, self.community.id)) return error.InvalidInvite; + if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.InvalidInvite; if (!isInviteValid(invite)) return error.InvalidInvite; const url = try std.fmt.allocPrint( self.allocator, "{s}://{s}/invite/{s}", - .{ @tagName(self.community.scheme), self.community.host, invite.code }, + .{ @tagName(self.context.community.scheme), self.context.community.host, invite.code }, ); errdefer util.deepFree(self.allocator, url); diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index 3829851..3f994d8 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -18,25 +18,25 @@ pub fn methods(comptime models: type) type { pub fn register(self: anytype, username: []const u8, password: []const u8, opt: RegistrationOptions) !types.Actor { const tx = try self.db.beginOrSavepoint(); const maybe_invite = if (opt.invite_code) |code| - try models.invites.getByCode(tx, code, self.community.id, self.allocator) + try models.invites.getByCode(tx, code, self.context.community.id, self.allocator) else null; defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); if (maybe_invite) |invite| { - if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; + if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity; if (!isInviteValid(invite)) return error.InvalidInvite; } const invite_kind = if (maybe_invite) |inv| inv.kind else .user; - if (self.community.kind == .admin) @panic("Unimplmented"); + if (self.context.community.kind == .admin) @panic("Unimplmented"); const user_id = try models.auth.register( tx, username, password, - self.community.id, + self.context.community.id, .{ .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, .email = opt.email, @@ -48,7 +48,7 @@ pub fn methods(comptime models: type) type { .user => {}, .system => @panic("System user invites unimplemented"), .community_owner => { - try models.communities.transferOwnership(tx, self.community.id, user_id); + try models.communities.transferOwnership(tx, self.context.community.id, user_id); }, } diff --git a/src/main/controllers.zig b/src/main/controllers.zig index ad971b5..21305ad 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -282,7 +282,7 @@ pub const Response = struct { pub fn template(self: *Self, status_code: http.Status, srv: anytype, comptime templ: []const u8, data: anytype) !void { try self.headers.put("Content-Type", "text/html"); - const user = if (srv.user_id) |uid| try srv.getUser(uid) else null; + const user = if (srv.context.userId()) |uid| try srv.getUser(uid) else null; defer util.deepFree(srv.allocator, user); var stream = try self.open(status_code); @@ -300,7 +300,7 @@ pub const Response = struct { .{ .community = srv.community, .user = user, - .user_id = srv.user_id, + .user_id = srv.context.userId(), }, ); diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 72bc1bd..9af7100 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -61,7 +61,7 @@ const index = struct { pub const method = .GET; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { - if (srv.user_id == null) { + if (srv.context.userId() == null) { try res.headers.put("Location", about.path); return res.status(.see_other); } From ec4e99c41e96897894561c7affb6f6be83b8e797 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 1 Jan 2023 16:01:58 -0800 Subject: [PATCH 08/22] Update templates to use new context --- src/main/controllers.zig | 3 +-- src/main/controllers/web.zig | 2 -- src/main/controllers/web/_format.tmpl.html | 2 +- src/main/controllers/web/cluster/overview.tmpl.html | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 21305ad..d1aa26e 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -298,9 +298,8 @@ pub const Response = struct { @embedFile("./controllers/web/_format.tmpl.html"), data, .{ - .community = srv.community, + .community = srv.context.community, .user = user, - .user_id = srv.context.userId(), }, ); diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 9af7100..d6e5a39 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -208,7 +208,6 @@ const global_timeline = struct { try res.template(.ok, srv, @embedFile("./web/timelines/global.tmpl.html"), .{ .notes = timeline.items, - .community = srv.community, }); } }; @@ -370,7 +369,6 @@ const cluster = struct { 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, }); } diff --git a/src/main/controllers/web/_format.tmpl.html b/src/main/controllers/web/_format.tmpl.html index a8eab83..79f839b 100644 --- a/src/main/controllers/web/_format.tmpl.html +++ b/src/main/controllers/web/_format.tmpl.html @@ -11,7 +11,7 @@