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-09-15 01:12:07 +00:00
|
|
|
const sql = @import("sql");
|
2022-07-13 03:40:48 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
const DateTime = util.DateTime;
|
|
|
|
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-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-09-08 05:10:58 +00:00
|
|
|
const invites = @import("./api/invites.zig");
|
2022-09-08 07:52:23 +00:00
|
|
|
const notes = @import("./api/notes.zig");
|
2022-09-08 05:10:58 +00:00
|
|
|
};
|
|
|
|
|
2022-09-08 06:56:29 +00:00
|
|
|
pub const RegistrationRequest = struct {
|
|
|
|
username: []const u8,
|
|
|
|
password: []const u8,
|
|
|
|
invite_code: []const u8,
|
2022-09-08 07:06:55 +00:00
|
|
|
email: ?[]const u8 = null,
|
2022-09-08 06:56:29 +00:00
|
|
|
};
|
|
|
|
|
2022-09-08 05:10:58 +00:00
|
|
|
pub const InviteRequest = struct {
|
2022-10-04 02:41:59 +00:00
|
|
|
pub const Kind = services.invites.Kind;
|
2022-09-08 05:10:58 +00:00
|
|
|
|
|
|
|
name: ?[]const u8 = null,
|
2022-10-04 02:41:59 +00:00
|
|
|
lifespan: ?DateTime.Duration = null,
|
2022-09-15 01:12:07 +00:00
|
|
|
max_uses: ?u16 = null,
|
2022-09-08 05:10:58 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
kind: Kind = .user, // must be user unless the creator is an admin
|
2022-09-08 05:10:58 +00:00
|
|
|
to_community: ?[]const u8 = null, // only valid on admin community
|
2022-09-07 23:14:52 +00:00
|
|
|
};
|
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
pub const LoginResponse = services.auth.LoginResult;
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
pub const UserResponse = struct {
|
|
|
|
id: Uuid,
|
|
|
|
|
|
|
|
username: []const u8,
|
|
|
|
host: []const u8,
|
|
|
|
|
|
|
|
created_at: DateTime,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub const NoteResponse = struct {
|
|
|
|
id: Uuid,
|
|
|
|
author: struct {
|
|
|
|
id: Uuid,
|
|
|
|
username: []const u8,
|
|
|
|
host: []const u8,
|
|
|
|
},
|
|
|
|
|
|
|
|
content: []const u8,
|
|
|
|
created_at: DateTime,
|
|
|
|
};
|
|
|
|
|
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-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-29 21:52:01 +00:00
|
|
|
pub fn isAdminSetup(db: *sql.Db) !bool {
|
|
|
|
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
|
|
|
|
error.NotFound => return false,
|
|
|
|
else => return err,
|
|
|
|
};
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
pub fn setupAdmin(db: *sql.Db, origin: []const u8, username: []const u8, password: []const u8, allocator: std.mem.Allocator) anyerror!void {
|
2022-09-29 21:52:01 +00:00
|
|
|
const tx = try db.begin();
|
|
|
|
errdefer tx.rollback();
|
|
|
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
|
|
defer arena.deinit();
|
|
|
|
|
|
|
|
try tx.setConstraintMode(.deferred);
|
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
const community_id = try services.communities.create(
|
2022-09-29 21:52:01 +00:00
|
|
|
tx,
|
|
|
|
origin,
|
|
|
|
.{ .name = "Cluster Admin", .kind = .admin },
|
2022-10-04 02:41:59 +00:00
|
|
|
arena.allocator(),
|
2022-09-29 21:52:01 +00:00
|
|
|
);
|
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
const user = try services.auth.register(tx, username, password, community_id, .{ .kind = .admin }, arena.allocator());
|
2022-09-29 21:52:01 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
try services.communities.transferOwnership(tx, community_id, user);
|
2022-09-29 21:52:01 +00:00
|
|
|
|
|
|
|
try tx.commit();
|
|
|
|
|
|
|
|
std.log.info(
|
|
|
|
"Created admin user {s} (id {}) with cluster admin origin {s} (id {})",
|
2022-10-04 02:41:59 +00:00
|
|
|
.{ username, user, origin, community_id },
|
2022-09-29 21:52:01 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-26 02:07:05 +00:00
|
|
|
pub const ApiSource = struct {
|
2022-09-25 08:10:30 +00:00
|
|
|
db: *sql.Db,
|
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-09-25 08:10:30 +00:00
|
|
|
pub const Conn = ApiConn(*sql.Db);
|
2022-07-26 02:07:05 +00:00
|
|
|
|
2022-09-05 08:52:49 +00:00
|
|
|
const root_username = "root";
|
|
|
|
|
2022-09-29 21:52:01 +00:00
|
|
|
pub fn init(alloc: std.mem.Allocator, cfg: Config, db_conn: *sql.Db) !ApiSource {
|
|
|
|
return ApiSource{
|
2022-09-15 01:12:07 +00:00
|
|
|
.db = db_conn,
|
2022-09-05 08:52:49 +00:00
|
|
|
.internal_alloc = alloc,
|
|
|
|
.config = cfg,
|
|
|
|
};
|
2022-09-07 23:14:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
|
2022-09-29 21:52:01 +00:00
|
|
|
var arena = std.heap.ArenaAllocator.init(alloc);
|
|
|
|
errdefer arena.deinit();
|
|
|
|
|
|
|
|
const community = try services.communities.getByHost(self.db, host, arena.allocator());
|
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,
|
2022-09-29 21:52:01 +00:00
|
|
|
.community = community,
|
|
|
|
.arena = arena,
|
2022-07-26 02:07:05 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
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-29 21:52:01 +00:00
|
|
|
var arena = std.heap.ArenaAllocator.init(alloc);
|
|
|
|
errdefer arena.deinit();
|
|
|
|
|
|
|
|
const community = try services.communities.getByHost(self.db, host, arena.allocator());
|
2022-07-17 23:21:03 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
const token_info = try services.auth.verifyToken(
|
|
|
|
self.db,
|
|
|
|
token,
|
|
|
|
community.id,
|
|
|
|
arena.allocator(),
|
|
|
|
);
|
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,
|
2022-10-04 02:41:59 +00:00
|
|
|
.token_info = token_info,
|
|
|
|
.user_id = token_info.account_id,
|
2022-09-29 21:52:01 +00:00
|
|
|
.community = community,
|
|
|
|
.arena = arena,
|
2022-09-07 23:14:52 +00:00
|
|
|
};
|
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-10-04 02:41:59 +00:00
|
|
|
token_info: ?services.auth.TokenInfo = null,
|
|
|
|
user_id: ?Uuid = null,
|
2022-09-29 21:52:01 +00:00
|
|
|
community: services.communities.Community,
|
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-09-08 05:10:58 +00:00
|
|
|
fn isAdmin(self: *Self) bool {
|
|
|
|
// TODO
|
2022-09-29 21:52:01 +00:00
|
|
|
return self.user_id != null and self.community.kind == .admin;
|
2022-09-08 05:10:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-08 07:52:23 +00:00
|
|
|
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResponse {
|
2022-10-04 02:41:59 +00:00
|
|
|
return services.auth.login(
|
|
|
|
self.db,
|
|
|
|
username,
|
|
|
|
self.community.id,
|
|
|
|
password,
|
|
|
|
self.arena.allocator(),
|
|
|
|
);
|
2022-07-26 02:07:05 +00:00
|
|
|
}
|
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
pub const AuthorizationInfo = struct {
|
|
|
|
id: Uuid,
|
2022-09-05 10:33:54 +00:00
|
|
|
username: []const u8,
|
2022-10-04 02:41:59 +00:00
|
|
|
community_id: Uuid,
|
|
|
|
host: []const u8,
|
|
|
|
|
|
|
|
issued_at: DateTime,
|
2022-09-05 10:33:54 +00:00
|
|
|
};
|
2022-10-04 02:41:59 +00:00
|
|
|
pub fn verifyAuthorization(self: *Self) !AuthorizationInfo {
|
|
|
|
if (self.token_info) |info| {
|
|
|
|
const user = try services.users.get(self.db, info.account_id, self.arena.allocator());
|
|
|
|
|
|
|
|
return AuthorizationInfo{
|
|
|
|
.id = user.id,
|
|
|
|
.username = user.username,
|
|
|
|
.community_id = self.community.id,
|
|
|
|
.host = self.community.host,
|
|
|
|
|
|
|
|
.issued_at = info.issued_at,
|
2022-09-05 10:33:54 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return error.Unauthorized;
|
|
|
|
}
|
|
|
|
|
2022-09-07 23:14:52 +00:00
|
|
|
pub fn createCommunity(self: *Self, origin: []const u8) !services.communities.Community {
|
2022-09-08 05:10:58 +00:00
|
|
|
if (!self.isAdmin()) {
|
|
|
|
return error.PermissionDenied;
|
2022-09-07 23:14:52 +00:00
|
|
|
}
|
2022-07-26 02:07:05 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
const tx = try self.db.begin();
|
|
|
|
errdefer tx.rollback();
|
|
|
|
const community_id = try services.communities.create(
|
|
|
|
tx,
|
|
|
|
origin,
|
|
|
|
.{},
|
|
|
|
self.arena.allocator(),
|
|
|
|
);
|
|
|
|
|
|
|
|
const community = services.communities.get(
|
|
|
|
tx,
|
|
|
|
community_id,
|
|
|
|
self.arena.allocator(),
|
|
|
|
) catch |err| return switch (err) {
|
|
|
|
error.NotFound => error.DatabaseError,
|
|
|
|
else => |err2| err2,
|
|
|
|
};
|
|
|
|
|
|
|
|
try tx.commit();
|
|
|
|
|
|
|
|
return community;
|
2022-07-26 02:07:05 +00:00
|
|
|
}
|
2022-07-27 05:02:09 +00:00
|
|
|
|
2022-09-08 05:10:58 +00:00
|
|
|
pub fn createInvite(self: *Self, options: InviteRequest) !services.invites.Invite {
|
|
|
|
// Only logged in users can make invites
|
2022-09-08 07:52:23 +00:00
|
|
|
const user_id = self.user_id orelse return error.TokenRequired;
|
2022-07-27 05:02:09 +00:00
|
|
|
|
2022-08-02 04:33:23 +00:00
|
|
|
const community_id = if (options.to_community) |host| blk: {
|
2022-09-08 05:10:58 +00:00
|
|
|
// You can only specify a different community if you're on the admin domain
|
2022-09-29 21:52:01 +00:00
|
|
|
if (self.community.kind != .admin) return error.WrongCommunity;
|
2022-07-27 05:02:09 +00:00
|
|
|
|
2022-09-08 05:10:58 +00:00
|
|
|
// Only admins can invite on the admin domain
|
|
|
|
if (!self.isAdmin()) return error.PermissionDenied;
|
2022-07-27 05:02:09 +00:00
|
|
|
|
2022-09-25 08:10:30 +00:00
|
|
|
break :blk (try services.communities.getByHost(self.db, host, self.arena.allocator())).id;
|
2022-09-29 21:52:01 +00:00
|
|
|
} else self.community.id;
|
2022-07-27 05:02:09 +00:00
|
|
|
|
2022-09-08 05:10:58 +00:00
|
|
|
// Users can only make user invites
|
2022-10-04 02:41:59 +00:00
|
|
|
if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied;
|
2022-07-27 05:02:09 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
const invite_id = try services.invites.create(self.db, user_id, community_id, .{
|
2022-09-08 05:10:58 +00:00
|
|
|
.name = options.name,
|
2022-10-04 02:41:59 +00:00
|
|
|
.lifespan = options.lifespan,
|
2022-07-27 05:02:09 +00:00
|
|
|
.max_uses = options.max_uses,
|
2022-10-04 02:41:59 +00:00
|
|
|
.kind = options.kind,
|
2022-09-08 05:10:58 +00:00
|
|
|
}, self.arena.allocator());
|
2022-10-04 02:41:59 +00:00
|
|
|
|
|
|
|
return try services.invites.get(self.db, invite_id, self.arena.allocator());
|
2022-07-27 05:02:09 +00:00
|
|
|
}
|
2022-09-08 06:56:29 +00:00
|
|
|
|
2022-09-08 07:52:23 +00:00
|
|
|
pub fn register(self: *Self, request: RegistrationRequest) !UserResponse {
|
2022-09-08 06:56:29 +00:00
|
|
|
std.log.debug("registering user {s} with code {s}", .{ request.username, request.invite_code });
|
2022-09-25 08:10:30 +00:00
|
|
|
const invite = try services.invites.getByCode(self.db, request.invite_code, self.arena.allocator());
|
2022-09-08 06:56:29 +00:00
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
if (!Uuid.eql(invite.community_id, self.community.id)) return error.NotFound;
|
2022-09-08 06:56:29 +00:00
|
|
|
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return error.InviteExpired;
|
|
|
|
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return error.InviteExpired;
|
|
|
|
|
2022-09-29 21:52:01 +00:00
|
|
|
if (self.community.kind == .admin) @panic("Unimplmented");
|
2022-09-08 06:56:29 +00:00
|
|
|
|
2022-09-29 21:52:01 +00:00
|
|
|
const user_id = try services.users.create(self.db, request.username, request.password, self.community.id, .{ .invite_id = invite.id, .email = request.email }, self.internal_alloc);
|
2022-09-08 06:56:29 +00:00
|
|
|
|
|
|
|
switch (invite.invite_type) {
|
|
|
|
.user => {},
|
|
|
|
.system => @panic("System user invites unimplemented"),
|
|
|
|
.community_owner => {
|
2022-09-29 21:52:01 +00:00
|
|
|
try services.communities.transferOwnership(self.db, self.community.id, user_id);
|
2022-09-08 06:56:29 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-09-08 07:52:23 +00:00
|
|
|
return self.getUser(user_id) catch |err| switch (err) {
|
2022-09-08 06:56:29 +00:00
|
|
|
error.NotFound => error.Unexpected,
|
|
|
|
else => err,
|
|
|
|
};
|
|
|
|
}
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
|
2022-09-25 08:10:30 +00:00
|
|
|
const user = try services.users.get(self.db, user_id, self.arena.allocator());
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
if (self.user_id == null) {
|
2022-09-29 21:52:01 +00:00
|
|
|
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
|
2022-09-08 07:52:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return UserResponse{
|
|
|
|
.id = user.id,
|
|
|
|
.username = user.username,
|
|
|
|
.host = user.host,
|
|
|
|
.created_at = user.created_at,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn createNote(self: *Self, content: []const u8) !NoteResponse {
|
2022-09-29 21:52:01 +00:00
|
|
|
if (self.community.kind == .admin) return error.WrongCommunity;
|
2022-09-08 07:52:23 +00:00
|
|
|
const user_id = self.user_id orelse return error.TokenRequired;
|
|
|
|
|
2022-09-25 08:10:30 +00:00
|
|
|
const note_id = try services.notes.create(self.db, user_id, content);
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
return self.getNote(note_id) catch |err| switch (err) {
|
|
|
|
error.NotFound => error.Unexpected,
|
|
|
|
else => err,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn getNote(self: *Self, note_id: Uuid) !NoteResponse {
|
2022-09-25 08:10:30 +00:00
|
|
|
const note = try services.notes.get(self.db, note_id, self.arena.allocator());
|
|
|
|
const user = try services.users.get(self.db, note.author_id, self.arena.allocator());
|
2022-09-08 07:52:23 +00:00
|
|
|
|
|
|
|
// Only serve community-specific notes on unauthenticated requests
|
|
|
|
if (self.user_id == null) {
|
2022-09-29 21:52:01 +00:00
|
|
|
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
|
2022-09-08 07:52:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return NoteResponse{
|
|
|
|
.id = note.id,
|
|
|
|
.author = .{
|
|
|
|
.id = user.id,
|
|
|
|
.username = user.username,
|
|
|
|
.host = user.host,
|
|
|
|
},
|
|
|
|
.content = note.content,
|
|
|
|
.created_at = note.created_at,
|
|
|
|
};
|
|
|
|
}
|
2022-09-10 04:02:51 +00:00
|
|
|
|
|
|
|
pub fn queryCommunities(self: *Self, args: services.communities.QueryArgs) ![]services.communities.Community {
|
|
|
|
if (!self.isAdmin()) return error.PermissionDenied;
|
2022-09-25 08:10:30 +00:00
|
|
|
return services.communities.query(self.db, args, self.arena.allocator());
|
2022-09-10 04:02:51 +00:00
|
|
|
}
|
2022-07-26 02:07:05 +00:00
|
|
|
};
|
|
|
|
}
|