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_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;
|
||||
|
|
|
@ -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);
|
||||
,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue