Generate invite codes
This commit is contained in:
parent
a020199773
commit
01ef4427f5
7 changed files with 128 additions and 2 deletions
|
@ -15,6 +15,9 @@ const pw_hash_buf_size = 128;
|
||||||
const token_len = 20;
|
const token_len = 20;
|
||||||
const token_str_len = std.base64.standard.Encoder.calcSize(token_len);
|
const token_str_len = std.base64.standard.Encoder.calcSize(token_len);
|
||||||
|
|
||||||
|
const invite_code_len = 16;
|
||||||
|
const invite_code_str_len = std.base64.url_safe.Encoder.calcSize(invite_code_len);
|
||||||
|
|
||||||
// Frees an api struct and its fields allocated from alloc
|
// Frees an api struct and its fields allocated from alloc
|
||||||
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
||||||
switch (@typeInfo(@TypeOf(val))) {
|
switch (@typeInfo(@TypeOf(val))) {
|
||||||
|
@ -291,5 +294,49 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
.value = token,
|
.value = token,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const InviteOptions = struct {
|
||||||
|
name: []const u8 = "",
|
||||||
|
max_uses: ?i64 = null,
|
||||||
|
lifetime: ?i64 = null, // unix seconds, TODO make a TimeSpan type
|
||||||
|
};
|
||||||
|
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
|
||||||
|
const id = Uuid.randV4(prng.random());
|
||||||
|
|
||||||
|
const user_id = (try self.getAuthenticatedUser()).id;
|
||||||
|
|
||||||
|
var code: [invite_code_len]u8 = undefined;
|
||||||
|
std.crypto.random.bytes(&code);
|
||||||
|
|
||||||
|
var code_str = try self.arena.allocator().alloc(u8, invite_code_str_len);
|
||||||
|
_ = std.base64.url_safe.Encoder.encode(code_str, &code);
|
||||||
|
|
||||||
|
const now = DateTime.now();
|
||||||
|
const expires_at = if (options.lifetime) |lifetime| DateTime{
|
||||||
|
.seconds_since_epoch = lifetime + now.seconds_since_epoch,
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
const invite = models.Invite{
|
||||||
|
.id = id,
|
||||||
|
|
||||||
|
.name = try self.arena.allocator().dupe(u8, options.name),
|
||||||
|
.created_by = user_id,
|
||||||
|
.invite_code = code_str,
|
||||||
|
|
||||||
|
.uses = 0,
|
||||||
|
.max_uses = options.max_uses,
|
||||||
|
|
||||||
|
.created_at = now,
|
||||||
|
.expires_at = expires_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.db.insert(models.Invite, invite);
|
||||||
|
|
||||||
|
return invite;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getInvite(self: *Self, id: Uuid) !?models.Invite {
|
||||||
|
return self.db.getBy(models.Invite, .id, id, self.arena.allocator());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ const Uuid = @import("util").Uuid;
|
||||||
pub const auth = @import("./controllers/auth.zig");
|
pub const auth = @import("./controllers/auth.zig");
|
||||||
pub const notes = @import("./controllers/notes.zig");
|
pub const notes = @import("./controllers/notes.zig");
|
||||||
pub const actors = @import("./controllers/actors.zig");
|
pub const actors = @import("./controllers/actors.zig");
|
||||||
|
pub const admin = struct {
|
||||||
|
pub const invites = @import("./controllers/admin/invites.zig");
|
||||||
|
};
|
||||||
|
|
||||||
pub const utils = struct {
|
pub const utils = struct {
|
||||||
const json_options = if (builtin.mode == .Debug) .{
|
const json_options = if (builtin.mode == .Debug) .{
|
||||||
|
|
29
src/main/controllers/admin/invites.zig
Normal file
29
src/main/controllers/admin/invites.zig
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const root = @import("root");
|
||||||
|
const http = @import("http");
|
||||||
|
const Uuid = @import("util").Uuid;
|
||||||
|
|
||||||
|
const utils = @import("../../controllers.zig").utils;
|
||||||
|
const NoteCreateInfo = @import("../../api.zig").NoteCreateInfo;
|
||||||
|
|
||||||
|
const RequestServer = root.RequestServer;
|
||||||
|
const RouteArgs = http.RouteArgs;
|
||||||
|
|
||||||
|
pub fn create(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||||
|
var api = try utils.getApiConn(srv, ctx);
|
||||||
|
defer api.close();
|
||||||
|
|
||||||
|
const invite = try api.createInvite(.{});
|
||||||
|
|
||||||
|
try utils.respondJson(ctx, .created, invite);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void {
|
||||||
|
const id_str = args.get("id") orelse return error.NotFound;
|
||||||
|
const id = Uuid.parse(id_str) catch return utils.respondError(ctx, .bad_request, "Invalid UUID");
|
||||||
|
var api = try utils.getApiConn(srv, ctx);
|
||||||
|
defer api.close();
|
||||||
|
|
||||||
|
const invite = (try api.getInvite(id)) orelse return utils.respondError(ctx, .not_found, "Invite not found");
|
||||||
|
|
||||||
|
try utils.respondJson(ctx, .ok, invite);
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ fn tableName(comptime T: type) String {
|
||||||
models.Reaction => "reaction",
|
models.Reaction => "reaction",
|
||||||
models.LocalUser => "local_user",
|
models.LocalUser => "local_user",
|
||||||
models.Token => "token",
|
models.Token => "token",
|
||||||
|
models.Invite => "invite",
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -111,7 +112,7 @@ fn bind(stmt: sql.PreparedStmt, idx: u15, val: anytype) !void {
|
||||||
else => |T| switch (@typeInfo(T)) {
|
else => |T| switch (@typeInfo(T)) {
|
||||||
.Optional => if (val) |v| bind(stmt, idx, v) else stmt.bindNull(idx),
|
.Optional => if (val) |v| bind(stmt, idx, v) else stmt.bindNull(idx),
|
||||||
.Struct, .Union, .Enum, .Opaque => if (@hasDecl(T, "bindToSql")) val.bindToSql(stmt, idx),
|
.Struct, .Union, .Enum, .Opaque => if (@hasDecl(T, "bindToSql")) val.bindToSql(stmt, idx),
|
||||||
else => @compileError("Unknown Type" ++ @typeName(T)),
|
else => @compileError("Unknown Type " ++ @typeName(T)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -203,6 +204,21 @@ pub const Database = struct {
|
||||||
\\
|
\\
|
||||||
\\ PRIMARY KEY(id)
|
\\ PRIMARY KEY(id)
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
|
,
|
||||||
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
|
\\invite(
|
||||||
|
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
\\
|
||||||
|
\\ name TEXT NOT NULL,
|
||||||
|
\\ invite_code TEXT NOT NULL,
|
||||||
|
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
|
||||||
|
\\
|
||||||
|
\\ uses INTEGER NOT NULL,
|
||||||
|
\\ max_uses INTEGER,
|
||||||
|
\\
|
||||||
|
\\ created_at INTEGER NOT NULL,
|
||||||
|
\\ expires_at INTEGER
|
||||||
|
\\) STRICT;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init() !Database {
|
pub fn init() !Database {
|
||||||
|
|
|
@ -20,6 +20,18 @@ pub fn ByteArray(comptime n: usize) type {
|
||||||
_ = try row.getBlob(idx, &self.data);
|
_ = try row.getBlob(idx, &self.data);
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format(self: Self, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||||
|
const Encoder = std.base64.standard.Encoder;
|
||||||
|
const buf_len = comptime Encoder.calcSize(n);
|
||||||
|
var buf: [buf_len]u8 = undefined;
|
||||||
|
const str = Encoder.encode(&buf, &self.data);
|
||||||
|
try std.fmt.format(writer, "{s}", .{str});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stringifyJson(self: Self, _: std.json.StringifyOptions, writer: anytype) !void {
|
||||||
|
try self.format("{}", .{}, writer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,3 +102,17 @@ pub const Token = struct {
|
||||||
user_id: Ref(LocalUser),
|
user_id: Ref(LocalUser),
|
||||||
issued_at: DateTime,
|
issued_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Invite = struct {
|
||||||
|
id: Uuid,
|
||||||
|
|
||||||
|
name: []const u8,
|
||||||
|
invite_code: []const u8,
|
||||||
|
created_by: Ref(LocalUser),
|
||||||
|
|
||||||
|
uses: i64,
|
||||||
|
max_uses: ?i64,
|
||||||
|
|
||||||
|
created_at: DateTime,
|
||||||
|
expires_at: ?DateTime,
|
||||||
|
};
|
||||||
|
|
|
@ -25,6 +25,9 @@ const router = Router{
|
||||||
Route.new(.POST, "/notes/:id/reacts", c.notes.reacts.create),
|
Route.new(.POST, "/notes/:id/reacts", c.notes.reacts.create),
|
||||||
|
|
||||||
Route.new(.GET, "/actors/:id", c.actors.get),
|
Route.new(.GET, "/actors/:id", c.actors.get),
|
||||||
|
|
||||||
|
Route.new(.POST, "/admin/invites", c.admin.invites.create),
|
||||||
|
Route.new(.GET, "/admin/invites/:id", c.admin.invites.get),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,9 @@ pub const PreparedStmt = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bindText(self: PreparedStmt, idx: u15, str: []const u8) !void {
|
pub fn bindText(self: PreparedStmt, idx: u15, str: []const u8) !void {
|
||||||
return switch (c.sqlite3_bind_text(self.stmt, idx, str.ptr, @intCast(c_int, str.len), c.SQLITE_TRANSIENT)) {
|
// Work around potential null pointer in empty string
|
||||||
|
const eff_str = if (str.len == 0) (" ")[0..0] else str;
|
||||||
|
return switch (c.sqlite3_bind_text(self.stmt, idx, eff_str.ptr, @intCast(c_int, eff_str.len), c.SQLITE_TRANSIENT)) {
|
||||||
c.SQLITE_OK => {},
|
c.SQLITE_OK => {},
|
||||||
else => error.UnknownError,
|
else => error.UnknownError,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue