Create cluster community

This commit is contained in:
jaina heartles 2022-09-05 02:15:16 -07:00
parent 1d4b0a6e77
commit 8ee663000d
3 changed files with 22 additions and 181 deletions

View File

@ -130,6 +130,7 @@ pub const ApiSource = struct {
const root_username = "root";
const root_id = Uuid.nil;
const root_password_envvar = "CLUSTER_ROOT_PASSWORD";
const cluster_community_id = Uuid.nil;
pub fn init(alloc: std.mem.Allocator, cfg: Config) !ApiSource {
var self = ApiSource{
@ -152,10 +153,16 @@ pub const ApiSource = struct {
var buf: [pw_hash_buf_size]u8 = undefined;
const hash = try hashPassword(root_password, self.internal_alloc, &buf);
try self.db.insert2("community", .{
.id = cluster_community_id,
.name = "Cluster System Pseudocommunity",
.host = cfg.cluster_host,
.scheme = cfg.cluster_scheme,
});
try self.db.insert2("user", .{
.id = root_id,
.username = root_username,
.community_id = null,
.community_id = cluster_community_id,
});
try self.db.insert2("local_user", .{
.user_id = root_id,
@ -203,6 +210,13 @@ pub const ApiSource = struct {
models.Token.HashFn.hash(&decoded, &hash.data, .{});
const db_token = (try self.db.getBy(models.Token, .hash, hash, conn.arena.allocator())) orelse return error.InvalidToken;
//const token_result = try self.db.execRow2(
//&.{Uuid},
//\\SELECT user.id
//\\FROM token
//\\ JOIN user ON token.user_id = user.id
//\\ JOIN community ON
//);
//const token_result = (try self.db.execRow2(
//&.{Uuid},
//"SELECT id FROM token WHERE hash = ?",
@ -260,182 +274,6 @@ fn ApiConn(comptime DbConn: type) type {
error.NotAuthorized;
}
pub fn createNote(self: *Self, info: NoteCreateInfo) !models.Note {
const id = Uuid.randV4(prng.random());
const actor = try self.getAuthenticatedActor();
const note = models.Note{
.id = id,
.author_id = actor.user_id,
.content = info.content,
.created_at = DateTime.now(),
};
try self.db.insert(models.Note, note);
return note;
}
//pub fn getNote(self: *Self, id: Uuid) !?models.Note {
pub fn getNote(self: *Self, id: Uuid) !?[]const u8 {
const row = try self.db.execRow("select content from note where id = ?", .{id}, &.{[]const u8}, self.arena.allocator());
if (row) |results| {
return results[0];
} else return null;
//return self.db.getBy(models.Note, .id, id, self.arena.allocator());
}
pub fn getActor(self: *Self, user_id: Uuid) !?models.Actor {
return self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator());
}
pub fn getActorByHandle(self: *Self, handle: []const u8) !?models.Actor {
const user = (try self.db.getBy(models.User, .username, handle, self.arena.allocator())) orelse return null;
return self.db.getBy(models.Actor, .user_id, user.id, self.arena.allocator());
}
pub fn react(self: *Self, note_id: Uuid) !void {
const id = Uuid.randV4(prng.random());
const actor = try self.getAuthenticatedActor();
try self.db.insert(models.Reaction, .{ .id = id, .note_id = note_id, .reactor_id = actor.user_id, .created_at = DateTime.now() });
}
pub fn listReacts(self: *Self, note_id: Uuid) ![]models.Reaction {
return try self.db.getWhereEq(models.Reaction, .note_id, note_id, self.arena.allocator());
}
pub fn createCommunity(self: *Self, info: CommunityCreateOptions) !models.Community {
const scheme_len = firstIndexOf(info.host, ':') orelse return error.InvalidHost;
const scheme_str = info.host[0..scheme_len];
const scheme = std.meta.stringToEnum(models.Community.Scheme, scheme_str) orelse return error.UnsupportedScheme;
const host = blk: {
// host must be in the format "{scheme}://{host}"
if (info.host.len <= scheme_len + ("://").len or
info.host[scheme_len] != ':' or
info.host[scheme_len + 1] != '/' or
info.host[scheme_len + 2] != '/') return error.InvalidHost;
const host = info.host[scheme_len + 3 ..];
// community cannot use non-default ports (except for testing)
// NOTE: Do not add, say localhost and localhost:80 or bugs may happen.
// Avoid using non-default ports unless a test can't be conducted without it.
if (firstIndexOf(host, ':') != null and builtin.mode != .Debug) return error.InvalidHost;
// community cannot be hosted on a path
if (firstIndexOf(host, '/') != null) return error.InvalidHost;
break :blk host;
};
const id = Uuid.randV4(prng.random());
const now = DateTime.now();
// Require TLS on production builds
if (scheme != .https and builtin.mode != .Debug) return error.UnsupportedScheme;
const community = models.Community{
.id = id,
.created_at = now,
.name = info.name,
.host = host,
.scheme = scheme,
};
try self.db.insert(models.Community, community);
return community;
}
pub fn getCommunity(self: *Self, host: []const u8) !?models.Community {
return try self.db.getBy(models.Community, .host, host, self.arena.allocator());
}
pub fn registerOld(self: *Self, info: RegistrationInfo) !models.Actor {
// Try to find invite id
const user_id = Uuid.randV4(prng.random());
// TODO: not community aware :(
if (try self.db.execRow2(&.{}, "SELECT 1 FROM user WHERE username = ?", .{info.username}, null) != null) {
//if (try self.db.existsWhereEq(models.User, .username, info.username)) {
return error.UsernameUnavailable;
}
const now = DateTime.now();
const invite_id = if (info.invite_code) |invite_code| blk: {
// TODO have this query also check for time-based expiration
const result = (try self.db.execRow2(
&.{ Uuid, ?DateTime },
\\SELECT invite.id, invite.expires_at
\\FROM invite
\\ LEFT OUTER JOIN local_user ON invite.id = local_user.invite_id
\\WHERE invite.invite_code = ?
\\GROUP BY invite.id
\\HAVING
\\ (invite.max_uses IS NULL OR invite.max_uses > COUNT(local_user.user_id))
\\
,
.{invite_code},
null,
)) orelse return error.InvalidInvite;
const expired = if (result[1]) |expires_at| now.seconds_since_epoch > expires_at.seconds_since_epoch else false;
if (expired) return error.InvalidInvite;
//const invite = (try self.db.getBy(models.Invite, .invite_code, invite_code, self.arena.allocator())) orelse return error.InvalidInvite;
//const invite = (try self.db.getBy(models.Invite, .invite_code, invite_code, self.arena.allocator())) orelse return error.InvalidInvite;
//const uses = try self.db.countWhereEq(models.LocalUser, .invite_id, invite.id);
//const uses_left = if (invite.max_uses) |max_uses| uses < max_uses else true;
//const expired = if (invite.expires_at) |expires_at| now.seconds_since_epoch > expires_at.seconds_since_epoch else false;
//if (!uses_left or expired) return error.InvalidInvite;
// TODO: increment uses
break :blk result[0];
} else null;
// use internal alloc because necessary buffer is *big*
var buf: [pw_hash_buf_size]u8 = undefined;
const hash = try PwHash.strHash(info.password, .{ .allocator = self.internal_alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, &buf);
const community_id = if (info.community_host) |host| blk: {
//const id_tuple = (try self.db.execRow("select id from community where host = '?'", host, &.{Uuid}, self.arena.allocator())) orelse return error.CommunityNotFound;
const community_result = (try self.db.execRow2(
&.{Uuid},
"SELECT id FROM community WHERE host = ?",
.{host},
null,
)) orelse return error.CommunityNotFound;
//const community = (try self.db.getBy(models.Community, .host, host, self.arena.allocator())) orelse return error.CommunityNotFound;
break :blk community_result[0];
//break :blk id_tuple[0];
} else null;
const user = models.User{
.id = user_id,
.username = info.username,
.created_at = now,
.community_id = community_id,
};
const actor = models.Actor{
.user_id = user_id,
.public_id = "abc", // TODO
};
const local_user = models.LocalUser{
.user_id = user_id,
.email = info.email,
.invite_id = invite_id,
.hashed_password = hash,
.password_changed_at = now,
};
try self.db.insert(models.User, user);
try self.db.insert(models.Actor, actor);
try self.db.insert(models.LocalUser, local_user);
return actor;
}
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResult {
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
//const user_info = (try self.db.getBy(models.User, .username, username, self.arena.allocator())) orelse return error.InvalidLogin;

View File

@ -71,8 +71,8 @@ const create_migration_table =
\\CREATE TABLE IF NOT EXISTS
\\migration(
\\ name TEXT NOT NULL PRIMARY KEY,
\\ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
\\ applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
;
// NOTE: Until the first public release, i may collapse multiple
@ -182,8 +182,8 @@ const migrations: []const Migration = &.{
\\ host TEXT NOT NULL UNIQUE,
\\ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
\\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
\\ALTER TABLE user ADD COLUMN community_id TEXT REFERENCES community(id);
\\ALTER TABLE invite ADD COLUMN to_community TEXT REFERENCES community(id);
,

View File

@ -4,6 +4,7 @@ const http = @import("http");
const util = @import("util");
const api = @import("./api.zig");
const models = @import("./db/models.zig");
const Uuid = util.Uuid;
const c = @import("./controllers.zig");
@ -19,6 +20,7 @@ const router = Router{
//Route.new(.POST, "/auth/register", &c.auth.register),
Route.new(.POST, "/login", &c.auth.login),
//Route.new(.GET, "/current-login", &c.auth.verifyLogin),
//Route.new(.POST, "/notes", &c.notes.create),
//Route.new(.GET, "/notes/:id", &c.notes.get),
@ -75,6 +77,7 @@ pub const RequestServer = struct {
pub const Config = struct {
cluster_host: []const u8,
cluster_scheme: models.Community.Scheme,
};
fn loadConfig(alloc: std.mem.Allocator) !Config {