From e617406872ec40d5eea0767759cd0add1cf693b0 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sun, 11 Dec 2022 19:52:11 -0800 Subject: [PATCH] Add community creation page --- src/api/lib.zig | 75 +++++++++++++++++-- src/api/services/invites.zig | 8 +- src/main/controllers/api/communities.zig | 3 +- src/main/controllers/web.zig | 42 +++++++++++ .../community-create-success.tmpl.html | 9 +++ .../web/cluster/community-create.tmpl.html | 16 ++++ src/sql/engines/sqlite.zig | 7 +- 7 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 src/main/controllers/web/cluster/community-create-success.tmpl.html create mode 100644 src/main/controllers/web/cluster/community-create.tmpl.html diff --git a/src/api/lib.zig b/src/api/lib.zig index e145fba..e28bda5 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -34,7 +34,7 @@ pub const InviteOptions = struct { name: ?[]const u8 = null, lifespan: ?DateTime.Duration = null, - max_uses: ?u16 = null, + max_uses: ?usize = null, // admin only options kind: Kind = .user, @@ -209,11 +209,21 @@ pub const FileResult = struct { data: []const u8, }; -pub const ValidInvite = struct { +pub const InviteResponse = struct { code: []const u8, kind: services.invites.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 { @@ -356,7 +366,7 @@ fn ApiConn(comptime DbConn: type) type { return error.TokenRequired; } - pub fn createCommunity(self: *Self, origin: []const u8) !services.communities.Community { + pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !services.communities.Community { if (!self.isAdmin()) { return error.PermissionDenied; } @@ -366,7 +376,7 @@ fn ApiConn(comptime DbConn: type) type { const community_id = try services.communities.create( tx, origin, - .{}, + .{ .name = name }, self.allocator, ); @@ -384,7 +394,7 @@ fn ApiConn(comptime DbConn: type) type { return community; } - pub fn createInvite(self: *Self, options: InviteOptions) !services.invites.Invite { + pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse { // Only logged in users can make invites const user_id = self.user_id orelse return error.TokenRequired; @@ -405,7 +415,39 @@ fn ApiConn(comptime DbConn: type) type { .kind = options.kind, }, self.allocator); - return try services.invites.get(self.db, invite_id, self.allocator); + const invite = try services.invites.get(self.db, invite_id, self.allocator); + errdefer util.deepFree(self.allocator, invite); + + const url = if (options.to_community) |cid| blk: { + const community = try services.communities.get(self.db, cid, self.allocator); + 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.community.scheme), self.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: services.invites.Invite) bool { @@ -800,7 +842,7 @@ fn ApiConn(comptime DbConn: type) type { try services.actors.updateProfile(self.db, id, data, self.allocator); } - pub fn validateInvite(self: *Self, code: []const u8) !ValidInvite { + pub fn validateInvite(self: *Self, code: []const u8) !InviteResponse { const invite = services.invites.getByCode( self.db, code, @@ -815,16 +857,33 @@ fn ApiConn(comptime DbConn: type) type { if (!Uuid.eql(invite.community_id, self.community.id)) return error.InvalidInvite; if (!isInviteValid(invite)) return error.InvalidInvite; + const url = try std.fmt.allocPrint( + self.allocator, + "{s}://{s}/invite/{s}", + .{ @tagName(self.community.scheme), self.community.host, invite.code }, + ); + 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 ValidInvite{ + 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, }; } }; diff --git a/src/api/services/invites.zig b/src/api/services/invites.zig index 2a58354..72684e7 100644 --- a/src/api/services/invites.zig +++ b/src/api/services/invites.zig @@ -20,12 +20,12 @@ pub const Kind = enum { pub const jsonStringify = util.jsonSerializeEnumAsString; }; -const InviteCount = u16; +const InviteCount = usize; pub const Invite = struct { id: Uuid, created_by: Uuid, // User ID - community_id: ?Uuid, + community_id: Uuid, name: []const u8, code: []const u8, @@ -127,8 +127,8 @@ fn doGetQuery( ); return db.queryRow(Invite, query, query_args, alloc) catch |err| switch (err) { - error.NoRows => error.NotFound, - else => error.DatabaseFailure, + error.NoRows => return error.NotFound, + else => return error.DatabaseFailure, }; } diff --git a/src/main/controllers/api/communities.zig b/src/main/controllers/api/communities.zig index 7b61d86..224e33c 100644 --- a/src/main/controllers/api/communities.zig +++ b/src/main/controllers/api/communities.zig @@ -9,10 +9,11 @@ pub const create = struct { pub const Body = struct { origin: []const u8, + name: ?[]const u8 = null, }; pub fn handler(req: anytype, res: anytype, srv: anytype) !void { - const invite = try srv.createCommunity(req.body.origin); + const invite = try srv.createCommunity(req.body.origin, req.body.name); try res.json(.created, invite); } diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 7910d71..1a66380 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -14,6 +14,8 @@ pub const routes = .{ controllers.apiEndpoint(signup.page), controllers.apiEndpoint(signup.with_invite), controllers.apiEndpoint(signup.submit), + controllers.apiEndpoint(cluster.communities.create.page), + controllers.apiEndpoint(cluster.communities.create.submit), }; const static = struct { @@ -221,6 +223,46 @@ const cluster = struct { }); } }; + + const communities = struct { + const create = struct { + const tmpl = @embedFile("./web/cluster/community-create.tmpl.html"); + const success_tmpl = @embedFile("./web/cluster/community-create-success.tmpl.html"); + const page = struct { + pub const path = "/cluster/communities/create"; + pub const method = .GET; + + pub fn handler(_: anytype, res: anytype, srv: anytype) !void { + try res.template(.ok, srv, tmpl, .{}); + } + }; + + const submit = struct { + pub const path = "/cluster/communities/create"; + pub const method = .POST; + + pub const Body = struct { + origin: []const u8, + name: ?[]const u8, + }; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const community = try srv.createCommunity(req.body.origin, req.body.name); + defer util.deepFree(srv.allocator, community); + + const invite = try srv.createInvite(.{ + .max_uses = 1, + + .kind = .community_owner, + .to_community = community.id, + }); + defer util.deepFree(srv.allocator, invite); + + try res.template(.ok, srv, success_tmpl, .{ .community = community, .invite = invite }); + } + }; + }; + }; }; const media = struct { diff --git a/src/main/controllers/web/cluster/community-create-success.tmpl.html b/src/main/controllers/web/cluster/community-create-success.tmpl.html new file mode 100644 index 0000000..ab48c6d --- /dev/null +++ b/src/main/controllers/web/cluster/community-create-success.tmpl.html @@ -0,0 +1,9 @@ +

+ Community {.community.name} created +

+
+ Send the following invite to the community owner for them + to complete setup. + +
{.invite.url}
+
diff --git a/src/main/controllers/web/cluster/community-create.tmpl.html b/src/main/controllers/web/cluster/community-create.tmpl.html new file mode 100644 index 0000000..3185c33 --- /dev/null +++ b/src/main/controllers/web/cluster/community-create.tmpl.html @@ -0,0 +1,16 @@ +
+

Create Local Community

+ + + +
diff --git a/src/sql/engines/sqlite.zig b/src/sql/engines/sqlite.zig index be94c7e..99d1f50 100644 --- a/src/sql/engines/sqlite.zig +++ b/src/sql/engines/sqlite.zig @@ -308,10 +308,11 @@ pub const Row = struct { }; fn getColumn(stmt: *c.sqlite3_stmt, comptime T: type, idx: u15, alloc: ?Allocator) common.GetError!T { + const Eff = if (comptime std.meta.trait.is(.Optional)(T)) std.meta.Child(T) else T; return switch (c.sqlite3_column_type(stmt, idx)) { - c.SQLITE_INTEGER => getColumnInt(stmt, T, idx), - c.SQLITE_FLOAT => getColumnFloat(stmt, T, idx), - c.SQLITE_TEXT => getColumnText(stmt, T, idx, alloc), + c.SQLITE_INTEGER => try getColumnInt(stmt, Eff, idx), + c.SQLITE_FLOAT => try getColumnFloat(stmt, Eff, idx), + c.SQLITE_TEXT => try getColumnText(stmt, Eff, idx, alloc), c.SQLITE_NULL => { if (T == DateTime) { std.log.warn("SQLite: Treating NULL as DateTime epoch", .{});