From 494d317ac1aa1b2f3e76fef0b1aaeb2b55db1f05 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 2 Jan 2023 12:38:42 -0800 Subject: [PATCH 1/9] refactor db layer --- src/api/lib.zig | 8 ++++ src/api/methods/auth.zig | 101 +++++++++++++++++++-------------------- src/api/services.zig | 78 ++++++++++++++++++++++++++++++ src/sql/lib.zig | 1 + 4 files changed, 137 insertions(+), 51 deletions(-) create mode 100644 src/api/services.zig diff --git a/src/api/lib.zig b/src/api/lib.zig index 30a2b58..c9c7901 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -318,6 +318,10 @@ pub const ApiContext = struct { pub fn userId(self: ApiContext) ?Uuid { if (self.token_info) |t| return t.user_id else return null; } + + pub fn isAdmin(self: ApiContext) bool { + return self.userId() != null and self.community.kind == .admin; + } }; fn ApiConn(comptime DbConn: type, comptime models: anytype) type { @@ -457,6 +461,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return true; } + pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { + return @import("./methods/auth.zig").login(self.allocator, self.context, @import("./services.zig").Services(DbConn){ .db = self.db }, username, password); + } + 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(); diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index 2847953..27f4543 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -91,57 +91,6 @@ pub fn methods(comptime models: type) type { return id; } - pub fn login(self: anytype, username: []const u8, password: []const u8) !Token { - const community_id = self.context.community.id; - const credentials = try models.accounts.getCredentialsByUsername( - self.db, - username, - community_id, - self.allocator, - ); - defer util.deepFree(self.allocator, credentials); - - try verifyPassword(credentials.password_hash, password, self.allocator); - - const token = try generateToken(self.allocator); - errdefer util.deepFree(self.allocator, token); - const token_hash = hashToken(token, self.allocator) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - else => unreachable, - }; - defer util.deepFree(self.allocator, token_hash); - - const tx = try self.db.begin(); - errdefer tx.rollback(); - - // ensure that the password has not changed in the meantime - { - const updated_info = try models.accounts.getCredentialsByUsername( - tx, - username, - community_id, - self.allocator, - ); - defer util.deepFree(self.allocator, updated_info); - - if (!std.mem.eql(u8, credentials.password_hash, updated_info.password_hash)) return error.InvalidLogin; - } - - try models.tokens.create(tx, credentials.account_id, token_hash, self.allocator); - - try tx.commit(); - const info = try models.tokens.getByHash(self.db, token_hash, community_id, self.allocator); - defer util.deepFree(self.allocator, info); - - return .{ - .value = token, - .info = .{ - .user_id = info.account_id, - .issued_at = info.issued_at, - }, - }; - } - pub fn verifyToken(self: anytype, token: []const u8) !Token.Info { const hash = try hashToken(token, self.allocator); defer self.allocator.free(hash); @@ -154,6 +103,56 @@ pub fn methods(comptime models: type) type { }; } +const ApiContext = @import("../lib.zig").ApiContext; +pub fn login(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, username: []const u8, password: []const u8) !Token { + const community_id = ctx.community.id; + const credentials = try svcs.getCredentialsByUsername( + alloc, + username, + community_id, + ); + defer util.deepFree(alloc, credentials); + + try verifyPassword(credentials.password_hash, password, alloc); + + const token = try generateToken(alloc); + errdefer util.deepFree(alloc, token); + const token_hash = hashToken(token, alloc) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + else => unreachable, + }; + defer util.deepFree(alloc, token_hash); + + const tx = try svcs.beginTx(); + errdefer tx.rollbackTx(); + + // ensure that the password has not changed in the meantime + { + const updated_info = try tx.getCredentialsByUsername( + alloc, + username, + community_id, + ); + defer util.deepFree(alloc, updated_info); + + if (!std.mem.eql(u8, credentials.password_hash, updated_info.password_hash)) return error.InvalidLogin; + } + + try tx.createToken(alloc, credentials.account_id, token_hash); + + try tx.commitTx(); + const info = try tx.getTokenByHash(alloc, token_hash, community_id); + defer util.deepFree(alloc, info); + + return .{ + .value = token, + .info = .{ + .user_id = info.account_id, + .issued_at = info.issued_at, + }, + }; +} + // We use scrypt, a password hashing algorithm that attempts to slow down // GPU-based cracking approaches by using large amounts of memory, for // password hashing. diff --git a/src/api/services.zig b/src/api/services.zig new file mode 100644 index 0000000..dad0506 --- /dev/null +++ b/src/api/services.zig @@ -0,0 +1,78 @@ +const std = @import("std"); +const util = @import("util"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; + +const communities = @import("./services/communities.zig"); +const actors = @import("./services/actors.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"); +const accounts = @import("./services/accounts.zig"); +const tokens = @import("./services/tokens.zig"); + +pub const Token = tokens.Token; +pub const Account = accounts.Account; +pub const Credentials = accounts.Credentials; + +pub fn Services(comptime Db: type) type { + return struct { + const Self = @This(); + + db: Db, + + pub fn beginTx(self: Self) !Services(Db.BeginOrSavepoint) { + return Services(Db.BeginOrSavepoint){ + .db = try self.db.beginOrSavepoint(), + }; + } + + pub fn commitTx(self: Self) !void { + return try self.db.commitOrRelease(); + } + + pub fn rollbackTx(self: Self) void { + return self.db.rollback(); + } + + pub fn createAccount( + self: Self, + alloc: std.mem.Allocator, + actor: Uuid, + password_hash: []const u8, + options: accounts.CreateOptions, + ) !Account { + return try accounts.create(self.db, actor, password_hash, options, alloc); + } + + pub fn getCredentialsByUsername( + self: Self, + alloc: std.mem.Allocator, + username: []const u8, + community_id: Uuid, + ) !Credentials { + return try accounts.getCredentialsByUsername(self.db, username, community_id, alloc); + } + + pub fn createToken( + self: Self, + alloc: std.mem.Allocator, + account_id: Uuid, + hash: []const u8, + ) !void { + return try tokens.create(self.db, account_id, hash, alloc); + } + + pub fn getTokenByHash( + self: Self, + alloc: std.mem.Allocator, + hash: []const u8, + community_id: Uuid, + ) !Token { + return try tokens.getByHash(self.db, hash, community_id, alloc); + } + }; +} diff --git a/src/sql/lib.zig b/src/sql/lib.zig index 776b159..bb55dd3 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -505,6 +505,7 @@ fn Tx(comptime tx_level: u8) type { }; } + pub const BeginOrSavepoint = Tx(tx_level + 1); pub const beginOrSavepoint = if (tx_level == 0) begin else savepoint; pub const commitOrRelease = if (tx_level < 2) commit else release; From 9774f214f36e52bdb34e0af6ef028f3b7f48781a Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 2 Jan 2023 17:17:42 -0800 Subject: [PATCH 2/9] add stubs to services.zig --- src/api/lib.zig | 42 +++++--- src/api/methods/auth.zig | 175 +++++++++++++++++-------------- src/api/services.zig | 50 ++++++++- src/api/services/actors.zig | 2 +- src/api/services/communities.zig | 2 +- src/api/services/invites.zig | 2 +- 6 files changed, 173 insertions(+), 100 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index c9c7901..5f0ddcf 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -243,32 +243,30 @@ pub fn isAdminSetup(db: sql.Db) !bool { } pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password: []const u8, allocator: std.mem.Allocator) anyerror!void { - const tx = try db.begin(); - errdefer tx.rollback(); + const svc = @import("./services.zig").Services(sql.Db){ .db = db }; + const tx = try svc.beginTx(); + errdefer tx.rollbackTx(); var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - try tx.setConstraintMode(.deferred); - - const community_id = try services.communities.create( - tx, + const community_id = try tx.createCommunity( + arena.allocator(), origin, .{ .name = "Cluster Admin", .kind = .admin }, - arena.allocator(), ); - const user = try @import("./methods/auth.zig").methods(services).createLocalAccount( + const user = try @import("./methods/auth.zig").createLocalAccount( + arena.allocator(), tx, username, password, community_id, .{ .role = .admin }, - arena.allocator(), ); - try services.communities.transferOwnership(tx, community_id, user); + try tx.transferCommunityOwnership(community_id, user); - try tx.commit(); + try tx.commitTx(); std.log.info( "Created admin user {s} (id {}) with cluster admin origin {s} (id {})", @@ -306,7 +304,7 @@ pub const ApiSource = struct { pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn { var conn = try self.connectUnauthorized(host, alloc); errdefer conn.close(); - conn.context.token_info = try conn.verifyToken(token); + conn.context.token_info = try @import("./methods/auth.zig").verifyToken(alloc, conn.context, conn.getServices(), token); return conn; } }; @@ -461,11 +459,27 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return true; } + const Services = @import("./services.zig").Services(DbConn); + fn getServices(self: *Self) Services { + return Services{ .db = self.db }; + } pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { - return @import("./methods/auth.zig").login(self.allocator, self.context, @import("./services.zig").Services(DbConn){ .db = self.db }, username, password); + return methods.auth.login(self.allocator, self.context, self.getServices(), username, password); } - pub usingnamespace @import("./methods/auth.zig").methods(models); + const methods = struct { + const auth = @import("./methods/auth.zig"); + }; + pub fn register( + self: *Self, + username: []const u8, + password: []const u8, + opt: methods.auth.RegistrationOptions, + ) !types.Actor { + return methods.auth.register(self.allocator, self.context, self.getServices(), username, password, opt); + } + + //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| diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index 27f4543..2a7316c 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -14,97 +14,108 @@ pub const RegistrationOptions = struct { pub const AccountCreateOptions = @import("../services/accounts.zig").CreateOptions; -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.context.community.id, self.allocator) - else - null; - defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); +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; +} - if (maybe_invite) |invite| { - if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity; - if (!isInviteValid(invite)) return error.InvalidInvite; - } +pub fn register( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + username: []const u8, + password: []const u8, + opt: RegistrationOptions, +) !types.Actor { + const tx = try svcs.beginTx(); + errdefer tx.rollbackTx(); - const invite_kind = if (maybe_invite) |inv| inv.kind else .user; + const maybe_invite = if (opt.invite_code) |code| + try tx.getInviteByCode(alloc, code, ctx.community.id) + else + null; + defer if (maybe_invite) |inv| util.deepFree(alloc, inv); - if (self.context.community.kind == .admin) @panic("Unimplmented"); + if (maybe_invite) |invite| { + if (!Uuid.eql(invite.community_id, ctx.community.id)) return error.WrongCommunity; + if (!isInviteValid(invite)) return error.InvalidInvite; + } - const user_id = try createLocalAccount( - tx, - username, - password, - self.context.community.id, - .{ - .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, - .email = opt.email, - }, - self.allocator, - ); + const invite_kind = if (maybe_invite) |inv| inv.kind else .user; - switch (invite_kind) { - .user => {}, - .system => @panic("System user invites unimplemented"), - .community_owner => { - try models.communities.transferOwnership(tx, self.context.community.id, user_id); - }, - } + if (ctx.community.kind == .admin) @panic("Unimplmented"); - 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); + const user_id = try createLocalAccount( + alloc, + tx, + username, + password, + ctx.community.id, + .{ + .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, + .email = opt.email, + }, + ); - try tx.commitOrRelease(); - return user; - } + switch (invite_kind) { + .user => {}, + .system => @panic("System user invites unimplemented"), + .community_owner => { + try tx.transferCommunityOwnership(ctx.community.id, user_id); + }, + } - // Only for internal use - pub fn createLocalAccount( - db: anytype, - username: []const u8, - password: []const u8, - community_id: Uuid, - opt: AccountCreateOptions, - alloc: std.mem.Allocator, - ) !Uuid { - const tx = try db.beginOrSavepoint(); - errdefer tx.rollback(); - - const hash = try hashPassword(password, alloc); - defer alloc.free(hash); - - const id = try models.actors.create(tx, username, community_id, false, alloc); - try models.accounts.create(tx, id, hash, opt, alloc); - - try tx.commitOrRelease(); - - return id; - } - - pub fn verifyToken(self: anytype, token: []const u8) !Token.Info { - const hash = try hashToken(token, self.allocator); - defer self.allocator.free(hash); - - const info = try models.tokens.getByHash(self.db, hash, self.context.community.id, self.allocator); - defer util.deepFree(self.allocator, info); - - return .{ .user_id = info.account_id, .issued_at = info.issued_at }; - } + const user = tx.getActor(alloc, user_id) catch |err| switch (err) { + error.NotFound => return error.Unexpected, + else => |e| return e, }; + errdefer util.deepFree(alloc, user); + + try tx.commitTx(); + return user; +} + +pub fn createLocalAccount( + alloc: std.mem.Allocator, + svcs: anytype, + username: []const u8, + password: []const u8, + community_id: Uuid, + opt: AccountCreateOptions, +) !Uuid { + const tx = try svcs.beginTx(); + errdefer tx.rollbackTx(); + + const hash = try hashPassword(password, alloc); + defer alloc.free(hash); + + const id = try tx.createActor(alloc, username, community_id, false); + try tx.createAccount(alloc, id, hash, opt); + + try tx.commitTx(); + + return id; +} + +pub fn verifyToken(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, token: []const u8) !Token.Info { + const hash = try hashToken(token, alloc); + defer alloc.free(hash); + + const info = try svcs.getTokenByHash(alloc, hash, ctx.community.id); + defer util.deepFree(alloc, info); + + return .{ .user_id = info.account_id, .issued_at = info.issued_at }; } const ApiContext = @import("../lib.zig").ApiContext; -pub fn login(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, username: []const u8, password: []const u8) !Token { +pub fn login( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + username: []const u8, + password: []const u8, +) !Token { const community_id = ctx.community.id; const credentials = try svcs.getCredentialsByUsername( alloc, @@ -141,7 +152,7 @@ pub fn login(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, username: try tx.createToken(alloc, credentials.account_id, token_hash); try tx.commitTx(); - const info = try tx.getTokenByHash(alloc, token_hash, community_id); + const info = try svcs.getTokenByHash(alloc, token_hash, community_id); defer util.deepFree(alloc, info); return .{ @@ -312,7 +323,9 @@ test "register" { }; var db = TestDb{}; - util.deepFree(std.testing.allocator, try methods(MockSvc).register(.{ + + _ = MockSvc; + util.deepFree(std.testing.allocator, try register(.{ .db = &db, .allocator = std.testing.allocator, .community = .{ diff --git a/src/api/services.zig b/src/api/services.zig index dad0506..cee6c94 100644 --- a/src/api/services.zig +++ b/src/api/services.zig @@ -14,9 +14,11 @@ const follows = @import("./services/follows.zig"); const accounts = @import("./services/accounts.zig"); const tokens = @import("./services/tokens.zig"); -pub const Token = tokens.Token; pub const Account = accounts.Account; pub const Credentials = accounts.Credentials; +pub const Actor = actors.Actor; +pub const Invite = invites.Invite; +pub const Token = tokens.Token; pub fn Services(comptime Db: type) type { return struct { @@ -44,7 +46,7 @@ pub fn Services(comptime Db: type) type { actor: Uuid, password_hash: []const u8, options: accounts.CreateOptions, - ) !Account { + ) !void { return try accounts.create(self.db, actor, password_hash, options, alloc); } @@ -57,6 +59,50 @@ pub fn Services(comptime Db: type) type { return try accounts.getCredentialsByUsername(self.db, username, community_id, alloc); } + pub fn createActor( + self: Self, + alloc: std.mem.Allocator, + username: []const u8, + community_id: Uuid, + lax_username: bool, // TODO: remove this + ) !Uuid { + return try actors.create(self.db, username, community_id, lax_username, alloc); + } + + pub fn getActor( + self: Self, + alloc: std.mem.Allocator, + user_id: Uuid, + ) !Actor { + return try actors.get(self.db, user_id, alloc); + } + + pub fn createCommunity( + self: Self, + alloc: std.mem.Allocator, + origin: []const u8, + options: communities.CreateOptions, + ) !Uuid { + return try communities.create(self.db, origin, options, alloc); + } + + pub fn transferCommunityOwnership( + self: Self, + community_id: Uuid, + owner_id: Uuid, + ) !void { + return try communities.transferOwnership(self.db, community_id, owner_id); + } + + pub fn getInviteByCode( + self: Self, + alloc: std.mem.Allocator, + code: []const u8, + community_id: Uuid, + ) !Invite { + return try invites.getByCode(self.db, code, community_id, alloc); + } + pub fn createToken( self: Self, alloc: std.mem.Allocator, diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 95bfe10..9c6dab2 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -7,7 +7,7 @@ const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const Actor = types.Actor; +pub const Actor = types.Actor; pub const CreateError = error{ UsernameTaken, diff --git a/src/api/services/communities.zig b/src/api/services/communities.zig index 6b575d5..216be86 100644 --- a/src/api/services/communities.zig +++ b/src/api/services/communities.zig @@ -8,7 +8,7 @@ const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; const Community = types.Community; -const CreateOptions = Community.CreateOptions; +pub const CreateOptions = Community.CreateOptions; const QueryArgs = Community.QueryArgs; const QueryResult = types.QueryResult(Community); diff --git a/src/api/services/invites.zig b/src/api/services/invites.zig index fe0ce6f..a9eddb8 100644 --- a/src/api/services/invites.zig +++ b/src/api/services/invites.zig @@ -5,7 +5,7 @@ const types = @import("../types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const Invite = types.Invite; +pub const Invite = types.Invite; // 9 random bytes = 12 random b64 const rand_len = 8; From 39565bccf00601d3c14ef02a4f9e5328aa805213 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Mon, 2 Jan 2023 17:21:08 -0800 Subject: [PATCH 3/9] fuck --- src/api/lib.zig | 156 ++----------- src/api/methods/auth.zig | 12 +- src/api/methods/communities.zig | 45 ++++ src/api/methods/timelines.zig | 91 ++++++++ src/api/services.zig | 254 ++++++++++++++++++--- src/api/services/accounts.zig | 17 +- src/api/services/actors.zig | 2 +- src/api/services/communities.zig | 13 +- src/api/services/files.zig | 2 +- src/api/services/invites.zig | 4 +- src/api/services/notes.zig | 3 +- src/api/services/tokens.zig | 7 +- src/api/services/types.zig | 373 +++++++++++++++++++++++++++++++ src/api/types.zig | 363 +----------------------------- 14 files changed, 785 insertions(+), 557 deletions(-) create mode 100644 src/api/methods/communities.zig create mode 100644 src/api/methods/timelines.zig create mode 100644 src/api/services/types.zig diff --git a/src/api/lib.zig b/src/api/lib.zig index 5f0ddcf..e6a815f 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -1,35 +1,19 @@ const std = @import("std"); const util = @import("util"); const sql = @import("sql"); +const services = @import("./services.zig"); +const types = @import("./types.zig"); const DateTime = util.DateTime; const Uuid = util.Uuid; const default_avatar = "static/default_avi.png"; -const services = struct { - pub const communities = @import("./services/communities.zig"); - pub const actors = @import("./services/actors.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"); - - pub const accounts = @import("./services/accounts.zig"); - pub const tokens = @import("./services/tokens.zig"); -}; - -test { - _ = @import("./methods/auth.zig"); -} - -const types = @import("./types.zig"); - -pub const QueryResult = types.QueryResult; +const QueryResult = types.QueryResult; +pub usingnamespace types; pub const Account = types.Account; -pub const Actor = types.Actor; +pub const Actor = types.actors.Actor; pub const Community = types.Community; pub const Invite = types.Invite; pub const Note = types.Note; @@ -93,37 +77,6 @@ pub const NoteResponse = struct { created_at: DateTime, }; -pub const TimelineArgs = struct { - pub const PageDirection = Note.QueryArgs.PageDirection; - pub const Prev = Note.QueryArgs.Prev; - - max_items: usize = 20, - - created_before: ?DateTime = null, - created_after: ?DateTime = null, - - prev: ?Prev = null, - - page_direction: PageDirection = .forward, - - fn from(args: Note.QueryArgs) TimelineArgs { - return .{ - .max_items = args.max_items, - .created_before = args.created_before, - .created_after = args.created_after, - .prev = args.prev, - .page_direction = args.page_direction, - }; - } -}; - -pub const TimelineResult = struct { - items: []Note, - - prev_page: TimelineArgs, - next_page: TimelineArgs, -}; - const FollowQueryArgs = struct { pub const OrderBy = services.follows.QueryArgs.OrderBy; pub const Direction = services.follows.QueryArgs.Direction; @@ -322,9 +275,15 @@ pub const ApiContext = struct { } }; +const methods = struct { + const auth = @import("./methods/auth.zig"); + const timelines = @import("./methods/timelines.zig"); +}; + fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return struct { const Self = @This(); + const Services = @import("./services.zig").Services(DbConn); db: DbConn, context: ApiContext, @@ -341,6 +300,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return self.context.userId() != null and self.context.community.kind == .admin; } + fn getServices(self: *Self) Services { + return Services{ .db = self.db }; + } + pub const AuthorizationInfo = struct { id: Uuid, username: []const u8, @@ -459,17 +422,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return true; } - const Services = @import("./services.zig").Services(DbConn); - fn getServices(self: *Self) Services { - return Services{ .db = self.db }; - } pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { return methods.auth.login(self.allocator, self.context, self.getServices(), username, password); } - const methods = struct { - const auth = @import("./methods/auth.zig"); - }; pub fn register( self: *Self, username: []const u8, @@ -479,54 +435,6 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return methods.auth.register(self.allocator, self.context, self.getServices(), username, password, opt); } - //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.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.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.context.community.kind == .admin) @panic("Unimplmented"); - - // const user_id = try models.auth.register( - // tx, - // username, - // password, - // self.context.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.context.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); - - // 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); @@ -618,38 +526,16 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { return try models.communities.query(self.db, args, self.allocator); } - pub fn globalTimeline(self: *Self, args: TimelineArgs) !TimelineResult { - 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, - .prev_page = TimelineArgs.from(result.prev_page), - .next_page = TimelineArgs.from(result.next_page), - }; + pub fn globalTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult { + return methods.timelines.globalTimeline(self.allocator, self.context, self.getServices(), args); } - pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult { - var all_args = std.mem.zeroInit(Note.QueryArgs, args); - 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, - .prev_page = TimelineArgs.from(result.prev_page), - .next_page = TimelineArgs.from(result.next_page), - }; + pub fn localTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult { + return methods.timelines.localTimeline(self.allocator, self.context, self.getServices(), args); } - pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult { - if (self.context.userId() == null) return error.NoToken; - - var all_args = std.mem.zeroInit(Note.QueryArgs, args); - all_args.followed_by = self.context.userId(); - const result = try models.notes.query(self.db, all_args, self.allocator); - return TimelineResult{ - .items = result.items, - .prev_page = TimelineArgs.from(result.prev_page), - .next_page = TimelineArgs.from(result.next_page), - }; + pub fn homeTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult { + return methods.timelines.homeTimeline(self.allocator, self.context, self.getServices(), args); } pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult { diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index 2a7316c..5639385 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -1,18 +1,21 @@ const std = @import("std"); const util = @import("util"); const types = @import("../types.zig"); +const pkg = @import("../lib.zig"); +const services = @import("../services.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const Invite = @import("../lib.zig").Invite; -pub const Token = types.Token; +const ApiContext = pkg.ApiContext; +const Invite = types.Invite; +const Token = types.Token; -pub const RegistrationOptions = struct { +const RegistrationOptions = struct { invite_code: ?[]const u8 = null, email: ?[]const u8 = null, }; -pub const AccountCreateOptions = @import("../services/accounts.zig").CreateOptions; +const AccountCreateOptions = services.accounts.CreateOptions; fn isInviteValid(invite: Invite) bool { if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false; @@ -108,7 +111,6 @@ pub fn verifyToken(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, tok return .{ .user_id = info.account_id, .issued_at = info.issued_at }; } -const ApiContext = @import("../lib.zig").ApiContext; pub fn login( alloc: std.mem.Allocator, ctx: ApiContext, diff --git a/src/api/methods/communities.zig b/src/api/methods/communities.zig new file mode 100644 index 0000000..466c120 --- /dev/null +++ b/src/api/methods/communities.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const util = @import("util"); +const types = @import("../types.zig"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const QueryResult = types.QueryResult; +const Community = types.Community; + +pub fn methods(comptime models: type) type { + return struct { + pub fn createCommunity(self: anytype, origin: []const u8, name: ?[]const u8) !Community { + if (!self.isAdmin()) { + return error.PermissionDenied; + } + + const tx = try self.db.begin(); + errdefer tx.rollback(); + const community_id = try models.communities.create( + tx, + origin, + .{ .name = name }, + self.allocator, + ); + + const community = models.communities.get( + tx, + community_id, + self.allocator, + ) catch |err| return switch (err) { + error.NotFound => error.DatabaseError, + else => |err2| err2, + }; + + try tx.commit(); + + return community; + } + + pub fn queryCommunities(self: anytype, args: Community.QueryArgs) !QueryResult(Community) { + if (!self.context.isAdmin()) return error.PermissionDenied; + return try models.communities.query(self.db, args, self.allocator); + } + }; +} diff --git a/src/api/methods/timelines.zig b/src/api/methods/timelines.zig new file mode 100644 index 0000000..a6a6f47 --- /dev/null +++ b/src/api/methods/timelines.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const util = @import("util"); +const pkg = @import("../lib.zig"); + +const ApiContext = pkg.ApiContext; +const Uuid = util.Uuid; +const DateTime = util.DateTime; + +const services = @import("../services.zig"); +const Note = services.Note; +const QueryArgs = services.notes.QueryArgs; + +pub const TimelineArgs = struct { + pub const PageDirection = QueryArgs.PageDirection; + pub const Prev = QueryArgs.Prev; + + max_items: usize = 20, + + created_before: ?DateTime = null, + created_after: ?DateTime = null, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + + fn from(args: QueryArgs) TimelineArgs { + return .{ + .max_items = args.max_items, + .created_before = args.created_before, + .created_after = args.created_after, + .prev = args.prev, + .page_direction = args.page_direction, + }; + } +}; + +pub const TimelineResult = struct { + items: []Note, + + prev_page: TimelineArgs, + next_page: TimelineArgs, +}; + +pub fn globalTimeline( + alloc: std.mem.Allocator, + _: ApiContext, + svcs: anytype, + args: TimelineArgs, +) !TimelineResult { + const all_args = std.mem.zeroInit(QueryArgs, args); + const result = try svcs.queryNotes(alloc, all_args); + return TimelineResult{ + .items = result.items, + .prev_page = TimelineArgs.from(result.prev_page), + .next_page = TimelineArgs.from(result.next_page), + }; +} + +pub fn localTimeline( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + args: TimelineArgs, +) !TimelineResult { + var all_args = std.mem.zeroInit(QueryArgs, args); + all_args.community_id = ctx.community.id; + const result = try svcs.queryNotes(alloc, all_args); + return TimelineResult{ + .items = result.items, + .prev_page = TimelineArgs.from(result.prev_page), + .next_page = TimelineArgs.from(result.next_page), + }; +} + +pub fn homeTimeline( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + args: TimelineArgs, +) !TimelineResult { + if (ctx.userId() == null) return error.NoToken; + + var all_args = std.mem.zeroInit(QueryArgs, args); + all_args.followed_by = ctx.userId(); + const result = try svcs.queryNotes(alloc, all_args); + return TimelineResult{ + .items = result.items, + .prev_page = TimelineArgs.from(result.prev_page), + .next_page = TimelineArgs.from(result.next_page), + }; +} diff --git a/src/api/services.zig b/src/api/services.zig index cee6c94..82347eb 100644 --- a/src/api/services.zig +++ b/src/api/services.zig @@ -4,21 +4,30 @@ const util = @import("util"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const communities = @import("./services/communities.zig"); -const actors = @import("./services/actors.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"); -const accounts = @import("./services/accounts.zig"); -const tokens = @import("./services/tokens.zig"); +const impl = struct { + const communities = @import("./services/communities.zig"); + const actors = @import("./services/actors.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"); + const accounts = @import("./services/accounts.zig"); + const tokens = @import("./services/tokens.zig"); +}; -pub const Account = accounts.Account; -pub const Credentials = accounts.Credentials; -pub const Actor = actors.Actor; -pub const Invite = invites.Invite; -pub const Token = tokens.Token; +const types = @import("./services/types.zig"); +pub usingnamespace types; + +pub const Account = types.accounts.Account; +pub const Credentials = types.accounts.Credentials; +pub const Actor = types.actors.Actor; +pub const Community = types.communities.Community; +pub const DriveEntry = types.drive.DriveEntry; +pub const FileUpload = types.files.FileUpload; +pub const Invite = types.invites.Invite; +pub const Note = types.notes.Note; +pub const Token = types.tokens.Token; pub fn Services(comptime Db: type) type { return struct { @@ -45,9 +54,9 @@ pub fn Services(comptime Db: type) type { alloc: std.mem.Allocator, actor: Uuid, password_hash: []const u8, - options: accounts.CreateOptions, + options: types.accounts.CreateOptions, ) !void { - return try accounts.create(self.db, actor, password_hash, options, alloc); + return try impl.accounts.create(self.db, actor, password_hash, options, alloc); } pub fn getCredentialsByUsername( @@ -56,7 +65,7 @@ pub fn Services(comptime Db: type) type { username: []const u8, community_id: Uuid, ) !Credentials { - return try accounts.getCredentialsByUsername(self.db, username, community_id, alloc); + return try impl.accounts.getCredentialsByUsername(self.db, username, community_id, alloc); } pub fn createActor( @@ -66,7 +75,7 @@ pub fn Services(comptime Db: type) type { community_id: Uuid, lax_username: bool, // TODO: remove this ) !Uuid { - return try actors.create(self.db, username, community_id, lax_username, alloc); + return try impl.actors.create(self.db, username, community_id, lax_username, alloc); } pub fn getActor( @@ -74,16 +83,50 @@ pub fn Services(comptime Db: type) type { alloc: std.mem.Allocator, user_id: Uuid, ) !Actor { - return try actors.get(self.db, user_id, alloc); + return try impl.actors.get(self.db, user_id, alloc); + } + + pub fn lookupActorByUsername( + self: Self, + alloc: std.mem.Allocator, + username: []const u8, + community_id: Uuid, + ) !Actor { + return try impl.actors.lookupByUsername(self.db, username, community_id, alloc); + } + + pub fn updateActorProfile( + self: Self, + alloc: std.mem.Allocator, + actor_id: Uuid, + new: types.actors.ProfileUpdateArgs, + ) !Actor { + return try impl.actors.updateProfile(self.db, actor_id, new, alloc); } pub fn createCommunity( self: Self, alloc: std.mem.Allocator, origin: []const u8, - options: communities.CreateOptions, + options: types.communities.CreateOptions, ) !Uuid { - return try communities.create(self.db, origin, options, alloc); + return try impl.communities.create(self.db, origin, options, alloc); + } + + pub fn getCommunity( + self: Self, + alloc: std.mem.Allocator, + id: Uuid, + ) !Community { + return try impl.communities.get(self.db, id, alloc); + } + + pub fn getCommunityByHost( + self: Self, + alloc: std.mem.Allocator, + host: []const u8, + ) !Community { + return try impl.communities.getByHost(self.db, host, alloc); } pub fn transferCommunityOwnership( @@ -91,7 +134,168 @@ pub fn Services(comptime Db: type) type { community_id: Uuid, owner_id: Uuid, ) !void { - return try communities.transferOwnership(self.db, community_id, owner_id); + return try impl.communities.transferOwnership(self.db, community_id, owner_id); + } + + pub fn statDriveEntry( + self: Self, + alloc: std.mem.Allocator, + owner_id: Uuid, + path: []const u8, + ) !DriveEntry { + return try impl.drive.stat(self.db, owner_id, path, alloc); + } + + pub fn createDriveEntry( + self: Self, + alloc: std.mem.Allocator, + owner_id: Uuid, + containing_path: []const u8, + name: []const u8, + file_id: ?Uuid, + ) !Uuid { + return try impl.drive.create(self.db, owner_id, containing_path, name, file_id, alloc); + } + + pub fn deleteDriveEntry( + self: Self, + alloc: std.mem.Allocator, + entry_id: Uuid, + ) !void { + return try impl.drive.delete(self.db, entry_id, alloc); + } + + pub fn moveDriveEntry( + self: Self, + alloc: std.mem.Allocator, + owner_id: Uuid, + src: []const u8, + dest: []const u8, + ) !void { + return try impl.drive.move(self.db, owner_id, src, dest, alloc); + } + + // TODO: paginate + pub fn listDriveEntry( + self: Self, + alloc: std.mem.Allocator, + owner_id: Uuid, + path: []const u8, + ) ![]DriveEntry { + return try impl.drive.list(self.db, owner_id, path, alloc); + } + + pub fn createFile( + self: Self, + alloc: std.mem.Allocator, + owner_id: Uuid, + meta: types.files.CreateOptions, + data: []const u8, + ) !Uuid { + return try impl.files.create(self.db, owner_id, meta, data, alloc); + } + + pub fn deleteFile( + self: Self, + alloc: std.mem.Allocator, + id: Uuid, + ) !void { + return try impl.files.delete(self.db, id, alloc); + } + + pub fn statFile( + self: Self, + alloc: std.mem.Allocator, + id: Uuid, + ) !FileUpload { + return try impl.files.get(self.db, id, alloc); + } + + pub fn derefFile( + _: Self, + alloc: std.mem.Allocator, + id: Uuid, + ) ![]const u8 { + return try impl.files.deref(alloc, id); + } + + pub fn updateFileMetadata( + self: Self, + alloc: std.mem.Allocator, + id: Uuid, + meta: types.files.UpdateArgs, + ) !FileUpload { + return try impl.files.update(self.db, id, meta, alloc); + } + + pub fn createFollow( + self: Self, + alloc: std.mem.Allocator, + followed_by: Uuid, + followee: Uuid, + ) !void { + return try impl.follows.create(self.db, followed_by, followee, alloc); + } + + pub fn deleteFollow( + self: Self, + alloc: std.mem.Allocator, + followed_by: Uuid, + followee: Uuid, + ) !void { + return try impl.follows.delete(self.db, followed_by, followee, alloc); + } + + pub fn queryFollows( + self: Self, + alloc: std.mem.Allocator, + args: types.follows.QueryArgs, + ) !types.follows.QueryResult { + return try impl.follows.query(self.db, args, alloc); + } + + pub fn createInvite( + self: Self, + alloc: std.mem.Allocator, + created_by: Uuid, + community_id: Uuid, + name: []const u8, + options: types.invites.CreateOptions, + ) !Invite { + return try impl.invites.create(self.db, created_by, community_id, name, options, alloc); + } + + pub fn getInvite( + self: Self, + alloc: std.mem.Allocator, + invite_id: Uuid, + ) !Invite { + return try impl.invites.get(self.db, invite_id, alloc); + } + + pub fn createNote( + self: Self, + alloc: std.mem.Allocator, + author_id: Uuid, + content: []const u8, + ) !Uuid { + return try impl.notes.create(self.db, author_id, content, alloc); + } + + pub fn getNote( + self: Self, + alloc: std.mem.Allocator, + id: Uuid, + ) !Note { + return try impl.notes.get(self.db, id, alloc); + } + + pub fn queryNotes( + self: Self, + alloc: std.mem.Allocator, + args: types.notes.QueryArgs, + ) !types.notes.QueryResult { + return try impl.notes.query(self.db, args, alloc); } pub fn getInviteByCode( @@ -100,7 +304,7 @@ pub fn Services(comptime Db: type) type { code: []const u8, community_id: Uuid, ) !Invite { - return try invites.getByCode(self.db, code, community_id, alloc); + return try impl.invites.getByCode(self.db, code, community_id, alloc); } pub fn createToken( @@ -109,7 +313,7 @@ pub fn Services(comptime Db: type) type { account_id: Uuid, hash: []const u8, ) !void { - return try tokens.create(self.db, account_id, hash, alloc); + return try impl.tokens.create(self.db, account_id, hash, alloc); } pub fn getTokenByHash( @@ -118,7 +322,7 @@ pub fn Services(comptime Db: type) type { hash: []const u8, community_id: Uuid, ) !Token { - return try tokens.getByHash(self.db, hash, community_id, alloc); + return try impl.tokens.getByHash(self.db, hash, community_id, alloc); } }; } diff --git a/src/api/services/accounts.zig b/src/api/services/accounts.zig index 1f28846..e97014a 100644 --- a/src/api/services/accounts.zig +++ b/src/api/services/accounts.zig @@ -1,18 +1,12 @@ const std = @import("std"); const util = @import("util"); +const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -pub const Role = enum { - user, - admin, -}; -pub const CreateOptions = struct { - invite_id: ?Uuid = null, - email: ?[]const u8 = null, - role: Role = .user, -}; +const CreateOptions = types.accounts.CreateOptions; +const Credentials = types.accounts.Credentials; /// Creates a local account with the given information pub fn create( @@ -40,11 +34,6 @@ pub fn create( tx.commitOrRelease() catch return error.DatabaseFailure; } -pub const Credentials = struct { - account_id: Uuid, - password_hash: []const u8, -}; - pub fn getCredentialsByUsername(db: anytype, username: []const u8, community_id: Uuid, alloc: std.mem.Allocator) !Credentials { return db.queryRow( Credentials, diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 9c6dab2..bc8578f 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/communities.zig b/src/api/services/communities.zig index 216be86..dcd7e7e 100644 --- a/src/api/services/communities.zig +++ b/src/api/services/communities.zig @@ -3,14 +3,15 @@ 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; -const Community = types.Community; -pub const CreateOptions = Community.CreateOptions; -const QueryArgs = Community.QueryArgs; -const QueryResult = types.QueryResult(Community); +const Community = types.communities.Community; +const Scheme = types.communities.Scheme; +const CreateOptions = types.communities.CreateOptions; +const QueryArgs = types.communities.QueryArgs; +const QueryResult = types.communities.QueryResult; pub const CreateError = error{ UnsupportedScheme, @@ -21,7 +22,7 @@ pub const CreateError = error{ pub fn create(db: anytype, origin: []const u8, options: CreateOptions, alloc: std.mem.Allocator) CreateError!Uuid { const scheme_len = std.mem.indexOfScalar(u8, origin, ':') orelse return error.InvalidOrigin; const scheme_str = origin[0..scheme_len]; - const scheme = std.meta.stringToEnum(Community.Scheme, scheme_str) orelse return error.UnsupportedScheme; + const scheme = std.meta.stringToEnum(Scheme, scheme_str) orelse return error.UnsupportedScheme; // host must be in the format "{scheme}://{host}" if (origin.len <= scheme_len + ("://").len or diff --git a/src/api/services/files.zig b/src/api/services/files.zig index 9b4412d..ca8361d 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 a9eddb8..dff5401 100644 --- a/src/api/services/invites.zig +++ b/src/api/services/invites.zig @@ -1,11 +1,11 @@ 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; -pub const Invite = types.Invite; +const Invite = types.Invite; // 9 random bytes = 12 random b64 const rand_len = 8; diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 0cce202..0d682c5 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -1,8 +1,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/tokens.zig b/src/api/services/tokens.zig index 1ed0a5b..48477cd 100644 --- a/src/api/services/tokens.zig +++ b/src/api/services/tokens.zig @@ -4,12 +4,7 @@ const util = @import("util"); const Uuid = util.Uuid; const DateTime = util.DateTime; -pub const Token = struct { - account_id: Uuid, - issued_at: DateTime, - - hash: []const u8, -}; +const Token = @import("./types.zig").tokens.Token; pub fn create(db: anytype, account_id: Uuid, hash: []const u8, alloc: std.mem.Allocator) !void { const now = DateTime.now(); diff --git a/src/api/services/types.zig b/src/api/services/types.zig new file mode 100644 index 0000000..7005d18 --- /dev/null +++ b/src/api/services/types.zig @@ -0,0 +1,373 @@ +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; + }; + + fn QueryResult(comptime R: type, comptime A: type) type { + return struct { + items: []R, + + next_page: A, + prev_page: A, + }; + } +}; + +pub const accounts = struct { + pub const Role = enum { + user, + admin, + }; + + pub const CreateOptions = struct { + invite_id: ?Uuid = null, + email: ?[]const u8 = null, + role: Role = .user, + }; + + pub const Credentials = struct { + account_id: Uuid, + password_hash: []const u8, + }; +}; + +pub const actors = struct { + pub const Actor = struct { + 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, + + pub const sql_serialize = struct { + pub const profile_fields = .json; + }; + }; + + pub const ProfileField = struct { + key: []const u8, + value: []const u8, + }; + + // 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 communities = struct { + pub const Community = struct { + id: Uuid, + + owner_id: ?Uuid, + host: []const u8, + name: []const u8, + + scheme: Scheme, + kind: Kind, + created_at: DateTime, + }; + + pub const Kind = enum { + admin, + local, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + pub const Scheme = enum { + https, + http, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + 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 QueryResult = common.QueryResult(Community, QueryArgs); +}; + +pub const drive = struct { + 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 files = struct { + pub const FileUpload = struct { + 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 Status = enum { + uploading, + uploaded, + external, + deleted, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + + 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 invites = struct { + const UseCount = usize; + pub const Invite = struct { + 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 Kind = enum { + system, + community_owner, + user, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + 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 follows = struct { + 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 QueryResult = common.QueryResult(Follow, QueryArgs); +}; + +pub const notes = struct { + pub const Note = struct { + id: Uuid, + + author_id: actors.Actor, // TODO + content: []const u8, + created_at: DateTime, + + // TODO: This sucks + pub const sql_serialize = struct { + pub const @"author.profile_fields" = .json; + }; + }; + 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, + }; + pub const QueryResult = common.QueryResult(Note, QueryArgs); +}; + +pub const tokens = struct { + pub const Token = struct { + account_id: Uuid, + issued_at: DateTime, + + hash: []const u8, + }; +}; diff --git a/src/api/types.zig b/src/api/types.zig index 7bd3a13..cef3525 100644 --- a/src/api/types.zig +++ b/src/api/types.zig @@ -1,362 +1,5 @@ -const util = @import("util"); +const services = @import("../services.zig"); -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, +pub const actors = struct { + pub const Actor = services.actors.Actor; }; From b58266bdd83087506247ee2044c17fa731f649df Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Wed, 4 Jan 2023 11:03:23 -0800 Subject: [PATCH 4/9] Move api code into methods namespace --- src/api/lib.zig | 651 +++------------------ src/api/methods/actors.zig | 79 +++ src/api/methods/auth.zig | 41 +- src/api/methods/communities.zig | 75 +-- src/api/methods/drive.zig | 192 ++++++ src/api/methods/follows.zig | 83 +++ src/api/methods/invites.zig | 107 ++++ src/api/methods/notes.zig | 38 ++ src/api/methods/timelines.zig | 45 +- src/api/services.zig | 34 +- src/api/services/actors.zig | 4 +- src/api/services/drive.zig | 38 +- src/api/services/files.zig | 8 +- src/api/services/follows.zig | 50 +- src/api/services/invites.zig | 13 +- src/api/services/notes.zig | 6 +- src/api/services/types.zig | 33 +- src/api/types.zig | 198 ++++++- src/main/controllers.zig | 2 +- src/main/controllers/api/auth.zig | 8 +- src/main/controllers/api/communities.zig | 10 +- src/main/controllers/api/invites.zig | 2 +- src/main/controllers/api/timelines.zig | 8 +- src/main/controllers/api/users.zig | 8 +- src/main/controllers/api/users/follows.zig | 4 +- src/main/controllers/web.zig | 14 +- src/main/controllers/web/signup.tmpl.html | 4 +- 27 files changed, 922 insertions(+), 833 deletions(-) create mode 100644 src/api/methods/actors.zig create mode 100644 src/api/methods/drive.zig create mode 100644 src/api/methods/follows.zig create mode 100644 src/api/methods/invites.zig create mode 100644 src/api/methods/notes.zig diff --git a/src/api/lib.zig b/src/api/lib.zig index e6a815f..7e72810 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -7,17 +7,13 @@ const types = @import("./types.zig"); const DateTime = util.DateTime; const Uuid = util.Uuid; -const default_avatar = "static/default_avi.png"; - -const QueryResult = types.QueryResult; - pub usingnamespace types; -pub const Account = types.Account; +pub const Account = types.accounts.Account; pub const Actor = types.actors.Actor; -pub const Community = types.Community; -pub const Invite = types.Invite; -pub const Note = types.Note; -pub const Token = types.Token; +pub const Community = types.communities.Community; +pub const Invite = types.invites.Invite; +pub const Note = types.notes.Note; +pub const Token = types.tokens.Token; pub const ClusterMeta = struct { community_count: usize, @@ -25,171 +21,11 @@ pub const ClusterMeta = struct { note_count: usize, }; -pub const RegistrationOptions = struct { - invite_code: ?[]const u8 = null, - email: ?[]const u8 = null, -}; - -pub const InviteOptions = struct { - pub const Kind = Invite.Kind; - - name: ?[]const u8 = null, - lifespan: ?DateTime.Duration = null, - max_uses: ?usize = null, - - // admin only options - kind: Kind = .user, - to_community: ?Uuid = null, -}; - -pub const UserResponse = struct { - id: Uuid, - - username: []const u8, - host: []const u8, - - display_name: ?[]const u8, - bio: []const u8, - - avatar_file_id: ?Uuid, - avatar_url: []const u8, - - header_file_id: ?Uuid, - header_url: ?[]const u8, - - profile_fields: []const Actor.ProfileField, - - community_id: Uuid, - - created_at: DateTime, - updated_at: DateTime, -}; - -pub const NoteResponse = struct { - id: Uuid, - author: struct { - id: Uuid, - username: []const u8, - host: []const u8, - }, - - content: []const u8, - created_at: DateTime, -}; - -const FollowQueryArgs = struct { - pub const OrderBy = services.follows.QueryArgs.OrderBy; - pub const Direction = services.follows.QueryArgs.Direction; - pub const PageDirection = services.follows.QueryArgs.PageDirection; - pub const Prev = services.follows.QueryArgs.Prev; - - max_items: usize = 20, - - order_by: OrderBy = .created_at, - - direction: Direction = .descending, - - prev: ?Prev = null, - - page_direction: PageDirection = .forward, - - fn from(args: services.follows.QueryArgs) FollowQueryArgs { - return .{ - .max_items = args.max_items, - .order_by = args.order_by, - .direction = args.direction, - .prev = args.prev, - .page_direction = args.page_direction, - }; - } -}; - -const FollowQueryResult = struct { - items: []services.follows.Follow, - - prev_page: FollowQueryArgs, - next_page: FollowQueryArgs, -}; - -pub const FollowerQueryArgs = FollowQueryArgs; -pub const FollowerQueryResult = FollowQueryResult; -pub const FollowingQueryArgs = FollowQueryArgs; -pub const FollowingQueryResult = FollowQueryResult; - -pub const UploadFileArgs = struct { - filename: []const u8, - dir: []const u8, - description: ?[]const u8, - content_type: []const u8, - sensitive: bool, -}; - -pub const DriveEntry = union(enum) { - const Kind = services.drive.Kind; - dir: struct { - id: Uuid, - owner_id: Uuid, - name: ?[]const u8, - path: []const u8, - parent_directory_id: ?Uuid, - - kind: Kind = .dir, - - // If null = not enumerated - children: ?[]const DriveEntry, - }, - file: struct { - id: Uuid, - owner_id: Uuid, - name: ?[]const u8, - path: []const u8, - parent_directory_id: ?Uuid, - - kind: Kind = .file, - - meta: FileUpload, - }, -}; - -pub const FileUpload = types.FileUpload; - -pub const DriveGetResult = union(services.drive.Kind) { - dir: struct { - entry: DriveEntry, - children: []DriveEntry, - }, - file: struct { - entry: DriveEntry, - file: FileUpload, - }, -}; - -pub const FileResult = struct { - meta: FileUpload, - data: []const u8, -}; - -pub const InviteResponse = struct { - code: []const u8, - kind: Invite.Kind, - name: []const u8, - creator: UserResponse, - - url: []const u8, - - community_id: Uuid, - - created_at: DateTime, - expires_at: ?DateTime, - - times_used: usize, - max_uses: ?usize, -}; - pub fn isAdminSetup(db: sql.Db) !bool { - _ = services.communities.adminCommunityId(db) catch |err| switch (err) { + const svc = services.Services(sql.Db){ .db = db }; + _ = svc.getAdminCommunityId() catch |err| switch (err) { error.NotFound => return false, - else => return err, + else => |e| return e, }; return true; @@ -230,7 +66,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, services); + pub const Conn = ApiConn(sql.Db, method_impl); const root_username = "root"; @@ -243,15 +79,17 @@ pub const ApiSource = struct { pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn { const db = try self.db_conn_pool.acquire(); errdefer db.releaseConnection(); - const community = try services.communities.getByHost(db, host, alloc); - - return Conn{ + var conn = Conn{ .db = db, .context = .{ - .community = community, + .community = undefined, }, .allocator = alloc, }; + + conn.context.community = try conn.getServices().getCommunityByHost(alloc, host); + + return conn; } pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn { @@ -267,7 +105,7 @@ pub const ApiContext = struct { community: Community, pub fn userId(self: ApiContext) ?Uuid { - if (self.token_info) |t| return t.user_id else return null; + if (self.token_info) |t| return t.account_id else return null; } pub fn isAdmin(self: ApiContext) bool { @@ -275,12 +113,18 @@ pub const ApiContext = struct { } }; -const methods = struct { +const method_impl = struct { + const actors = @import("./methods/actors.zig"); const auth = @import("./methods/auth.zig"); + const communities = @import("./methods/communities.zig"); + const drive = @import("./methods/drive.zig"); + const follows = @import("./methods/follows.zig"); + const invites = @import("./methods/invites.zig"); + const notes = @import("./methods/notes.zig"); const timelines = @import("./methods/timelines.zig"); }; -fn ApiConn(comptime DbConn: type, comptime models: anytype) type { +fn ApiConn(comptime DbConn: type, comptime methods: anytype) type { return struct { const Self = @This(); const Services = @import("./services.zig").Services(DbConn); @@ -295,131 +139,22 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { self.db.releaseConnection(); } - fn isAdmin(self: *Self) bool { - // TODO - return self.context.userId() != null and self.context.community.kind == .admin; - } - fn getServices(self: *Self) Services { return Services{ .db = self.db }; } - pub const AuthorizationInfo = struct { - id: Uuid, - username: []const u8, - community_id: Uuid, - host: []const u8, - - issued_at: DateTime, - }; - pub fn verifyAuthorization(self: *Self) !AuthorizationInfo { - 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); - - const username = try util.deepClone(self.allocator, user.username); - errdefer util.deepFree(self.allocator, username); - - return AuthorizationInfo{ - .id = user.id, - .username = username, - .community_id = self.context.community.id, - .host = try util.deepClone(self.allocator, self.context.community.host), - - .issued_at = info.issued_at, - }; - } - - return error.TokenRequired; - } - - pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Community { - if (!self.isAdmin()) { - return error.PermissionDenied; - } - - const tx = try self.db.begin(); - errdefer tx.rollback(); - const community_id = try models.communities.create( - tx, + pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Uuid { + return try methods.communities.create( + self.allocator, + self.context, + self.getServices(), origin, - .{ .name = name }, - self.allocator, + name, ); - - const community = models.communities.get( - tx, - community_id, - self.allocator, - ) catch |err| return switch (err) { - error.NotFound => error.DatabaseError, - else => |err2| err2, - }; - - try tx.commit(); - - return community; } - pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse { - // Only logged in users can make invites - 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.context.community.id; - - // 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, options.name orelse "", .{ - .lifespan = options.lifespan, - .max_uses = options.max_uses, - .kind = options.kind, - }, 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 models.communities.get(self.db, cid, self.allocator); - defer util.deepFree(self.allocator, community); - - break :blk try std.fmt.allocPrint( - self.allocator, - "{s}://{s}/invite/{s}", - .{ @tagName(community.scheme), community.host, invite.code }, - ); - } else try std.fmt.allocPrint( - self.allocator, - "{s}://{s}/invite/{s}", - .{ @tagName(self.context.community.scheme), self.context.community.host, invite.code }, - ); - errdefer util.deepFree(self.allocator, url); - - const user = try self.getUserUnchecked(self.db, user_id); - - return InviteResponse{ - .code = invite.code, - .kind = invite.kind, - .name = invite.name, - .creator = user, - .url = url, - .community_id = invite.community_id, - .created_at = invite.created_at, - .expires_at = invite.expires_at, - .times_used = invite.times_used, - .max_uses = invite.max_uses, - }; - } - - 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 createInvite(self: *Self, options: types.invites.CreateOptions) !Uuid { + return methods.invites.create(self.allocator, self.context, self.getServices(), options); } pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { @@ -430,144 +165,53 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { self: *Self, username: []const u8, password: []const u8, - opt: methods.auth.RegistrationOptions, - ) !types.Actor { + opt: types.auth.RegistrationOptions, + ) !Uuid { return methods.auth.register(self.allocator, self.context, self.getServices(), username, password, opt); } - fn getUserUnchecked(self: *Self, db: anytype, user_id: Uuid) !UserResponse { - const user = try models.actors.get(db, user_id, self.allocator); - - const avatar_url = if (user.avatar_file_id) |fid| - try std.fmt.allocPrint( - self.allocator, - "{s}://{s}/media/{}", - .{ @tagName(self.context.community.scheme), self.context.community.host, fid }, - ) - else - try std.fmt.allocPrint( - self.allocator, - "{s}://{s}/{s}", - .{ @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.context.community.scheme), self.context.community.host, fid }, - ) - else - null; - - return UserResponse{ - .id = user.id, - - .username = user.username, - .host = user.host, - - .display_name = user.display_name, - .bio = user.bio, - - .avatar_file_id = user.avatar_file_id, - .avatar_url = avatar_url, - - .header_file_id = user.header_file_id, - .header_url = header_url, - - .profile_fields = user.profile_fields, - - .community_id = user.community_id, - - .created_at = user.created_at, - .updated_at = user.updated_at, - }; + pub fn getActor(self: *Self, user_id: Uuid) !Actor { + return methods.actors.get(self.allocator, self.context, self.getServices(), user_id); } - pub fn getUser(self: *Self, user_id: Uuid) !UserResponse { - const user = try self.getUserUnchecked(self.db, user_id); - errdefer util.deepFree(self.allocator, user); - - if (self.context.userId() == null) { - if (!Uuid.eql(self.context.community.id, user.community_id)) return error.NotFound; - } - - return user; - } - - pub fn createNote(self: *Self, content: []const u8) !Note { - // You cannot post on admin accounts - if (self.context.community.kind == .admin) return error.WrongCommunity; - - // Only authenticated users can post - 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) { - error.NotFound => error.Unexpected, - else => err, - }; + pub fn createNote(self: *Self, content: []const u8) !Uuid { + return methods.notes.create(self.allocator, self.context, self.getServices(), content); } pub fn getNote(self: *Self, note_id: Uuid) !Note { - const note = try models.notes.get(self.db, note_id, self.allocator); - errdefer util.deepFree(self.allocator, note); - - // Only serve community-specific notes on unauthenticated requests - if (self.context.userId() == null) { - if (!Uuid.eql(self.context.community.id, note.author.community_id)) return error.NotFound; - } - - return note; + return methods.notes.get(self.allocator, self.context, self.getServices(), note_id); } - 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 queryCommunities(self: *Self, args: types.communities.QueryArgs) !types.communities.QueryResult { + return methods.communities.query(self.allocator, self.context, self.getServices(), args); } - pub fn globalTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult { + pub fn globalTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult { return methods.timelines.globalTimeline(self.allocator, self.context, self.getServices(), args); } - pub fn localTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult { + pub fn localTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult { return methods.timelines.localTimeline(self.allocator, self.context, self.getServices(), args); } - pub fn homeTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult { + pub fn homeTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult { return methods.timelines.homeTimeline(self.allocator, self.context, self.getServices(), args); } - pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult { - var all_args = std.mem.zeroInit(models.follows.QueryArgs, args); - all_args.followee_id = user_id; - const result = try models.follows.query(self.db, all_args, self.allocator); - return FollowerQueryResult{ - .items = result.items, - .prev_page = FollowQueryArgs.from(result.prev_page), - .next_page = FollowQueryArgs.from(result.next_page), - }; + pub fn queryFollowers(self: *Self, user_id: Uuid, args: types.follows.FollowerQueryArgs) !types.follows.FollowerQueryResult { + return methods.follows.queryFollowers(self.allocator, self.context, self.getServices(), user_id, args); } - pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult { - var all_args = std.mem.zeroInit(models.follows.QueryArgs, args); - all_args.followed_by_id = user_id; - const result = try models.follows.query(self.db, all_args, self.allocator); - return FollowingQueryResult{ - .items = result.items, - .prev_page = FollowQueryArgs.from(result.prev_page), - .next_page = FollowQueryArgs.from(result.next_page), - }; + pub fn queryFollowing(self: *Self, user_id: Uuid, args: types.follows.FollowingQueryArgs) !types.follows.FollowingQueryResult { + return methods.follows.queryFollowing(self.allocator, self.context, self.getServices(), user_id, args); } pub fn follow(self: *Self, followee: Uuid) !void { - const result = try models.follows.create(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator); - defer util.deepFree(self.allocator, result); + try methods.follows.follow(self.allocator, self.context, self.getServices(), followee); } pub fn unfollow(self: *Self, followee: Uuid) !void { - const result = try models.follows.delete(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator); - defer util.deepFree(self.allocator, result); + try methods.follows.unfollow(self.allocator, self.context, self.getServices(), followee); } pub fn getClusterMeta(self: *Self) !ClusterMeta { @@ -587,201 +231,40 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type { ); } - fn backendDriveEntryToFrontend(self: *Self, entry: models.drive.Entry, recurse: bool) !DriveEntry { - return if (entry.file_id) |file_id| .{ - .file = .{ - .id = entry.id, - .owner_id = entry.owner_id, - .name = entry.name, - .path = entry.path, - .parent_directory_id = entry.parent_directory_id, - - .meta = try models.files.get(self.db, file_id, self.allocator), - }, - } else .{ - .dir = .{ - .id = entry.id, - .owner_id = entry.owner_id, - .name = entry.name, - .path = entry.path, - .parent_directory_id = entry.parent_directory_id, - - .children = blk: { - if (!recurse) break :blk null; - - 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); - return err; - }; - var count: usize = 0; - errdefer for (children) |child, i| { - if (i < count) - util.deepFree(self.allocator, result[i]) - else - util.deepFree(self.allocator, child); - }; - defer self.allocator.free(children); - errdefer self.allocator.free(result); - - for (children) |child, i| { - result[i] = try backendDriveEntryToFrontend(self, child, false); - count += 1; - } - - break :blk result; - }, - }, - }; + pub fn driveUpload(self: *Self, meta: types.drive.UploadArgs, body: []const u8) !Uuid { + return try methods.drive.upload(self.allocator, self.context, self.getServices(), meta, body); } - pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry { - 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, - .content_type = meta.content_type, - .sensitive = meta.sensitive, - }, body, self.allocator); - - const entry = entry: { - errdefer models.files.delete(self.db, file_id, self.allocator) catch |err| { - std.log.err("Unable to delete file {}: {}", .{ file_id, err }); - }; - - break :entry models.drive.create( - self.db, - user_id, - meta.dir, - meta.filename, - file_id, - self.allocator, - ) catch |err| switch (err) { - error.PathAlreadyExists => { - var buf: [256]u8 = undefined; - var split = std.mem.splitBackwards(u8, meta.filename, "."); - const ext = split.first(); - const name = split.rest(); - const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext }); - - break :entry try models.drive.create( - self.db, - user_id, - meta.dir, - new_name, - file_id, - self.allocator, - ); - }, - else => |e| return e, - }; - }; - errdefer util.deepFree(self.allocator, entry); - - return try self.backendDriveEntryToFrontend(entry, true); - } - - pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry { - 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 driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !Uuid { + return try methods.drive.mkdir(self.allocator, self.context, self.getServices(), parent_path, name); } pub fn driveDelete(self: *Self, path: []const u8) !void { - 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); - if (entry.file_id) |file_id| try models.files.delete(self.db, file_id, self.allocator); + return try methods.drive.delete(self.allocator, self.context, self.getServices(), path); } - pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry { - 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 driveMove(self: *Self, src: []const u8, dest: []const u8) !void { + return try methods.drive.move(self.allocator, self.context, self.getServices(), src, dest); } - pub fn driveGet(self: *Self, path: []const u8) !DriveEntry { - 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); - - return try self.backendDriveEntryToFrontend(entry, true); + pub fn driveGet(self: *Self, path: []const u8) !types.drive.DriveEntry { + return try methods.drive.get(self.allocator, self.context, self.getServices(), path); } - pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry { - 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); - - std.log.debug("{}", .{entry.id}); - try models.files.update(self.db, entry.file_id orelse return error.NotAFile, meta, self.allocator); - - return try self.driveGet(path); + pub fn driveUpdate(self: *Self, path: []const u8, meta: types.files.UpdateArgs) !void { + return try methods.drive.update(self.allocator, self.context, self.getServices(), path, meta); } - pub fn fileDereference(self: *Self, id: Uuid) !FileResult { - const meta = try models.files.get(self.db, id, self.allocator); - errdefer util.deepFree(self.allocator, meta); - - return FileResult{ - .meta = meta, - .data = try models.files.deref(self.allocator, id), - }; + pub fn fileDereference(self: *Self, id: Uuid) !types.files.DerefResult { + return try methods.drive.dereference(self.allocator, self.context, self.getServices(), id); } - pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void { - if (!Uuid.eql(id, self.context.userId() orelse return error.NoToken)) return error.AccessDenied; - try models.actors.updateProfile(self.db, id, data, self.allocator); + pub fn updateUserProfile(self: *Self, id: Uuid, data: types.actors.ProfileUpdateArgs) !void { + try methods.actors.updateProfile(self.allocator, self.context, self.getServices(), id, data); } - pub fn validateInvite(self: *Self, code: []const u8) !InviteResponse { - const invite = models.invites.getByCode( - self.db, - code, - self.context.community.id, - self.allocator, - ) catch |err| switch (err) { - error.NotFound => return error.InvalidInvite, - else => return error.DatabaseFailure, - }; - errdefer util.deepFree(self.allocator, invite); - - 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.context.community.scheme), self.context.community.host, invite.code }, - ); - errdefer util.deepFree(self.allocator, url); - - const creator = self.getUserUnchecked(self.db, invite.created_by) catch |err| switch (err) { - error.NotFound => return error.Unexpected, - else => return error.DatabaseFailure, - }; - - return InviteResponse{ - .code = invite.code, - .name = invite.name, - .kind = invite.kind, - .creator = creator, - - .url = url, - - .community_id = invite.community_id, - - .created_at = invite.created_at, - .expires_at = invite.expires_at, - - .times_used = invite.times_used, - .max_uses = invite.max_uses, - }; + pub fn validateInvite(self: *Self, code: []const u8) !Invite { + return try methods.invites.getByCode(self.allocator, self.context, self.getServices(), code); } }; } diff --git a/src/api/methods/actors.zig b/src/api/methods/actors.zig new file mode 100644 index 0000000..079c462 --- /dev/null +++ b/src/api/methods/actors.zig @@ -0,0 +1,79 @@ +const std = @import("std"); +const util = @import("util"); +const services = @import("../services.zig"); +const pkg = @import("../lib.zig"); + +const Uuid = util.Uuid; +const ApiContext = pkg.ApiContext; + +const default_avatar = "static/default_avi.png"; +pub fn get( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + id: Uuid, +) !pkg.Actor { + const actor = try svcs.getActor(alloc, id); + errdefer util.deepFree(alloc, actor); + + if (!Uuid.eql(actor.community_id, ctx.community.id) and ctx.userId() == null) { + return error.NotFound; + } + + const avatar_url = if (actor.avatar_file_id) |fid| + try std.fmt.allocPrint( + alloc, + "{s}://{s}/media/{}", + .{ @tagName(ctx.community.scheme), ctx.community.host, fid }, + ) + else + try std.fmt.allocPrint( + alloc, + "{s}://{s}/{s}", + .{ @tagName(ctx.community.scheme), ctx.community.host, default_avatar }, + ); + errdefer alloc.free(avatar_url); + const header_url = if (actor.header_file_id) |fid| + try std.fmt.allocPrint( + alloc, + "{s}://{s}/media/{}", + .{ @tagName(ctx.community.scheme), ctx.community.host, fid }, + ) + else + null; + errdefer alloc.free(header_url); + + return pkg.Actor{ + .id = actor.id, + + .username = actor.username, + .host = actor.host, + + .display_name = actor.display_name, + .bio = actor.bio, + + .avatar_file_id = actor.avatar_file_id, + .avatar_url = avatar_url, + + .header_file_id = actor.header_file_id, + .header_url = header_url, + + .profile_fields = actor.profile_fields, + + .community_id = actor.community_id, + + .created_at = actor.created_at, + .updated_at = actor.updated_at, + }; +} + +pub fn updateProfile( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + id: Uuid, + data: pkg.actors.ProfileUpdateArgs, +) !void { + if (!Uuid.eql(id, ctx.userId() orelse return error.NoToken)) return error.AccessDenied; + try svcs.updateActorProfile(alloc, id, data); +} diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index 5639385..80a462c 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -1,28 +1,19 @@ const std = @import("std"); const util = @import("util"); -const types = @import("../types.zig"); const pkg = @import("../lib.zig"); const services = @import("../services.zig"); +const invites = @import("./invites.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; const ApiContext = pkg.ApiContext; -const Invite = types.Invite; -const Token = types.Token; +const Invite = pkg.invites.Invite; +const Token = pkg.tokens.Token; -const RegistrationOptions = struct { - invite_code: ?[]const u8 = null, - email: ?[]const u8 = null, -}; +const RegistrationOptions = pkg.auth.RegistrationOptions; const AccountCreateOptions = services.accounts.CreateOptions; -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( alloc: std.mem.Allocator, ctx: ApiContext, @@ -30,7 +21,7 @@ pub fn register( username: []const u8, password: []const u8, opt: RegistrationOptions, -) !types.Actor { +) !Uuid { const tx = try svcs.beginTx(); errdefer tx.rollbackTx(); @@ -42,14 +33,14 @@ pub fn register( if (maybe_invite) |invite| { if (!Uuid.eql(invite.community_id, ctx.community.id)) return error.WrongCommunity; - if (!isInviteValid(invite)) return error.InvalidInvite; + if (!invites.isValid(invite)) return error.InvalidInvite; } const invite_kind = if (maybe_invite) |inv| inv.kind else .user; if (ctx.community.kind == .admin) @panic("Unimplmented"); - const user_id = try createLocalAccount( + const account_id = try createLocalAccount( alloc, tx, username, @@ -65,18 +56,12 @@ pub fn register( .user => {}, .system => @panic("System user invites unimplemented"), .community_owner => { - try tx.transferCommunityOwnership(ctx.community.id, user_id); + try tx.transferCommunityOwnership(ctx.community.id, account_id); }, } - const user = tx.getActor(alloc, user_id) catch |err| switch (err) { - error.NotFound => return error.Unexpected, - else => |e| return e, - }; - errdefer util.deepFree(alloc, user); - try tx.commitTx(); - return user; + return account_id; } pub fn createLocalAccount( @@ -108,7 +93,7 @@ pub fn verifyToken(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, tok const info = try svcs.getTokenByHash(alloc, hash, ctx.community.id); defer util.deepFree(alloc, info); - return .{ .user_id = info.account_id, .issued_at = info.issued_at }; + return .{ .account_id = info.account_id, .issued_at = info.issued_at }; } pub fn login( @@ -160,7 +145,7 @@ pub fn login( return .{ .value = token, .info = .{ - .user_id = info.account_id, + .account_id = info.account_id, .issued_at = info.issued_at, }, }; @@ -309,9 +294,9 @@ test "register" { } }; const actors = struct { - fn get(_: *TestDb, id: Uuid, alloc: std.mem.Allocator) anyerror!types.Actor { + fn get(_: *TestDb, id: Uuid, alloc: std.mem.Allocator) anyerror!pkg.Actor { try std.testing.expectEqual(uid, id); - return try util.deepClone(alloc, std.mem.zeroInit(types.Actor, .{ + return try util.deepClone(alloc, std.mem.zeroInit(pkg.Actor, .{ .id = id, .username = "root", .host = "example.com", diff --git a/src/api/methods/communities.zig b/src/api/methods/communities.zig index 466c120..6006af9 100644 --- a/src/api/methods/communities.zig +++ b/src/api/methods/communities.zig @@ -1,45 +1,48 @@ const std = @import("std"); const util = @import("util"); const types = @import("../types.zig"); +const pkg = @import("../lib.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const QueryResult = types.QueryResult; -const Community = types.Community; +const ApiContext = pkg.ApiContext; +const Community = types.communities.Community; +const QueryArgs = types.communities.QueryArgs; +const QueryResult = types.communities.QueryResult; -pub fn methods(comptime models: type) type { - return struct { - pub fn createCommunity(self: anytype, origin: []const u8, name: ?[]const u8) !Community { - if (!self.isAdmin()) { - return error.PermissionDenied; - } +pub fn create( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + origin: []const u8, + name: ?[]const u8, +) !Uuid { + if (!ctx.isAdmin()) { + return error.PermissionDenied; + } - const tx = try self.db.begin(); - errdefer tx.rollback(); - const community_id = try models.communities.create( - tx, - origin, - .{ .name = name }, - self.allocator, - ); - - const community = models.communities.get( - tx, - community_id, - self.allocator, - ) catch |err| return switch (err) { - error.NotFound => error.DatabaseError, - else => |err2| err2, - }; - - try tx.commit(); - - return community; - } - - pub fn queryCommunities(self: anytype, args: Community.QueryArgs) !QueryResult(Community) { - if (!self.context.isAdmin()) return error.PermissionDenied; - return try models.communities.query(self.db, args, self.allocator); - } - }; + return try svcs.createCommunity( + alloc, + origin, + .{ .name = name }, + ); +} + +pub fn get( + alloc: std.mem.Allocator, + _: ApiContext, + svcs: anytype, + id: Uuid, +) !Community { + return try svcs.getCommunity(alloc, id); +} + +pub fn query( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + args: QueryArgs, +) !QueryResult { + if (!ctx.isAdmin()) return error.PermissionDenied; + return try svcs.queryCommunities(alloc, args); } diff --git a/src/api/methods/drive.zig b/src/api/methods/drive.zig new file mode 100644 index 0000000..29b2df6 --- /dev/null +++ b/src/api/methods/drive.zig @@ -0,0 +1,192 @@ +const std = @import("std"); +const util = @import("util"); +const pkg = @import("../lib.zig"); +const services = @import("../services.zig"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const ApiContext = pkg.ApiContext; +const DriveEntry = pkg.drive.DriveEntry; + +pub fn upload( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + args: pkg.drive.UploadArgs, + body: []const u8, +) !Uuid { + const owner = ctx.userId() orelse return error.NoToken; + const file_id = try svcs.createFile(alloc, owner, .{ + .filename = args.filename, + .description = args.description, + .content_type = args.content_type, + .sensitive = args.sensitive, + }, body); + + const entry_id = entry: { + errdefer svcs.deleteFile(alloc, file_id) catch |err| { + std.log.err("Unable to delete file {}: {}", .{ file_id, err }); + }; + + break :entry svcs.createDriveEntry( + alloc, + owner, + args.dir, + args.filename, + file_id, + ) catch |err| switch (err) { + error.PathAlreadyExists => { + var buf: [256]u8 = undefined; + var split = std.mem.splitBackwards(u8, args.filename, "."); + const ext = split.first(); + const name = split.rest(); + const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext }); + + break :entry try svcs.createDriveEntry( + alloc, + owner, + args.dir, + new_name, + file_id, + ); + }, + else => |e| return e, + }; + }; + + return entry_id; +} + +pub fn mkdir( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + parent_path: []const u8, + name: []const u8, +) !Uuid { + const user_id = ctx.userId() orelse return error.NoToken; + return try svcs.createDriveEntry(alloc, user_id, parent_path, name, null); +} + +pub fn delete( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + path: []const u8, +) !void { + const user_id = ctx.userId() orelse return error.NoToken; + const entry = try svcs.statDriveEntry(alloc, user_id, path); + defer util.deepFree(alloc, entry); + try svcs.deleteDriveEntry(alloc, entry.id); + if (entry.file_id) |file_id| try svcs.deleteFile(alloc, file_id); +} + +pub fn move( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + src: []const u8, + dest: []const u8, +) !void { + const user_id = ctx.userId() orelse return error.NoToken; + try svcs.moveDriveEntry(alloc, user_id, src, dest); +} + +pub fn get( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + path: []const u8, +) !pkg.drive.DriveEntry { + const user_id = ctx.userId() orelse return error.NoToken; + const entry = try svcs.statDriveEntry(alloc, user_id, path); + defer util.deepFree(alloc, entry); + + return try convert(alloc, svcs, entry, true); +} + +// TODO: These next two functions are more about files than drive entries, consider refactor? + +pub fn update( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + path: []const u8, + meta: pkg.files.UpdateArgs, +) !void { + const user_id = ctx.userId() orelse return error.NoToken; + + const entry = try svcs.statDriveEntry(alloc, user_id, path); + defer util.deepFree(alloc, entry); + + try svcs.updateFileMetadata(alloc, entry.file_id orelse return error.NotAFile, meta); +} + +pub fn dereference( + alloc: std.mem.Allocator, + _: ApiContext, + svcs: anytype, + file_id: Uuid, +) !pkg.files.DerefResult { + const meta = try svcs.statFile(alloc, file_id); + errdefer util.deepFree(alloc, meta); + + return .{ + .meta = meta, + .data = try svcs.derefFile(alloc, file_id), + }; +} + +fn convert( + alloc: std.mem.Allocator, + svcs: anytype, + entry: services.drive.DriveEntry, + recurse: bool, +) !DriveEntry { + if (entry.file_id) |file_id| return .{ + .file = .{ + .id = entry.id, + .owner_id = entry.owner_id, + .name = entry.name, + .path = entry.path, + .parent_directory_id = entry.parent_directory_id, + + .meta = try svcs.statFile(alloc, file_id), + }, + } else return .{ + .dir = .{ + .id = entry.id, + .owner_id = entry.owner_id, + .name = entry.name, + .path = entry.path, + .parent_directory_id = entry.parent_directory_id, + + .children = blk: { + if (!recurse) break :blk null; + + const children = try svcs.listDriveEntry(alloc, entry.id); + + const result = alloc.alloc(DriveEntry, children.len) catch |err| { + util.deepFree(alloc, children); + return err; + }; + var count: usize = 0; + errdefer for (children) |child, i| { + if (i < count) + util.deepFree(alloc, result[i]) + else + util.deepFree(alloc, child); + }; + defer alloc.free(children); + errdefer alloc.free(result); + + for (children) |child, i| { + result[i] = try convert(alloc, svcs, child, false); + count += 1; + } + + break :blk result; + }, + }, + }; +} diff --git a/src/api/methods/follows.zig b/src/api/methods/follows.zig new file mode 100644 index 0000000..852d236 --- /dev/null +++ b/src/api/methods/follows.zig @@ -0,0 +1,83 @@ +const std = @import("std"); +const util = @import("util"); +const pkg = @import("../lib.zig"); +const services = @import("../services.zig"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const ApiContext = pkg.ApiContext; +const QueryArgs = services.follows.QueryArgs; +const FollowerQueryArgs = pkg.follows.FollowerQueryArgs; +const FollowingQueryArgs = pkg.follows.FollowingQueryArgs; + +pub fn follow( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + followee: Uuid, +) !void { + const user_id = ctx.userId() orelse return error.NoToken; + try svcs.createFollow(alloc, user_id, followee); +} + +pub fn unfollow( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + followee: Uuid, +) !void { + const user_id = ctx.userId() orelse return error.NoToken; + try svcs.deleteFollow(alloc, user_id, followee); +} + +pub fn queryFollowers( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + of: Uuid, + args: FollowerQueryArgs, +) !pkg.follows.FollowerQueryResult { + const user = try svcs.getActor(alloc, of); + defer util.deepFree(alloc, user); + if (!Uuid.eql(user.community_id, ctx.community.id) and ctx.userId() == null) return error.NotFound; + + var all_args = std.mem.zeroInit(QueryArgs, args); + all_args.followee_id = of; + const result = try svcs.queryFollows(alloc, all_args); + return .{ + .items = result.items, + .prev_page = convert(FollowerQueryArgs, result.prev_page), + .next_page = convert(FollowerQueryArgs, result.next_page), + }; +} + +pub fn queryFollowing( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + of: Uuid, + args: FollowingQueryArgs, +) !pkg.follows.FollowingQueryResult { + const user = try svcs.getActor(alloc, of); + defer util.deepFree(alloc, user); + if (!Uuid.eql(user.community_id, ctx.community.id) and ctx.userId() == null) return error.NotFound; + + var all_args = std.mem.zeroInit(QueryArgs, args); + all_args.followed_by_id = of; + const result = try svcs.queryFollows(alloc, all_args); + return .{ + .items = result.items, + .prev_page = convert(FollowingQueryArgs, result.prev_page), + .next_page = convert(FollowingQueryArgs, result.next_page), + }; +} + +fn convert(comptime T: type, args: QueryArgs) T { + return .{ + .max_items = args.max_items, + .order_by = args.order_by, + .direction = args.direction, + .prev = args.prev, + .page_direction = args.page_direction, + }; +} diff --git a/src/api/methods/invites.zig b/src/api/methods/invites.zig new file mode 100644 index 0000000..dc06724 --- /dev/null +++ b/src/api/methods/invites.zig @@ -0,0 +1,107 @@ +const std = @import("std"); +const util = @import("util"); +const services = @import("../services.zig"); +const pkg = @import("../lib.zig"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; +const ApiContext = pkg.ApiContext; +const CreateOptions = pkg.invites.CreateOptions; +const Invite = pkg.invites.Invite; + +pub fn create( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + options: CreateOptions, +) !Uuid { + // Only logged in users can make invites + const user_id = ctx.userId() orelse return error.TokenRequired; + + const community_id = if (options.to_community) |id| blk: { + // Only admins can send invites for other communities + if (!ctx.isAdmin()) return error.PermissionDenied; + + break :blk id; + } else ctx.community.id; + + // Users can only make user invites + if (options.kind != .user and !ctx.isAdmin()) return error.PermissionDenied; + + return try svcs.createInvite(alloc, .{ + .created_by = user_id, + .community_id = community_id, + .name = options.name, + .lifespan = options.lifespan, + .max_uses = options.max_uses, + .kind = options.kind, + }); +} + +pub fn isValid(invite: services.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; +} + +fn getInviteImpl( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + invite: services.invites.Invite, +) !Invite { + errdefer util.deepFree(alloc, invite); + + if (!Uuid.eql(invite.community_id, ctx.community.id) and !ctx.isAdmin()) return error.NotFound; + if (!isValid(invite)) return error.NotFound; + + const community = try svcs.getCommunity(alloc, invite.community_id); + defer util.deepFree(alloc, community); + + const url = try std.fmt.allocPrint( + alloc, + "{s}://{s}/invite/{s}", + .{ @tagName(community.scheme), community.host, invite.code }, + ); + errdefer util.deepFree(alloc, url); + + return Invite{ + .id = invite.id, + + .created_by = invite.created_by, + .community_id = invite.community_id, + .name = invite.name, + .code = invite.code, + .url = url, + + .created_at = invite.created_at, + .times_used = invite.times_used, + + .expires_at = invite.expires_at, + .max_uses = invite.max_uses, + + .kind = invite.kind, + }; +} + +pub fn get( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + id: Uuid, +) !Invite { + const invite = try svcs.getInvite(alloc, id); + + return getInviteImpl(alloc, ctx, svcs, invite); +} + +pub fn getByCode( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + code: []const u8, +) !Invite { + const invite = try svcs.getInviteByCode(alloc, code, ctx.community.id); + + return getInviteImpl(alloc, ctx, svcs, invite); +} diff --git a/src/api/methods/notes.zig b/src/api/methods/notes.zig new file mode 100644 index 0000000..5d9d3ff --- /dev/null +++ b/src/api/methods/notes.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const util = @import("util"); +const services = @import("../services.zig"); +const pkg = @import("../lib.zig"); + +const Uuid = util.Uuid; +const ApiContext = pkg.ApiContext; + +pub fn create( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + content: []const u8, +) !Uuid { + // You cannot post on admin accounts + if (ctx.community.kind == .admin) return error.WrongCommunity; + + // Only authenticated users can post + const user_id = ctx.userId() orelse return error.TokenRequired; + return try svcs.createNote(alloc, user_id, content); +} + +pub fn get( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + note_id: Uuid, +) !pkg.Note { + const note = try svcs.getNote(alloc, note_id); + errdefer util.deepFree(alloc, note); + + // Only serve community-specific notes on unauthenticated requests + if (ctx.userId() == null) { + if (!Uuid.eql(ctx.community.id, note.author.community_id)) return error.NotFound; + } + + return note; +} diff --git a/src/api/methods/timelines.zig b/src/api/methods/timelines.zig index a6a6f47..ba7bd1b 100644 --- a/src/api/methods/timelines.zig +++ b/src/api/methods/timelines.zig @@ -1,6 +1,7 @@ const std = @import("std"); const util = @import("util"); const pkg = @import("../lib.zig"); +const types = @import("../types.zig"); const ApiContext = pkg.ApiContext; const Uuid = util.Uuid; @@ -10,29 +11,17 @@ const services = @import("../services.zig"); const Note = services.Note; const QueryArgs = services.notes.QueryArgs; -pub const TimelineArgs = struct { - pub const PageDirection = QueryArgs.PageDirection; - pub const Prev = QueryArgs.Prev; +const TimelineArgs = types.timelines.TimelineArgs; - max_items: usize = 20, - - created_before: ?DateTime = null, - created_after: ?DateTime = null, - - prev: ?Prev = null, - - page_direction: PageDirection = .forward, - - fn from(args: QueryArgs) TimelineArgs { - return .{ - .max_items = args.max_items, - .created_before = args.created_before, - .created_after = args.created_after, - .prev = args.prev, - .page_direction = args.page_direction, - }; - } -}; +fn timelineArgs(args: services.notes.QueryArgs) TimelineArgs { + return .{ + .max_items = args.max_items, + .created_before = args.created_before, + .created_after = args.created_after, + .prev = args.prev, + .page_direction = args.page_direction, + }; +} pub const TimelineResult = struct { items: []Note, @@ -51,8 +40,8 @@ pub fn globalTimeline( const result = try svcs.queryNotes(alloc, all_args); return TimelineResult{ .items = result.items, - .prev_page = TimelineArgs.from(result.prev_page), - .next_page = TimelineArgs.from(result.next_page), + .prev_page = timelineArgs(result.prev_page), + .next_page = timelineArgs(result.next_page), }; } @@ -67,8 +56,8 @@ pub fn localTimeline( const result = try svcs.queryNotes(alloc, all_args); return TimelineResult{ .items = result.items, - .prev_page = TimelineArgs.from(result.prev_page), - .next_page = TimelineArgs.from(result.next_page), + .prev_page = timelineArgs(result.prev_page), + .next_page = timelineArgs(result.next_page), }; } @@ -85,7 +74,7 @@ pub fn homeTimeline( const result = try svcs.queryNotes(alloc, all_args); return TimelineResult{ .items = result.items, - .prev_page = TimelineArgs.from(result.prev_page), - .next_page = TimelineArgs.from(result.next_page), + .prev_page = timelineArgs(result.prev_page), + .next_page = timelineArgs(result.next_page), }; } diff --git a/src/api/services.zig b/src/api/services.zig index 82347eb..026bcb9 100644 --- a/src/api/services.zig +++ b/src/api/services.zig @@ -100,7 +100,7 @@ pub fn Services(comptime Db: type) type { alloc: std.mem.Allocator, actor_id: Uuid, new: types.actors.ProfileUpdateArgs, - ) !Actor { + ) !void { return try impl.actors.updateProfile(self.db, actor_id, new, alloc); } @@ -129,14 +129,22 @@ pub fn Services(comptime Db: type) type { return try impl.communities.getByHost(self.db, host, alloc); } - pub fn transferCommunityOwnership( - self: Self, - community_id: Uuid, - owner_id: Uuid, - ) !void { + pub fn getAdminCommunityId(self: Self) !Uuid { + return try impl.communities.adminCommunityId(self.db); + } + + pub fn transferCommunityOwnership(self: Self, community_id: Uuid, owner_id: Uuid) !void { return try impl.communities.transferOwnership(self.db, community_id, owner_id); } + pub fn queryCommunities( + self: Self, + alloc: std.mem.Allocator, + args: types.communities.QueryArgs, + ) !types.communities.QueryResult { + return try impl.communities.query(self.db, args, alloc); + } + pub fn statDriveEntry( self: Self, alloc: std.mem.Allocator, @@ -179,10 +187,9 @@ pub fn Services(comptime Db: type) type { pub fn listDriveEntry( self: Self, alloc: std.mem.Allocator, - owner_id: Uuid, - path: []const u8, + entry_id: Uuid, ) ![]DriveEntry { - return try impl.drive.list(self.db, owner_id, path, alloc); + return try impl.drive.list(self.db, entry_id, alloc); } pub fn createFile( @@ -224,7 +231,7 @@ pub fn Services(comptime Db: type) type { alloc: std.mem.Allocator, id: Uuid, meta: types.files.UpdateArgs, - ) !FileUpload { + ) !void { return try impl.files.update(self.db, id, meta, alloc); } @@ -257,12 +264,9 @@ pub fn Services(comptime Db: type) type { pub fn createInvite( self: Self, alloc: std.mem.Allocator, - created_by: Uuid, - community_id: Uuid, - name: []const u8, options: types.invites.CreateOptions, - ) !Invite { - return try impl.invites.create(self.db, created_by, community_id, name, options, alloc); + ) !Uuid { + return try impl.invites.create(self.db, options, alloc); } pub fn getInvite( diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index bc8578f..ba51472 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -7,7 +7,7 @@ const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -pub const Actor = types.Actor; +pub const Actor = types.actors.Actor; pub const CreateError = error{ UsernameTaken, @@ -124,7 +124,7 @@ 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: Actor.ProfileUpdateArgs, alloc: std.mem.Allocator) !void { +pub fn updateProfile(db: anytype, id: Uuid, new: types.actors.ProfileUpdateArgs, alloc: std.mem.Allocator) !void { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); diff --git a/src/api/services/drive.zig b/src/api/services/drive.zig index e5c0125..6457d54 100644 --- a/src/api/services/drive.zig +++ b/src/api/services/drive.zig @@ -1,30 +1,11 @@ const std = @import("std"); const util = @import("util"); const sql = @import("sql"); +const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; - -pub const DriveOwner = union(enum) { - user_id: Uuid, - community_id: Uuid, -}; - -pub const Entry = struct { - id: Uuid, - owner_id: Uuid, - name: ?[]const u8, - path: []const u8, - parent_directory_id: ?Uuid, - file_id: ?Uuid, - kind: Kind, -}; - -pub const Kind = enum { - dir, - file, - pub const jsonStringify = util.jsonSerializeEnumAsString; -}; +const Entry = types.drive.DriveEntry; pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator) !Entry { return (db.queryRow(Entry, @@ -42,7 +23,7 @@ pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator } /// Creates a file or directory -pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !Entry { +pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !Uuid { if (name.len == 0) return error.EmptyName; const id = Uuid.randV4(util.getThreadPrng()); @@ -66,18 +47,7 @@ pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_ try tx.commit(); - const path = try std.mem.join(alloc, "/", if (dir.len == 0) &.{ "", name } else &.{ "", dir, name }); - errdefer alloc.free(path); - - return Entry{ - .id = id, - .owner_id = owner, - .name = try util.deepClone(alloc, name), - .path = path, - .parent_directory_id = parent.id, - .file_id = file_id, - .kind = if (file_id) |_| .file else .dir, - }; + return id; } pub fn delete(db: anytype, id: Uuid, alloc: std.mem.Allocator) !void { diff --git a/src/api/services/files.zig b/src/api/services/files.zig index ca8361d..1fad5b9 100644 --- a/src/api/services/files.zig +++ b/src/api/services/files.zig @@ -5,7 +5,9 @@ const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const FileUpload = types.FileUpload; +const FileUpload = types.files.FileUpload; +const CreateOptions = types.files.CreateOptions; +const UpdateArgs = types.files.UpdateArgs; pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload { return try db.queryRow( @@ -30,7 +32,7 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload { ); } -pub fn update(db: anytype, id: Uuid, meta: FileUpload.UpdateArgs, alloc: std.mem.Allocator) !void { +pub fn update(db: anytype, id: Uuid, meta: UpdateArgs, alloc: std.mem.Allocator) !void { var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); @@ -57,7 +59,7 @@ pub fn update(db: anytype, id: Uuid, meta: FileUpload.UpdateArgs, alloc: std.mem }, alloc); } -pub fn create(db: anytype, owner_id: Uuid, meta: FileUpload.CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid { +pub fn create(db: anytype, owner_id: Uuid, meta: CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid { const id = Uuid.randV4(util.getThreadPrng()); const now = DateTime.now(); try db.insert("file_upload", .{ diff --git a/src/api/services/follows.zig b/src/api/services/follows.zig index 7da65b7..6c34d7b 100644 --- a/src/api/services/follows.zig +++ b/src/api/services/follows.zig @@ -1,20 +1,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 Follow = struct { - id: Uuid, - - followed_by_id: Uuid, - followee_id: Uuid, - - created_at: DateTime, -}; +const QueryArgs = types.follows.QueryArgs; +const QueryResult = types.follows.QueryResult; +const Follow = types.follows.Follow; pub fn create(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.mem.Allocator) !void { if (Uuid.eql(followed_by_id, followee_id)) return error.SelfFollow; @@ -46,41 +39,6 @@ pub fn delete(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.m const max_max_items = 100; -pub const QueryArgs = struct { - pub const Direction = common.Direction; - pub const PageDirection = common.PageDirection; - pub const Prev = std.meta.Child(std.meta.fieldInfo(@This(), .prev).field_type); - - pub const OrderBy = enum { - created_at, - }; - - max_items: usize = 20, - - followed_by_id: ?Uuid = null, - followee_id: ?Uuid = null, - - order_by: OrderBy = .created_at, - - direction: Direction = .descending, - - prev: ?struct { - id: Uuid, - order_val: union(OrderBy) { - created_at: DateTime, - }, - } = null, - - page_direction: PageDirection = .forward, -}; - -pub const QueryResult = struct { - items: []Follow, - - 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(); diff --git a/src/api/services/invites.zig b/src/api/services/invites.zig index dff5401..dddd74c 100644 --- a/src/api/services/invites.zig +++ b/src/api/services/invites.zig @@ -5,7 +5,7 @@ const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const Invite = types.Invite; +const Invite = types.invites.Invite; // 9 random bytes = 12 random b64 const rand_len = 8; @@ -16,10 +16,7 @@ const Decoder = std.base64.url_safe.Decoder; pub fn create( db: anytype, - created_by: Uuid, - community_id: Uuid, - name: []const u8, - options: Invite.InternalCreateOptions, + options: types.invites.CreateOptions, alloc: std.mem.Allocator, ) !Uuid { const id = Uuid.randV4(util.getThreadPrng()); @@ -38,9 +35,9 @@ pub fn create( .{ .id = id, - .created_by = created_by, - .community_id = community_id, - .name = name, + .created_by = options.created_by, + .community_id = options.community_id, + .name = options.name, .code = code, .max_uses = options.max_uses, diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 0d682c5..6bda6a8 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -5,9 +5,9 @@ const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const Note = types.Note; -const QueryArgs = Note.QueryArgs; -const QueryResult = types.QueryResult(Note); +const Note = types.notes.Note; +const QueryArgs = types.notes.QueryArgs; +const QueryResult = types.notes.QueryResult; pub const CreateError = error{ DatabaseFailure, diff --git a/src/api/services/types.zig b/src/api/services/types.zig index 7005d18..6db53e7 100644 --- a/src/api/services/types.zig +++ b/src/api/services/types.zig @@ -208,6 +208,15 @@ pub const drive = struct { pub const files = struct { pub const FileUpload = struct { + pub const Status = enum { + uploading, + uploaded, + external, + deleted, + + pub const jsonStringify = util.jsonSerializeEnumAsString; + }; + id: Uuid, owner_id: Uuid, @@ -224,15 +233,6 @@ pub const files = struct { updated_at: DateTime, }; - pub const Status = enum { - uploading, - uploaded, - external, - deleted, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - }; - pub const CreateOptions = struct { filename: []const u8, description: ?[]const u8, @@ -249,7 +249,7 @@ pub const files = struct { }; pub const invites = struct { - const UseCount = usize; + pub const UseCount = usize; pub const Invite = struct { id: Uuid, @@ -273,15 +273,10 @@ pub const invites = struct { pub const jsonStringify = util.jsonSerializeEnumAsString; }; - 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 { + pub const CreateOptions = struct { + created_by: Uuid, + community_id: Uuid, name: ?[]const u8 = null, max_uses: ?UseCount = null, lifespan: ?DateTime.Duration = null, @@ -333,7 +328,7 @@ pub const notes = struct { pub const Note = struct { id: Uuid, - author_id: actors.Actor, // TODO + author: actors.Actor, // TODO content: []const u8, created_at: DateTime, diff --git a/src/api/types.zig b/src/api/types.zig index cef3525..e072f3d 100644 --- a/src/api/types.zig +++ b/src/api/types.zig @@ -1,5 +1,199 @@ -const services = @import("../services.zig"); +const util = @import("util"); +const services = @import("./services.zig"); + +const Uuid = util.Uuid; +const DateTime = util.DateTime; + +fn QueryResult(comptime R: type, comptime A: type) type { + return struct { + items: []R, + + next_page: A, + prev_page: A, + }; +} + +pub const auth = struct { + pub const RegistrationOptions = struct { + invite_code: ?[]const u8 = null, + email: ?[]const u8 = null, + }; +}; pub const actors = struct { - pub const Actor = services.actors.Actor; + pub const Actor = struct { + id: Uuid, + + username: []const u8, + host: []const u8, + + display_name: ?[]const u8, + bio: []const u8, + + avatar_file_id: ?Uuid, + avatar_url: []const u8, + + header_file_id: ?Uuid, + header_url: ?[]const u8, + + profile_fields: []const ProfileField, + + community_id: Uuid, + + created_at: DateTime, + updated_at: DateTime, + }; + pub const ProfileField = services.actors.ProfileField; + pub const ProfileUpdateArgs = services.actors.ProfileUpdateArgs; +}; + +pub const communities = struct { + pub const Community = services.communities.Community; + pub const QueryArgs = services.communities.QueryArgs; + pub const QueryResult = services.communities.QueryResult; +}; + +pub const drive = struct { + pub const DriveEntry = union(enum) { + pub const Kind = services.drive.DriveEntry.Kind; + dir: struct { + id: Uuid, + owner_id: Uuid, + name: ?[]const u8, + path: []const u8, + parent_directory_id: ?Uuid, + + kind: Kind = .dir, + + // If null = not enumerated + children: ?[]const DriveEntry, + }, + file: struct { + id: Uuid, + owner_id: Uuid, + name: ?[]const u8, + path: []const u8, + parent_directory_id: ?Uuid, + + kind: Kind = .file, + + meta: files.FileUpload, + }, + }; + + pub const UploadArgs = struct { + filename: []const u8, + dir: []const u8, + description: ?[]const u8, + content_type: []const u8, + sensitive: bool, + }; +}; + +pub const files = struct { + pub const FileUpload = services.files.FileUpload; + pub const UpdateArgs = services.files.UpdateArgs; + + pub const DerefResult = struct { + meta: FileUpload, + data: []const u8, + }; +}; + +pub const follows = struct { + pub const Follow = services.follows.Follow; + + const QueryArgs = struct { + pub const OrderBy = services.follows.QueryArgs.OrderBy; + pub const Direction = services.follows.QueryArgs.Direction; + pub const PageDirection = services.follows.QueryArgs.PageDirection; + pub const Prev = services.follows.QueryArgs.Prev; + + max_items: usize = 20, + + order_by: OrderBy = .created_at, + + direction: Direction = .descending, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + }; + + pub const FollowerQueryArgs = QueryArgs; + pub const FollowingQueryArgs = QueryArgs; + + pub const FollowerQueryResult = QueryResult(Follow, FollowerQueryArgs); + pub const FollowingQueryResult = QueryResult(Follow, FollowingQueryArgs); +}; + +pub const invites = struct { + pub const UseCount = services.invites.UseCount; + pub const Invite = struct { + id: Uuid, + + created_by: Uuid, // User ID + community_id: Uuid, + name: []const u8, + code: []const u8, + url: []const u8, + + created_at: DateTime, + times_used: UseCount, + + expires_at: ?DateTime, + max_uses: ?UseCount, + + kind: Kind, + }; + pub const Kind = services.invites.Kind; + pub const CreateOptions = struct { + name: ?[]const u8 = null, + lifespan: ?DateTime.Duration = null, + max_uses: ?usize = null, + + // admin only options + kind: Kind = .user, + to_community: ?Uuid = null, + }; +}; + +pub const notes = struct { + pub const Note = services.notes.Note; + pub const QueryArgs = services.notes.QueryArgs; +}; + +pub const timelines = struct { + pub const TimelineArgs = struct { + pub const PageDirection = notes.QueryArgs.PageDirection; + pub const Prev = notes.QueryArgs.Prev; + + max_items: usize = 20, + + created_before: ?DateTime = null, + created_after: ?DateTime = null, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + }; + + pub const TimelineResult = struct { + items: []notes.Note, + + prev_page: TimelineArgs, + next_page: TimelineArgs, + }; +}; + +pub const tokens = struct { + pub const Token = struct { + pub const Info = struct { + account_id: Uuid, + issued_at: DateTime, + }; + + value: []const u8, + info: Info, + }; }; diff --git a/src/main/controllers.zig b/src/main/controllers.zig index d1aa26e..1efadb4 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.context.userId()) |uid| try srv.getUser(uid) else null; + const user = if (srv.context.userId()) |uid| try srv.getActor(uid) else null; defer util.deepFree(srv.allocator, user); var stream = try self.open(status_code); diff --git a/src/main/controllers/api/auth.zig b/src/main/controllers/api/auth.zig index 1b6e652..560b19c 100644 --- a/src/main/controllers/api/auth.zig +++ b/src/main/controllers/api/auth.zig @@ -24,8 +24,10 @@ pub const verify_login = struct { pub const path = "/auth/login"; pub fn handler(_: anytype, res: anytype, srv: anytype) !void { - const info = try srv.verifyAuthorization(); - - try res.json(.ok, info); + if (srv.context.token_info) |token| { + return try res.json(.ok, token); + } else { + return try res.status(.unauthorized); + } } }; diff --git a/src/main/controllers/api/communities.zig b/src/main/controllers/api/communities.zig index 6c79355..2d8a74d 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.Community.QueryArgs; +const QueryArgs = api.communities.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.Community.QueryArgs.OrderBy; - const Direction = api.Community.QueryArgs.Direction; - const PageDirection = api.Community.QueryArgs.PageDirection; + const OrderBy = QueryArgs.OrderBy; + const Direction = QueryArgs.Direction; + const PageDirection = 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.Community.QueryArgs) Query { + fn func(args: QueryArgs) Query { return .{ .max_items = args.max_items, .owner_id = args.owner_id, diff --git a/src/main/controllers/api/invites.zig b/src/main/controllers/api/invites.zig index 0355bcf..6ee88ec 100644 --- a/src/main/controllers/api/invites.zig +++ b/src/main/controllers/api/invites.zig @@ -4,7 +4,7 @@ pub const create = struct { pub const method = .POST; pub const path = "/invites"; - pub const Body = api.InviteOptions; + pub const Body = api.invites.CreateOptions; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { // No need to free because it will be freed when the api conn diff --git a/src/main/controllers/api/timelines.zig b/src/main/controllers/api/timelines.zig index df41599..d14f695 100644 --- a/src/main/controllers/api/timelines.zig +++ b/src/main/controllers/api/timelines.zig @@ -2,11 +2,13 @@ const std = @import("std"); const api = @import("api"); const controller_utils = @import("../../controllers.zig").helpers; +const TimelineArgs = api.timelines.TimelineArgs; + pub const global = struct { pub const method = .GET; pub const path = "/timelines/global"; - pub const Query = api.TimelineArgs; + pub const Query = TimelineArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.globalTimeline(req.query); @@ -18,7 +20,7 @@ pub const local = struct { pub const method = .GET; pub const path = "/timelines/local"; - pub const Query = api.TimelineArgs; + pub const Query = TimelineArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.localTimeline(req.query); @@ -30,7 +32,7 @@ pub const home = struct { pub const method = .GET; pub const path = "/timelines/home"; - pub const Query = api.TimelineArgs; + pub const Query = TimelineArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.homeTimeline(req.query); diff --git a/src/main/controllers/api/users.zig b/src/main/controllers/api/users.zig index 450c652..3bf0190 100644 --- a/src/main/controllers/api/users.zig +++ b/src/main/controllers/api/users.zig @@ -35,7 +35,7 @@ pub const get = struct { }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const result = try srv.getUser(req.args.id); + const result = try srv.getActor(req.args.id); defer util.deepFree(srv.allocator, result); try res.json(.ok, result); @@ -50,13 +50,13 @@ pub const update_profile = struct { id: util.Uuid, }; - pub const Body = api.Actor.ProfileUpdateArgs; + pub const Body = api.actors.ProfileUpdateArgs; - // TODO: I don't like that the request body dn response body are substantially different + // TODO: I don't like that the request body and response body are substantially different pub fn handler(req: anytype, res: anytype, srv: anytype) !void { try srv.updateUserProfile(req.args.id, req.body); - const result = try srv.getUser(req.args.id); + const result = try srv.getActor(req.args.id); defer util.deepFree(srv.allocator, result); try res.json(.ok, result); diff --git a/src/main/controllers/api/users/follows.zig b/src/main/controllers/api/users/follows.zig index 4ac3799..07a42b3 100644 --- a/src/main/controllers/api/users/follows.zig +++ b/src/main/controllers/api/users/follows.zig @@ -42,7 +42,7 @@ pub const query_followers = struct { id: Uuid, }; - pub const Query = api.FollowingQueryArgs; + pub const Query = api.follows.FollowingQueryArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.queryFollowers(req.args.id, req.query); @@ -59,7 +59,7 @@ pub const query_following = struct { id: Uuid, }; - pub const Query = api.FollowerQueryArgs; + pub const Query = api.follows.FollowerQueryArgs; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const results = try srv.queryFollowing(req.args.id, req.query); diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 342ff31..b23af48 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -114,14 +114,19 @@ const signup = struct { srv: anytype, ) !void { const invite = if (invite_code) |code| srv.validateInvite(code) catch |err| switch (err) { - error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv), + //error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv), else => |e| return e, } else null; defer util.deepFree(srv.allocator, invite); + const creator = if (invite) |inv| try srv.getActor(inv.created_by) else null; + defer util.deepFree(srv.allocator, creator); try res.template(status, srv, tmpl, .{ .error_msg = error_msg, - .invite = invite, + .invite = if (invite) |inv| .{ + .meta = inv, + .creator = creator.?, + } else null, }); } @@ -223,7 +228,7 @@ const user_details = struct { }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const user = try srv.getUser(req.args.id); + const user = try srv.getActor(req.args.id); defer util.deepFree(srv.allocator, user); try res.template(.ok, srv, tmpl, user); @@ -397,7 +402,8 @@ const cluster = struct { }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const community = try srv.createCommunity(req.body.origin, req.body.name); + const cid = try srv.createCommunity(req.body.origin, req.body.name); + const community = try srv.getCommunity(cid); defer util.deepFree(srv.allocator, community); const invite = try srv.createInvite(.{ diff --git a/src/main/controllers/web/signup.tmpl.html b/src/main/controllers/web/signup.tmpl.html index 8eae435..b7c38ac 100644 --- a/src/main/controllers/web/signup.tmpl.html +++ b/src/main/controllers/web/signup.tmpl.html @@ -10,7 +10,7 @@
You are about to accept an invite from:
{#template mini-user $invite.creator} - {#if @isTag($invite.kind, community_owner) =} + {#if @isTag($invite.meta.kind, community_owner) =}
This act will make your new account the owner of { %community.name }.
{/if =}
@@ -39,7 +39,7 @@ {#if .invite |$invite| =} - + {/if =} From 303bce771d880c527c80cbfc7cf07155f7a1c13a Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Wed, 4 Jan 2023 11:25:08 -0800 Subject: [PATCH 5/9] Fix compile errors --- src/api/lib.zig | 12 +++++++++++ src/api/methods/drive.zig | 14 +++++++++++++ src/api/services.zig | 8 ++++++++ src/api/services/drive.zig | 33 ++++++++++++++++++++++-------- src/main/controllers/api/drive.zig | 8 ++++++-- src/main/controllers/web.zig | 10 +++++---- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 7e72810..29f18d2 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -143,6 +143,10 @@ fn ApiConn(comptime DbConn: type, comptime methods: anytype) type { return Services{ .db = self.db }; } + pub fn getCommunity(self: *Self, id: Uuid) !types.communities.Community { + return try methods.communities.get(self.allocator, self.context, self.getServices(), id); + } + pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Uuid { return try methods.communities.create( self.allocator, @@ -157,6 +161,10 @@ fn ApiConn(comptime DbConn: type, comptime methods: anytype) type { return methods.invites.create(self.allocator, self.context, self.getServices(), options); } + pub fn getInvite(self: *Self, id: Uuid) !Invite { + return try methods.invites.get(self.allocator, self.context, self.getServices(), id); + } + pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { return methods.auth.login(self.allocator, self.context, self.getServices(), username, password); } @@ -251,6 +259,10 @@ fn ApiConn(comptime DbConn: type, comptime methods: anytype) type { return try methods.drive.get(self.allocator, self.context, self.getServices(), path); } + pub fn driveGetEntryById(self: *Self, id: Uuid) !types.drive.DriveEntry { + return try methods.drive.getById(self.allocator, self.context, self.getServices(), id); + } + pub fn driveUpdate(self: *Self, path: []const u8, meta: types.files.UpdateArgs) !void { return try methods.drive.update(self.allocator, self.context, self.getServices(), path, meta); } diff --git a/src/api/methods/drive.zig b/src/api/methods/drive.zig index 29b2df6..2607a16 100644 --- a/src/api/methods/drive.zig +++ b/src/api/methods/drive.zig @@ -105,6 +105,20 @@ pub fn get( return try convert(alloc, svcs, entry, true); } +pub fn getById( + alloc: std.mem.Allocator, + ctx: ApiContext, + svcs: anytype, + id: Uuid, +) !pkg.drive.DriveEntry { + const user_id = ctx.userId() orelse return error.NoToken; + const entry = try svcs.getDriveEntry(alloc, id); + defer util.deepFree(alloc, entry); + if (!Uuid.eql(entry.owner_id, user_id)) return error.NotFound; + + return try convert(alloc, svcs, entry, true); +} + // TODO: These next two functions are more about files than drive entries, consider refactor? pub fn update( diff --git a/src/api/services.zig b/src/api/services.zig index 026bcb9..dbaf690 100644 --- a/src/api/services.zig +++ b/src/api/services.zig @@ -154,6 +154,14 @@ pub fn Services(comptime Db: type) type { return try impl.drive.stat(self.db, owner_id, path, alloc); } + pub fn getDriveEntry( + self: Self, + alloc: std.mem.Allocator, + id: Uuid, + ) !DriveEntry { + return try impl.drive.get(self.db, id, alloc); + } + pub fn createDriveEntry( self: Self, alloc: std.mem.Allocator, diff --git a/src/api/services/drive.zig b/src/api/services/drive.zig index 6457d54..d4f21f1 100644 --- a/src/api/services/drive.zig +++ b/src/api/services/drive.zig @@ -7,19 +7,36 @@ const Uuid = util.Uuid; const DateTime = util.DateTime; const Entry = types.drive.DriveEntry; -pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator) !Entry { - return (db.queryRow(Entry, +fn doGetQuery(db: anytype, comptime clause: []const u8, args: anytype, alloc: std.mem.Allocator) !Entry { + const q = std.fmt.comptimePrint( \\SELECT id, path, owner_id, name, file_id, kind, parent_directory_id \\FROM drive_entry_path - \\WHERE owner_id = $1 AND path = ('/' || $2) + \\WHERE {s} \\LIMIT 1 - , .{ - owner, - std.mem.trim(u8, path, "/"), - }, alloc) catch |err| switch (err) { + , + .{clause}, + ); + + return db.queryRow(Entry, q, args, alloc) catch |err| switch (err) { error.NoRows => return error.NotFound, else => |e| return e, - }); + }; +} + +pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator) !Entry { + return try doGetQuery( + db, + "owner_id = $1 AND path = ('/' || $2)", + .{ + owner, + std.mem.trim(u8, path, "/"), + }, + alloc, + ); +} + +pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !Entry { + return try doGetQuery(db, "id = $1", .{id}, alloc); } /// Creates a file or directory diff --git a/src/main/controllers/api/drive.zig b/src/main/controllers/api/drive.zig index 17e8a27..8b7aad3 100644 --- a/src/main/controllers/api/drive.zig +++ b/src/main/controllers/api/drive.zig @@ -98,12 +98,14 @@ pub const update = struct { }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const result = try srv.driveUpdate(req.args.path, .{ + try srv.driveUpdate(req.args.path, .{ .filename = req.body.meta.filename, .description = req.body.meta.description, .content_type = req.body.meta.content_type, .sensitive = req.body.meta.sensitive, }); + + const result = try srv.driveGet(req.args.path); defer util.deepFree(srv.allocator, result); try res.json(.ok, result); } @@ -117,7 +119,9 @@ pub const move = struct { pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const destination = req.headers.get("Destination") orelse return error.NoDestination; - const result = try srv.driveMove(req.args.path, destination); + try srv.driveMove(req.args.path, destination); + + const result = try srv.driveGet(req.args.path); defer util.deepFree(srv.allocator, result); try res.headers.put("Location", destination); diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index b23af48..4c83aaa 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -341,7 +341,7 @@ const drive = struct { return res.status(.see_other); }, .upload => |body| { - const entry = try srv.driveUpload( + const entry_id = try srv.driveUpload( .{ .filename = body.file.filename, .dir = trimmed_path, @@ -351,6 +351,7 @@ const drive = struct { }, body.file.data, ); + const entry = try srv.driveGetEntryById(entry_id); defer util.deepFree(srv.allocator, entry); const url = try std.fmt.allocPrint(srv.allocator, "{s}/drive/{s}", .{ @@ -402,16 +403,17 @@ const cluster = struct { }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const cid = try srv.createCommunity(req.body.origin, req.body.name); - const community = try srv.getCommunity(cid); + const comm_id = try srv.createCommunity(req.body.origin, req.body.name); + const community = try srv.getCommunity(comm_id); defer util.deepFree(srv.allocator, community); - const invite = try srv.createInvite(.{ + const invite_id = try srv.createInvite(.{ .max_uses = 1, .kind = .community_owner, .to_community = community.id, }); + const invite = try srv.getInvite(invite_id); defer util.deepFree(srv.allocator, invite); try res.template(.ok, srv, success_tmpl, .{ .community = community, .invite = invite }); From da558ac40e2c7383123557d66671d1dadad47483 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 8 Jan 2023 15:34:54 -0800 Subject: [PATCH 6/9] Add test plug for DateTime.now --- src/util/DateTime.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/util/DateTime.zig b/src/util/DateTime.zig index 2b096fa..be35aa6 100644 --- a/src/util/DateTime.zig +++ b/src/util/DateTime.zig @@ -67,7 +67,14 @@ pub fn parseRfc3339(str: []const u8) !DateTime { }; } +const is_test = @import("builtin").is_test; +const test_utils = struct { + pub threadlocal var test_now_timestamp: i64 = 1356076800; +}; +pub usingnamespace if (is_test) test_utils else struct {}; + pub fn now() DateTime { + if (comptime is_test) return .{ .seconds_since_epoch = test_utils.test_now_timestamp }; return .{ .seconds_since_epoch = std.time.timestamp() }; } From 25710435803b7b0edd0f48917790a89144aaace2 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 8 Jan 2023 15:35:13 -0800 Subject: [PATCH 7/9] Fix dependencies for unit tests --- build.zig | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 26d44c5..df7c55d 100644 --- a/build.zig +++ b/build.zig @@ -99,10 +99,16 @@ pub fn build(b: *std.build.Builder) !void { exe.addSystemIncludePath("/usr/include/"); exe.addSystemIncludePath("/usr/include/postgresql"); // HACK + const unittest_options = b.addOptions(); + unittest_options.addOption(bool, "enable_sqlite", false); + unittest_options.addOption(bool, "enable_postgres", false); + + const unittest_pkgs = makePkgs(b, unittest_options.getPackage("build_options")); + const unittest_http_cmd = b.step("unit:http", "Run tests for http package"); const unittest_http = b.addTest("src/http/lib.zig"); unittest_http_cmd.dependOn(&unittest_http.step); - unittest_http.addPackage(pkgs.util); + unittest_http.addPackage(unittest_pkgs.util); const unittest_util_cmd = b.step("unit:util", "Run tests for util package"); const unittest_util = b.addTest("src/util/lib.zig"); @@ -111,7 +117,8 @@ pub fn build(b: *std.build.Builder) !void { const unittest_sql_cmd = b.step("unit:sql", "Run tests for sql package"); const unittest_sql = b.addTest("src/sql/lib.zig"); unittest_sql_cmd.dependOn(&unittest_sql.step); - unittest_sql.addPackage(pkgs.util); + unittest_sql.addPackage(unittest_pkgs.util); + //unittest_sql.linkLibC(); const unittest_template_cmd = b.step("unit:template", "Run tests for template package"); const unittest_template = b.addTest("src/template/lib.zig"); @@ -120,8 +127,9 @@ pub fn build(b: *std.build.Builder) !void { 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); + unittest_api.addPackage(unittest_pkgs.util); + unittest_api.addPackage(unittest_pkgs.sql); + //unittest_api.linkLibC(); //const util_tests = b.addTest("src/util/lib.zig"); //const sql_tests = b.addTest("src/sql/lib.zig"); From 3a52aad023e03aee340b1e8ebe7ec18d86ca30b8 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 8 Jan 2023 15:35:58 -0800 Subject: [PATCH 8/9] Add tests for registration api call --- src/api/lib.zig | 15 +- src/api/methods/auth.zig | 331 +++++++++++++++++++---------- src/api/services.zig | 6 +- src/api/services/accounts.zig | 18 +- src/api/services/types.zig | 4 +- src/api/types.zig | 2 + src/main/controllers/api/users.zig | 4 +- 7 files changed, 249 insertions(+), 131 deletions(-) diff --git a/src/api/lib.zig b/src/api/lib.zig index 29f18d2..e046498 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -8,7 +8,6 @@ const DateTime = util.DateTime; const Uuid = util.Uuid; pub usingnamespace types; -pub const Account = types.accounts.Account; pub const Actor = types.actors.Actor; pub const Community = types.communities.Community; pub const Invite = types.invites.Invite; @@ -47,10 +46,12 @@ pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password const user = try @import("./methods/auth.zig").createLocalAccount( arena.allocator(), tx, - username, - password, - community_id, - .{ .role = .admin }, + .{ + .username = username, + .password = password, + .community_id = community_id, + .role = .admin, + }, ); try tx.transferCommunityOwnership(community_id, user); @@ -280,3 +281,7 @@ fn ApiConn(comptime DbConn: type, comptime methods: anytype) type { } }; } + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/api/methods/auth.zig b/src/api/methods/auth.zig index 80a462c..0d085d6 100644 --- a/src/api/methods/auth.zig +++ b/src/api/methods/auth.zig @@ -4,29 +4,29 @@ const pkg = @import("../lib.zig"); const services = @import("../services.zig"); const invites = @import("./invites.zig"); +const Allocator = std.mem.Allocator; const Uuid = util.Uuid; const DateTime = util.DateTime; const ApiContext = pkg.ApiContext; -const Invite = pkg.invites.Invite; const Token = pkg.tokens.Token; - const RegistrationOptions = pkg.auth.RegistrationOptions; -const AccountCreateOptions = services.accounts.CreateOptions; +const Invite = services.invites.Invite; pub fn register( alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, - username: []const u8, - password: []const u8, opt: RegistrationOptions, ) !Uuid { const tx = try svcs.beginTx(); errdefer tx.rollbackTx(); const maybe_invite = if (opt.invite_code) |code| - try tx.getInviteByCode(alloc, code, ctx.community.id) + tx.getInviteByCode(alloc, code, ctx.community.id) catch |err| switch (err) { + error.NotFound => return error.InvalidInvite, + else => |e| return e, + } else null; defer if (maybe_invite) |inv| util.deepFree(alloc, inv); @@ -43,10 +43,10 @@ pub fn register( const account_id = try createLocalAccount( alloc, tx, - username, - password, - ctx.community.id, .{ + .username = opt.username, + .password = opt.password, + .community_id = ctx.community.id, .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, .email = opt.email, }, @@ -64,22 +64,34 @@ pub fn register( return account_id; } -pub fn createLocalAccount( - alloc: std.mem.Allocator, - svcs: anytype, +pub const AccountCreateArgs = struct { username: []const u8, password: []const u8, community_id: Uuid, - opt: AccountCreateOptions, + invite_id: ?Uuid = null, + email: ?[]const u8 = null, + role: services.accounts.Role = .user, +}; + +pub fn createLocalAccount( + alloc: std.mem.Allocator, + svcs: anytype, + args: AccountCreateArgs, ) !Uuid { const tx = try svcs.beginTx(); errdefer tx.rollbackTx(); - const hash = try hashPassword(password, alloc); + const hash = try hashPassword(args.password, alloc); defer alloc.free(hash); - const id = try tx.createActor(alloc, username, community_id, false); - try tx.createAccount(alloc, id, hash, opt); + const id = try tx.createActor(alloc, args.username, args.community_id, false); + try tx.createAccount(alloc, .{ + .for_actor = id, + .password_hash = hash, + .invite_id = args.invite_id, + .email = args.email, + .role = args.role, + }); try tx.commitTx(); @@ -156,7 +168,7 @@ pub fn login( // password hashing. // Attempting to calculate/verify a hash will use about 50mb of work space. const scrypt = std.crypto.pwhash.scrypt; -const password_hash_len = 128; +const max_password_hash_len = 128; fn verifyPassword( hash: []const u8, password: []const u8, @@ -167,23 +179,33 @@ fn verifyPassword( password, .{ .allocator = alloc }, ) catch |err| return switch (err) { - error.PasswordVerificationFailed => error.InvalidLogin, - else => error.HashFailure, + error.PasswordVerificationFailed => return error.InvalidLogin, + error.OutOfMemory => return error.OutOfMemory, + else => |e| return e, }; } +const scrypt_params = if (!@import("builtin").is_test) + scrypt.Params.interactive +else + scrypt.Params{ + .ln = 8, + .r = 8, + .p = 1, + }; fn hashPassword(password: []const u8, alloc: std.mem.Allocator) ![]const u8 { - const buf = try alloc.alloc(u8, password_hash_len); - errdefer alloc.free(buf); - return scrypt.strHash( + var buf: [max_password_hash_len]u8 = undefined; + const hash = try scrypt.strHash( password, .{ .allocator = alloc, - .params = scrypt.Params.interactive, + .params = scrypt_params, .encoding = .phc, }, - buf, - ) catch error.HashFailure; + &buf, + ); + + return util.deepClone(alloc, hash); } /// A raw token is a sequence of N random bytes, base64 encoded. @@ -226,101 +248,190 @@ fn hashToken(token_b64: []const u8, alloc: std.mem.Allocator) ![]const u8 { const hash_b64 = try alloc.alloc(u8, hash_b64_len); return Base64Encoder.encode(hash_b64, &hash); } -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 testCase = struct { + const test_invite_code = "xyz"; + const test_invite_id = Uuid.parse("d24e7f2a-7e6e-4e2a-8e9d-987538a04a40") catch unreachable; + const test_acc_id = Uuid.parse("e8e21e1d-7b80-4e48-876d-9929326af511") catch unreachable; + const test_community_id = Uuid.parse("8bf88bd7-fb07-492d-a89a-6350c036183f") 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); + const Args = struct { + username: []const u8 = "username", + password: []const u8 = "password1234", - return try util.deepClone(alloc, Invite{ - .id = Uuid.parse("eac18f43-4dcc-489f-9fb5-4c1633e7b4e0") catch unreachable, + use_invite: bool = false, + invite_community_id: Uuid = test_community_id, + invite_kind: services.invites.Kind = .user, + invite_max_uses: ?usize = null, + invite_current_uses: usize = 0, + invite_expires_at: ?DateTime = null, - .created_by = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable, - .community_id = exp_community, - .name = "test invite", - .code = exp_code, + get_invite_error: ?anyerror = null, + create_account_error: ?anyerror = null, + create_actor_error: ?anyerror = null, + transfer_error: ?anyerror = null, - .kind = .user, + expect_error: ?anyerror = null, + expect_transferred: bool = false, + }; - .created_at = DateTime.parse("2022-12-21T09:05:50Z") catch unreachable, - .times_used = 0, + fn runCaseOnce(allocator: std.mem.Allocator, test_args: Args) anyerror!void { + const Svc = struct { + test_args: Args, + tx_level: usize = 0, + rolled_back: bool = false, + committed: bool = false, - .expires_at = null, - .max_uses = null, - }); + account_created: bool = false, + actor_created: bool = false, + community_transferred: bool = false, + + fn beginTx(self: *@This()) !*@This() { + self.tx_level += 1; + return self; + } + fn rollbackTx(self: *@This()) void { + self.tx_level -= 1; + self.rolled_back = true; + } + fn commitTx(self: *@This()) !void { + self.tx_level -= 1; + self.committed = true; + } + + fn getInviteByCode(self: *@This(), alloc: Allocator, code: []const u8, community_id: Uuid) anyerror!services.invites.Invite { + try std.testing.expect(self.tx_level > 0); + try std.testing.expectEqualStrings(test_invite_code, code); + try std.testing.expectEqual(test_community_id, community_id); + if (self.test_args.get_invite_error) |err| return err; + return try util.deepClone(alloc, std.mem.zeroInit(services.invites.Invite, .{ + .id = test_invite_id, + .community_id = self.test_args.invite_community_id, + .code = code, + .kind = self.test_args.invite_kind, + + .times_used = self.test_args.invite_current_uses, + .max_uses = self.test_args.invite_max_uses, + .expires_at = self.test_args.invite_expires_at, + })); + } + + fn createActor(self: *@This(), _: Allocator, username: []const u8, community_id: Uuid, _: bool) anyerror!Uuid { + try std.testing.expect(self.tx_level > 0); + if (self.test_args.create_actor_error) |err| return err; + try std.testing.expectEqualStrings(self.test_args.username, username); + try std.testing.expectEqual(test_community_id, community_id); + self.actor_created = true; + return test_acc_id; + } + + fn createAccount(self: *@This(), alloc: Allocator, args: services.accounts.CreateArgs) anyerror!void { + try std.testing.expect(self.tx_level > 0); + if (self.test_args.create_account_error) |err| return err; + try verifyPassword(args.password_hash, self.test_args.password, alloc); + if (self.test_args.use_invite) + try std.testing.expectEqual(@as(?Uuid, test_invite_id), args.invite_id) + else + try std.testing.expect(args.invite_id == null); + + try std.testing.expectEqual(services.accounts.Role.user, args.role); + self.account_created = true; + } + + fn transferCommunityOwnership(self: *@This(), community_id: Uuid, account_id: Uuid) !void { + try std.testing.expect(self.tx_level > 0); + if (self.test_args.transfer_error) |err| return err; + self.community_transferred = true; + try std.testing.expectEqual(test_community_id, community_id); + try std.testing.expectEqual(test_acc_id, account_id); + } + }; + + var svc = Svc{ .test_args = test_args }; + + const community = std.mem.zeroInit(pkg.Community, .{ .kind = .local, .id = test_community_id }); + + const result = register( + allocator, + .{ .community = community }, + &svc, + .{ + .username = test_args.username, + .password = test_args.password, + .invite_code = if (test_args.use_invite) test_invite_code else null, + }, + // shortcut out of memory errors to test allocation + ) catch |err| if (err == error.OutOfMemory) return err else err; + + if (test_args.expect_error) |err| { + try std.testing.expectError(err, result); + try std.testing.expect(!svc.committed); + if (svc.account_created or svc.actor_created or svc.community_transferred) { + try std.testing.expect(svc.rolled_back); + } + } else { + try std.testing.expectEqual(test_acc_id, try result); + try std.testing.expect(svc.committed); + try std.testing.expect(!svc.rolled_back); + try std.testing.expect(svc.account_created); + try std.testing.expect(svc.actor_created); + try std.testing.expectEqual(test_args.expect_transferred, svc.community_transferred); } - }; - const auth = struct { - fn register( - db: *TestDb, - username: []const u8, - password: []const u8, - community_id: Uuid, - _: AccountCreateOptions, - _: 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!pkg.Actor { - try std.testing.expectEqual(uid, id); - return try util.deepClone(alloc, std.mem.zeroInit(pkg.Actor, .{ - .id = id, - .username = "root", - .host = "example.com", - .community_id = exp_community, - })); - } - }; - const communities = struct { - fn transferOwnership(_: *TestDb, _: Uuid, _: Uuid) anyerror!void {} - }; - }; + fn case(args: Args) !void { + try std.testing.checkAllAllocationFailures(std.testing.allocator, runCaseOnce, .{args}); + } + }.case; - var db = TestDb{}; + // regular registration + try testCase(.{}); - _ = MockSvc; - util.deepFree(std.testing.allocator, try 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); + // registration with invite + try testCase(.{ .use_invite = true }); + + // registration with invite for a different community + try testCase(.{ + .invite_community_id = Uuid.parse("11111111-1111-1111-1111-111111111111") catch unreachable, + .use_invite = true, + .expect_error = error.WrongCommunity, + }); + + // registration as a new community owner + try testCase(.{ + .use_invite = true, + .invite_kind = .community_owner, + .expect_transferred = true, + }); + + // invite with expiration info + try testCase(.{ + .use_invite = true, + .invite_max_uses = 100, + .invite_current_uses = 10, + .invite_expires_at = DateTime{ .seconds_since_epoch = DateTime.test_now_timestamp + 3600 }, + }); + + // missing invite + try testCase(.{ + .use_invite = true, + .get_invite_error = error.NotFound, + .expect_error = error.InvalidInvite, + }); + + // expired invite + try testCase(.{ + .use_invite = true, + .invite_expires_at = DateTime{ .seconds_since_epoch = DateTime.test_now_timestamp - 3600 }, + .expect_error = error.InvalidInvite, + }); + + // used invite + try testCase(.{ + .use_invite = true, + .invite_max_uses = 100, + .invite_current_uses = 110, + .expect_error = error.InvalidInvite, + }); } diff --git a/src/api/services.zig b/src/api/services.zig index dbaf690..8d09f48 100644 --- a/src/api/services.zig +++ b/src/api/services.zig @@ -52,11 +52,9 @@ pub fn Services(comptime Db: type) type { pub fn createAccount( self: Self, alloc: std.mem.Allocator, - actor: Uuid, - password_hash: []const u8, - options: types.accounts.CreateOptions, + args: types.accounts.CreateArgs, ) !void { - return try impl.accounts.create(self.db, actor, password_hash, options, alloc); + return try impl.accounts.create(self.db, args, alloc); } pub fn getCredentialsByUsername( diff --git a/src/api/services/accounts.zig b/src/api/services/accounts.zig index e97014a..2bf5419 100644 --- a/src/api/services/accounts.zig +++ b/src/api/services/accounts.zig @@ -5,29 +5,27 @@ const types = @import("./types.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; -const CreateOptions = types.accounts.CreateOptions; +const CreateArgs = types.accounts.CreateArgs; const Credentials = types.accounts.Credentials; /// Creates a local account with the given information pub fn create( db: anytype, - for_actor: Uuid, - password_hash: []const u8, - options: CreateOptions, + args: CreateArgs, alloc: std.mem.Allocator, ) !void { const tx = try db.beginOrSavepoint(); errdefer tx.rollback(); tx.insert("account", .{ - .id = for_actor, - .invite_id = options.invite_id, - .email = options.email, - .kind = options.role, + .id = args.for_actor, + .invite_id = args.invite_id, + .email = args.email, + .kind = args.role, }, alloc) catch return error.DatabaseFailure; tx.insert("password", .{ - .account_id = for_actor, - .hash = password_hash, + .account_id = args.for_actor, + .hash = args.password_hash, .changed_at = DateTime.now(), }, alloc) catch return error.DatabaseFailure; diff --git a/src/api/services/types.zig b/src/api/services/types.zig index 6db53e7..2b66058 100644 --- a/src/api/services/types.zig +++ b/src/api/services/types.zig @@ -34,7 +34,9 @@ pub const accounts = struct { admin, }; - pub const CreateOptions = struct { + pub const CreateArgs = struct { + for_actor: Uuid, + password_hash: []const u8, invite_id: ?Uuid = null, email: ?[]const u8 = null, role: Role = .user, diff --git a/src/api/types.zig b/src/api/types.zig index e072f3d..390306a 100644 --- a/src/api/types.zig +++ b/src/api/types.zig @@ -15,6 +15,8 @@ fn QueryResult(comptime R: type, comptime A: type) type { pub const auth = struct { pub const RegistrationOptions = struct { + username: []const u8, + password: []const u8, invite_code: ?[]const u8 = null, email: ?[]const u8 = null, }; diff --git a/src/main/controllers/api/users.zig b/src/main/controllers/api/users.zig index 3bf0190..321b9ba 100644 --- a/src/main/controllers/api/users.zig +++ b/src/main/controllers/api/users.zig @@ -14,10 +14,12 @@ pub const create = struct { pub fn handler(req: anytype, res: anytype, srv: anytype) !void { const options = .{ + .username = req.body.username, + .password = req.body.password, .invite_code = req.body.invite_code, .email = req.body.email, }; - const user = srv.register(req.body.username, req.body.password, options) catch |err| switch (err) { + const user = srv.register(options) catch |err| switch (err) { error.UsernameTaken => return res.err(.unprocessable_entity, "Username Unavailable", {}), else => return err, }; From a1a93a7466329e3b2e248848ad0ddb83eb4cbf89 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 8 Jan 2023 15:36:11 -0800 Subject: [PATCH 9/9] Add message after server startup --- src/main/main.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/main.zig b/src/main/main.zig index 18f644a..70b0e71 100644 --- a/src/main/main.zig +++ b/src/main/main.zig @@ -84,7 +84,11 @@ pub fn main() !void { var api_src = try api.ApiSource.init(&pool); var srv = http.Server.init(); defer srv.deinit(); - try srv.listen(std.net.Address.parseIp("::1", 8080) catch unreachable); + + const addr = "::1"; + const port = 8080; + try srv.listen(std.net.Address.parseIp(addr, port) catch unreachable); + std.log.info("Listening on {s}:{}", .{ addr, port }); var i: usize = 0; while (i < cfg.worker_threads - 1) : (i += 1) {