Create cluster community
This commit is contained in:
parent
1d4b0a6e77
commit
8ee663000d
3 changed files with 22 additions and 181 deletions
192
src/main/api.zig
192
src/main/api.zig
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
,
|
,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue