Add community creation page
This commit is contained in:
parent
b781b34029
commit
e617406872
7 changed files with 144 additions and 16 deletions
|
@ -34,7 +34,7 @@ pub const InviteOptions = struct {
|
||||||
|
|
||||||
name: ?[]const u8 = null,
|
name: ?[]const u8 = null,
|
||||||
lifespan: ?DateTime.Duration = null,
|
lifespan: ?DateTime.Duration = null,
|
||||||
max_uses: ?u16 = null,
|
max_uses: ?usize = null,
|
||||||
|
|
||||||
// admin only options
|
// admin only options
|
||||||
kind: Kind = .user,
|
kind: Kind = .user,
|
||||||
|
@ -209,11 +209,21 @@ pub const FileResult = struct {
|
||||||
data: []const u8,
|
data: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ValidInvite = struct {
|
pub const InviteResponse = struct {
|
||||||
code: []const u8,
|
code: []const u8,
|
||||||
kind: services.invites.Kind,
|
kind: services.invites.Kind,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
creator: UserResponse,
|
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 {
|
pub fn isAdminSetup(db: sql.Db) !bool {
|
||||||
|
@ -356,7 +366,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
return error.TokenRequired;
|
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()) {
|
if (!self.isAdmin()) {
|
||||||
return error.PermissionDenied;
|
return error.PermissionDenied;
|
||||||
}
|
}
|
||||||
|
@ -366,7 +376,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
const community_id = try services.communities.create(
|
const community_id = try services.communities.create(
|
||||||
tx,
|
tx,
|
||||||
origin,
|
origin,
|
||||||
.{},
|
.{ .name = name },
|
||||||
self.allocator,
|
self.allocator,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -384,7 +394,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
return community;
|
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
|
// Only logged in users can make invites
|
||||||
const user_id = self.user_id orelse return error.TokenRequired;
|
const user_id = self.user_id orelse return error.TokenRequired;
|
||||||
|
|
||||||
|
@ -405,7 +415,39 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
.kind = options.kind,
|
.kind = options.kind,
|
||||||
}, self.allocator);
|
}, 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 {
|
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);
|
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(
|
const invite = services.invites.getByCode(
|
||||||
self.db,
|
self.db,
|
||||||
code,
|
code,
|
||||||
|
@ -815,16 +857,33 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
if (!Uuid.eql(invite.community_id, self.community.id)) return error.InvalidInvite;
|
if (!Uuid.eql(invite.community_id, self.community.id)) return error.InvalidInvite;
|
||||||
if (!isInviteValid(invite)) 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) {
|
const creator = self.getUserUnchecked(self.db, invite.created_by) catch |err| switch (err) {
|
||||||
error.NotFound => return error.Unexpected,
|
error.NotFound => return error.Unexpected,
|
||||||
else => return error.DatabaseFailure,
|
else => return error.DatabaseFailure,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ValidInvite{
|
return InviteResponse{
|
||||||
.code = invite.code,
|
.code = invite.code,
|
||||||
.name = invite.name,
|
.name = invite.name,
|
||||||
.kind = invite.kind,
|
.kind = invite.kind,
|
||||||
.creator = creator,
|
.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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,12 +20,12 @@ pub const Kind = enum {
|
||||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InviteCount = u16;
|
const InviteCount = usize;
|
||||||
pub const Invite = struct {
|
pub const Invite = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
||||||
created_by: Uuid, // User ID
|
created_by: Uuid, // User ID
|
||||||
community_id: ?Uuid,
|
community_id: Uuid,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
code: []const u8,
|
code: []const u8,
|
||||||
|
|
||||||
|
@ -127,8 +127,8 @@ fn doGetQuery(
|
||||||
);
|
);
|
||||||
|
|
||||||
return db.queryRow(Invite, query, query_args, alloc) catch |err| switch (err) {
|
return db.queryRow(Invite, query, query_args, alloc) catch |err| switch (err) {
|
||||||
error.NoRows => error.NotFound,
|
error.NoRows => return error.NotFound,
|
||||||
else => error.DatabaseFailure,
|
else => return error.DatabaseFailure,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,11 @@ pub const create = struct {
|
||||||
|
|
||||||
pub const Body = struct {
|
pub const Body = struct {
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
|
name: ?[]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
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);
|
try res.json(.created, invite);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ pub const routes = .{
|
||||||
controllers.apiEndpoint(signup.page),
|
controllers.apiEndpoint(signup.page),
|
||||||
controllers.apiEndpoint(signup.with_invite),
|
controllers.apiEndpoint(signup.with_invite),
|
||||||
controllers.apiEndpoint(signup.submit),
|
controllers.apiEndpoint(signup.submit),
|
||||||
|
controllers.apiEndpoint(cluster.communities.create.page),
|
||||||
|
controllers.apiEndpoint(cluster.communities.create.submit),
|
||||||
};
|
};
|
||||||
|
|
||||||
const static = struct {
|
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 {
|
const media = struct {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<h2>
|
||||||
|
Community {.community.name} created
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
Send the following invite to the community owner for them
|
||||||
|
to complete setup.
|
||||||
|
|
||||||
|
<div>{.invite.url}</div>
|
||||||
|
</div>
|
16
src/main/controllers/web/cluster/community-create.tmpl.html
Normal file
16
src/main/controllers/web/cluster/community-create.tmpl.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<form action="/cluster/communities/create" method="post">
|
||||||
|
<h2>Create Local Community</h2>
|
||||||
|
<label>
|
||||||
|
<div>Origin</div>
|
||||||
|
<div class="textinput">
|
||||||
|
<input type="url" name="origin" placeholder="https://example.com" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<div>Name</div>
|
||||||
|
<div class="textinput">
|
||||||
|
<input type="text" name="name" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
|
@ -308,10 +308,11 @@ pub const Row = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
fn getColumn(stmt: *c.sqlite3_stmt, comptime T: type, idx: u15, alloc: ?Allocator) common.GetError!T {
|
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)) {
|
return switch (c.sqlite3_column_type(stmt, idx)) {
|
||||||
c.SQLITE_INTEGER => getColumnInt(stmt, T, idx),
|
c.SQLITE_INTEGER => try getColumnInt(stmt, Eff, idx),
|
||||||
c.SQLITE_FLOAT => getColumnFloat(stmt, T, idx),
|
c.SQLITE_FLOAT => try getColumnFloat(stmt, Eff, idx),
|
||||||
c.SQLITE_TEXT => getColumnText(stmt, T, idx, alloc),
|
c.SQLITE_TEXT => try getColumnText(stmt, Eff, idx, alloc),
|
||||||
c.SQLITE_NULL => {
|
c.SQLITE_NULL => {
|
||||||
if (T == DateTime) {
|
if (T == DateTime) {
|
||||||
std.log.warn("SQLite: Treating NULL as DateTime epoch", .{});
|
std.log.warn("SQLite: Treating NULL as DateTime epoch", .{});
|
||||||
|
|
Loading…
Reference in a new issue