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_username = "root";
const root_id = Uuid.nil; const root_id = Uuid.nil;
const root_password_envvar = "CLUSTER_ROOT_PASSWORD"; const root_password_envvar = "CLUSTER_ROOT_PASSWORD";
const cluster_community_id = Uuid.nil;
pub fn init(alloc: std.mem.Allocator, cfg: Config) !ApiSource { pub fn init(alloc: std.mem.Allocator, cfg: Config) !ApiSource {
var self = ApiSource{ var self = ApiSource{
@ -152,10 +153,16 @@ pub const ApiSource = struct {
var buf: [pw_hash_buf_size]u8 = undefined; var buf: [pw_hash_buf_size]u8 = undefined;
const hash = try hashPassword(root_password, self.internal_alloc, &buf); 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", .{ try self.db.insert2("user", .{
.id = root_id, .id = root_id,
.username = root_username, .username = root_username,
.community_id = null, .community_id = cluster_community_id,
}); });
try self.db.insert2("local_user", .{ try self.db.insert2("local_user", .{
.user_id = root_id, .user_id = root_id,
@ -203,6 +210,13 @@ pub const ApiSource = struct {
models.Token.HashFn.hash(&decoded, &hash.data, .{}); 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 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( //const token_result = (try self.db.execRow2(
//&.{Uuid}, //&.{Uuid},
//"SELECT id FROM token WHERE hash = ?", //"SELECT id FROM token WHERE hash = ?",
@ -260,182 +274,6 @@ fn ApiConn(comptime DbConn: type) type {
error.NotAuthorized; 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 { 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? // 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; //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 \\CREATE TABLE IF NOT EXISTS
\\migration( \\migration(
\\ name TEXT NOT NULL PRIMARY KEY, \\ name TEXT NOT NULL PRIMARY KEY,
\\ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP \\ applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT; \\);
; ;
// NOTE: Until the first public release, i may collapse multiple // NOTE: Until the first public release, i may collapse multiple
@ -182,8 +182,8 @@ const migrations: []const Migration = &.{
\\ host TEXT NOT NULL UNIQUE, \\ host TEXT NOT NULL UNIQUE,
\\ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')), \\ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
\\ \\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP \\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT; \\);
\\ALTER TABLE user ADD COLUMN community_id TEXT REFERENCES community(id); \\ALTER TABLE user ADD COLUMN community_id TEXT REFERENCES community(id);
\\ALTER TABLE invite ADD COLUMN to_community 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 util = @import("util");
const api = @import("./api.zig"); const api = @import("./api.zig");
const models = @import("./db/models.zig");
const Uuid = util.Uuid; const Uuid = util.Uuid;
const c = @import("./controllers.zig"); const c = @import("./controllers.zig");
@ -19,6 +20,7 @@ const router = Router{
//Route.new(.POST, "/auth/register", &c.auth.register), //Route.new(.POST, "/auth/register", &c.auth.register),
Route.new(.POST, "/login", &c.auth.login), Route.new(.POST, "/login", &c.auth.login),
//Route.new(.GET, "/current-login", &c.auth.verifyLogin),
//Route.new(.POST, "/notes", &c.notes.create), //Route.new(.POST, "/notes", &c.notes.create),
//Route.new(.GET, "/notes/:id", &c.notes.get), //Route.new(.GET, "/notes/:id", &c.notes.get),
@ -75,6 +77,7 @@ pub const RequestServer = struct {
pub const Config = struct { pub const Config = struct {
cluster_host: []const u8, cluster_host: []const u8,
cluster_scheme: models.Community.Scheme,
}; };
fn loadConfig(alloc: std.mem.Allocator) !Config { fn loadConfig(alloc: std.mem.Allocator) !Config {