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_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
|
||||
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
||||
switch (@typeInfo(@TypeOf(val))) {
|
||||
|
@ -291,5 +294,49 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
.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 notes = @import("./controllers/notes.zig");
|
||||
pub const actors = @import("./controllers/actors.zig");
|
||||
pub const admin = struct {
|
||||
pub const invites = @import("./controllers/admin/invites.zig");
|
||||
};
|
||||
|
||||
pub const utils = struct {
|
||||
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.LocalUser => "local_user",
|
||||
models.Token => "token",
|
||||
models.Invite => "invite",
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
@ -111,7 +112,7 @@ fn bind(stmt: sql.PreparedStmt, idx: u15, val: anytype) !void {
|
|||
else => |T| switch (@typeInfo(T)) {
|
||||
.Optional => if (val) |v| bind(stmt, idx, v) else stmt.bindNull(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)
|
||||
\\) 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 {
|
||||
|
|
|
@ -20,6 +20,18 @@ pub fn ByteArray(comptime n: usize) type {
|
|||
_ = try row.getBlob(idx, &self.data);
|
||||
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),
|
||||
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(.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 {
|
||||
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 => {},
|
||||
else => error.UnknownError,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue