diff --git a/src/main/api.zig b/src/main/api.zig index 3fc6213..129fff2 100644 --- a/src/main/api.zig +++ b/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; diff --git a/src/main/db/migrations.zig b/src/main/db/migrations.zig index 6a3c4fb..e50bb6d 100644 --- a/src/main/db/migrations.zig +++ b/src/main/db/migrations.zig @@ -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); , diff --git a/src/main/main.zig b/src/main/main.zig index a121743..b776bcf 100644 --- a/src/main/main.zig +++ b/src/main/main.zig @@ -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 {