fediglam/src/main/api.zig

283 lines
9.0 KiB
Zig
Raw Normal View History

2022-07-13 03:40:48 +00:00
const std = @import("std");
2022-07-13 04:16:33 +00:00
const util = @import("util");
2022-08-02 04:33:23 +00:00
const builtin = @import("builtin");
2022-07-13 03:40:48 +00:00
const db = @import("./db.zig");
2022-07-24 09:01:17 +00:00
const models = @import("./db/models.zig");
2022-07-18 07:37:10 +00:00
pub const DateTime = util.DateTime;
2022-07-13 04:16:33 +00:00
pub const Uuid = util.Uuid;
2022-07-30 06:14:42 +00:00
const Config = @import("./main.zig").Config;
2022-07-13 03:40:48 +00:00
2022-07-22 04:19:08 +00:00
const PwHash = std.crypto.pwhash.scrypt;
const pw_hash_params = PwHash.Params.interactive;
const pw_hash_encoding = .phc;
const pw_hash_buf_size = 128;
2022-07-24 05:19:32 +00:00
const token_len = 20;
2022-07-25 00:04:44 +00:00
const token_str_len = std.base64.standard.Encoder.calcSize(token_len);
2022-07-24 05:19:32 +00:00
2022-07-27 05:02:09 +00:00
const invite_code_len = 16;
const invite_code_str_len = std.base64.url_safe.Encoder.calcSize(invite_code_len);
2022-09-07 23:14:52 +00:00
const services = struct {
const communities = @import("./api/communities.zig");
const users = @import("./api/users.zig");
const auth = @import("./api/auth.zig");
};
2022-07-16 18:44:46 +00:00
// Frees an api struct and its fields allocated from alloc
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
2022-07-19 09:22:19 +00:00
switch (@typeInfo(@TypeOf(val))) {
.Pointer => |ptr_info| switch (ptr_info.size) {
.One => {
free(alloc, val.*);
alloc.destroy(val);
},
.Slice => {
for (val) |elem| free(alloc, elem);
alloc.free(val);
},
else => unreachable,
},
.Struct => inline for (std.meta.fields(@TypeOf(val))) |f| free(alloc, @field(val, f.name)),
.Array => for (val) |elem| free(alloc, elem),
.Optional => if (val) |opt| free(alloc, opt),
.Bool, .Int, .Float, .Enum => {},
else => unreachable,
2022-07-16 18:44:46 +00:00
}
}
2022-08-02 05:18:54 +00:00
pub fn firstIndexOf(str: []const u8, ch: u8) ?usize {
for (str) |c, i| {
if (c == ch) return i;
}
return null;
}
2022-08-02 04:33:23 +00:00
pub const Scheme = models.Community.Scheme;
2022-07-22 04:19:08 +00:00
pub const RegistrationInfo = struct {
username: []const u8,
password: []const u8,
email: ?[]const u8,
2022-07-27 05:30:52 +00:00
invite_code: ?[]const u8,
2022-07-22 04:19:08 +00:00
};
2022-07-26 02:07:05 +00:00
pub const LoginResult = struct {
user_id: Uuid,
token: [token_str_len]u8,
issued_at: DateTime,
};
2022-07-27 06:03:27 +00:00
pub const InviteOptions = struct {
name: []const u8 = "",
max_uses: ?i64 = null,
lifetime: ?i64 = null, // unix seconds, TODO make a TimeSpan type
2022-08-02 04:33:23 +00:00
to_community: ?[]const u8,
2022-07-27 06:03:27 +00:00
};
2022-07-25 00:18:25 +00:00
threadlocal var prng: std.rand.DefaultPrng = undefined;
pub fn initThreadPrng(seed: u64) void {
prng = std.rand.DefaultPrng.init(seed +% std.Thread.getCurrentId());
}
2022-09-07 23:14:52 +00:00
pub fn getRandom() std.rand.Random {
return prng.random();
}
2022-09-05 08:52:49 +00:00
// Returned slice points into buf
fn hashPassword(password: []const u8, alloc: std.mem.Allocator, buf: *[pw_hash_buf_size]u8) ![]const u8 {
return PwHash.strHash(password, .{ .allocator = alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, buf);
}
2022-07-26 02:07:05 +00:00
pub const ApiSource = struct {
2022-07-15 07:27:27 +00:00
db: db.Database,
2022-07-22 04:19:08 +00:00
internal_alloc: std.mem.Allocator,
2022-07-30 06:14:42 +00:00
config: Config,
2022-07-15 00:58:08 +00:00
2022-07-26 02:07:05 +00:00
pub const Conn = ApiConn(db.Database);
2022-09-05 08:52:49 +00:00
const root_username = "root";
const root_password_envvar = "CLUSTER_ROOT_PASSWORD";
2022-07-30 06:14:42 +00:00
pub fn init(alloc: std.mem.Allocator, cfg: Config) !ApiSource {
2022-09-05 08:52:49 +00:00
var self = ApiSource{
.db = try db.Database.init(),
.internal_alloc = alloc,
.config = cfg,
};
2022-07-13 14:42:30 +00:00
2022-09-07 23:14:52 +00:00
if ((try services.users.lookupByUsername(&self.db, root_username, null)) == null) {
2022-09-05 08:52:49 +00:00
std.log.info("No cluster root user detected. Creating...", .{});
const root_password = std.os.getenv(root_password_envvar) orelse {
std.log.err(
2022-09-07 23:14:52 +00:00
"No root user created and no password specified. Please provide the password for the root user by the ${s} environment variable for initial startup. This only needs to be done once",
2022-09-05 08:52:49 +00:00
.{root_password_envvar},
);
@panic("No root password provided");
};
2022-09-05 07:03:31 +00:00
2022-09-07 23:14:52 +00:00
_ = try services.users.create(&self.db, root_username, root_password, null, .{}, alloc);
2022-07-13 14:42:30 +00:00
}
2022-09-05 08:52:49 +00:00
return self;
2022-07-17 23:21:03 +00:00
}
2022-09-07 23:14:52 +00:00
fn getCommunityFromHost(self: *ApiSource, host: []const u8) !?Uuid {
2022-09-08 02:01:24 +00:00
if (try self.db.execRow(
2022-09-05 10:33:54 +00:00
&.{Uuid},
"SELECT id FROM community WHERE host = ?",
.{host},
null,
2022-09-07 23:14:52 +00:00
)) |result| return result[0];
// Test for cluster admin community
if (util.ciutf8.eql(self.config.cluster_host, host)) {
return null;
}
return error.NoCommunity;
}
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
const community_id = try self.getCommunityFromHost(host);
2022-09-05 08:52:49 +00:00
2022-07-26 02:07:05 +00:00
return Conn{
.db = self.db,
.internal_alloc = self.internal_alloc,
2022-09-07 23:14:52 +00:00
.user_id = null,
.community_id = community_id,
2022-07-26 02:07:05 +00:00
.arena = std.heap.ArenaAllocator.init(alloc),
};
}
2022-09-05 08:52:49 +00:00
pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn {
2022-09-07 23:14:52 +00:00
const community_id = try self.getCommunityFromHost(host);
2022-07-17 23:21:03 +00:00
2022-09-07 23:14:52 +00:00
const token_info = try services.auth.tokens.verify(&self.db, token, community_id);
2022-07-17 23:21:03 +00:00
2022-09-07 23:14:52 +00:00
return Conn{
.db = self.db,
.internal_alloc = self.internal_alloc,
.user_id = token_info.user_id,
.community_id = community_id,
.arena = std.heap.ArenaAllocator.init(alloc),
};
2022-07-13 03:40:48 +00:00
}
2022-07-26 02:07:05 +00:00
};
2022-07-13 03:40:48 +00:00
2022-07-26 02:07:05 +00:00
fn ApiConn(comptime DbConn: type) type {
return struct {
const Self = @This();
2022-07-18 07:37:10 +00:00
2022-07-26 02:07:05 +00:00
db: DbConn,
internal_alloc: std.mem.Allocator, // used *only* for large, internal buffers
2022-09-07 23:14:52 +00:00
user_id: ?Uuid,
community_id: ?Uuid,
2022-07-26 02:07:05 +00:00
arena: std.heap.ArenaAllocator,
2022-07-18 06:11:42 +00:00
2022-07-26 02:07:05 +00:00
pub fn close(self: *Self) void {
self.arena.deinit();
}
2022-07-18 06:11:42 +00:00
2022-07-26 02:07:05 +00:00
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResult {
2022-09-07 23:14:52 +00:00
const user_id = (try services.users.lookupByUsername(&self.db, username, self.community_id)) orelse return error.InvalidLogin;
try services.auth.passwords.verify(&self.db, user_id, password, self.internal_alloc);
2022-07-21 05:26:13 +00:00
2022-09-07 23:14:52 +00:00
const token = try services.auth.tokens.create(&self.db, user_id);
2022-07-26 02:07:05 +00:00
return LoginResult{
2022-09-05 07:03:31 +00:00
.user_id = user_id,
2022-09-07 23:14:52 +00:00
.token = token.value,
2022-07-26 02:07:05 +00:00
.issued_at = token.info.issued_at,
};
}
2022-09-05 10:33:54 +00:00
const TokenInfo = struct {
username: []const u8,
};
pub fn getTokenInfo(self: *Self) !TokenInfo {
2022-09-07 23:14:52 +00:00
if (self.user_id) |user_id| {
2022-09-08 02:01:24 +00:00
const result = (try self.db.execRow(
2022-09-05 10:33:54 +00:00
&.{[]const u8},
"SELECT username FROM user WHERE id = ?",
.{user_id},
self.arena.allocator(),
)) orelse {
return error.UserNotFound;
};
return TokenInfo{ .username = result[0] };
}
return error.Unauthorized;
}
2022-09-07 23:14:52 +00:00
pub fn createCommunity(self: *Self, origin: []const u8) !services.communities.Community {
if (self.community_id != null) {
return error.NotAdminHost;
}
2022-07-26 02:07:05 +00:00
2022-09-07 23:14:52 +00:00
return services.communities.create(&self.db, origin, null);
2022-07-26 02:07:05 +00:00
}
2022-07-27 05:02:09 +00:00
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
const id = Uuid.randV4(prng.random());
2022-08-02 04:33:23 +00:00
const user = try self.getAuthenticatedUser();
// Users can only make invites to their own community, unless they
// are system users
const community_id = if (options.to_community) |host| blk: {
2022-09-08 02:01:24 +00:00
const desired_community = (try self.db.execRow(
2022-09-05 07:03:31 +00:00
&.{Uuid},
"SELECT id FROM community WHERE host = ?",
.{host},
null,
)) orelse return error.CommunityNotFound;
if (user.community_id != null and !Uuid.eql(desired_community[0], user.community_id.?)) {
2022-08-02 04:33:23 +00:00
return error.WrongCommunity;
}
2022-09-05 07:03:31 +00:00
break :blk desired_community[0];
2022-08-02 04:33:23 +00:00
} else null;
2022-09-05 07:03:31 +00:00
if (user.community_id != null and community_id == null) {
2022-08-02 04:33:23 +00:00
return error.WrongCommunity;
}
2022-07-27 05:02:09 +00:00
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),
2022-08-02 04:33:23 +00:00
.created_by = user.id,
2022-07-27 05:02:09 +00:00
.invite_code = code_str,
2022-08-02 04:33:23 +00:00
.to_community = community_id,
2022-07-27 05:02:09 +00:00
.max_uses = options.max_uses,
.created_at = now,
.expires_at = expires_at,
};
try self.db.insert(models.Invite, invite);
return invite;
}
2022-07-26 02:07:05 +00:00
};
}