Clean up db models

This commit is contained in:
jaina heartles 2022-07-29 22:46:00 -07:00
parent f6f24a557e
commit 99192bccdd
4 changed files with 136 additions and 85 deletions

View file

@ -156,34 +156,31 @@ fn ApiConn(comptime DbConn: type) type {
self.arena.deinit(); self.arena.deinit();
} }
fn getAuthenticatedUser(self: *Self) !models.LocalUser { fn getAuthenticatedLocalUser(self: *Self) !models.LocalUser {
if (self.as_user) |user_id| { if (self.as_user) |user_id| {
const local_user = try self.db.getBy(models.LocalUser, .id, user_id, self.arena.allocator()); const local_user = try self.db.getBy(models.LocalUser, .user_id, user_id, self.arena.allocator());
if (local_user == null) return error.UserNotFound; if (local_user == null) return error.NotAuthorized;
return local_user.?; return local_user.?;
} else { } else {
return error.NotAuthenticated; return error.NotAuthorized;
} }
} }
fn getAuthenticatedActor(self: *Self) !models.Actor { fn getAuthenticatedActor(self: *Self) !models.Actor {
const user = try self.getAuthenticatedUser(); return if (self.as_user) |user_id|
if (user.actor_id) |actor_id| { (try self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator())) orelse error.NotAuthorized
const actor = try self.db.getBy(models.Actor, .id, actor_id, self.arena); else
return actor.?; error.NotAuthorized;
} else {
return error.NoActor;
}
} }
pub fn createNote(self: *Self, info: NoteCreateInfo) !models.Note { pub fn createNote(self: *Self, info: NoteCreateInfo) !models.Note {
const id = Uuid.randV4(prng.random()); const id = Uuid.randV4(prng.random());
const user = try self.getAuthenticatedUser(); const actor = try self.getAuthenticatedActor();
const note = models.Note{ const note = models.Note{
.id = id, .id = id,
.author_id = user.actor_id orelse return error.NotAuthorized, .author_id = actor.user_id,
.content = info.content, .content = info.content,
.created_at = DateTime.now(), .created_at = DateTime.now(),
@ -197,18 +194,19 @@ fn ApiConn(comptime DbConn: type) type {
return self.db.getBy(models.Note, .id, id, self.arena.allocator()); return self.db.getBy(models.Note, .id, id, self.arena.allocator());
} }
pub fn getActor(self: *Self, id: Uuid) !?models.Actor { pub fn getActor(self: *Self, user_id: Uuid) !?models.Actor {
return self.db.getBy(models.Actor, .id, id, self.arena.allocator()); return self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator());
} }
pub fn getActorByHandle(self: *Self, handle: []const u8) !?models.Actor { pub fn getActorByHandle(self: *Self, handle: []const u8) !?models.Actor {
return self.db.getBy(models.Actor, .handle, handle, self.arena.allocator()); 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 { pub fn react(self: *Self, note_id: Uuid) !void {
const id = Uuid.randV4(prng.random()); const id = Uuid.randV4(prng.random());
const user = try self.getAuthenticatedUser(); const actor = try self.getAuthenticatedActor();
try self.db.insert(models.Reaction, .{ .id = id, .note_id = note_id, .reactor_id = user.actor_id orelse return error.NotAuthorized, .created_at = DateTime.now() }); 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 { pub fn listReacts(self: *Self, note_id: Uuid) ![]models.Reaction {
@ -216,18 +214,13 @@ fn ApiConn(comptime DbConn: type) type {
} }
pub fn register(self: *Self, info: RegistrationInfo) !models.Actor { pub fn register(self: *Self, info: RegistrationInfo) !models.Actor {
const actor_id = Uuid.randV4(prng.random());
const user_id = Uuid.randV4(prng.random()); const user_id = Uuid.randV4(prng.random());
// TODO: lock for transaction // TODO: lock for transaction
if (try self.db.existsWhereEq(models.LocalUser, .username, info.username)) { if (try self.db.existsWhereEq(models.User, .username, info.username)) {
return error.UsernameUnavailable; return error.UsernameUnavailable;
} }
if (try self.db.existsWhereEq(models.Actor, .handle, info.username)) {
return error.InconsistentDb;
}
const now = DateTime.now(); const now = DateTime.now();
const invite_id = if (info.invite_code) |invite_code| blk: { const invite_id = if (info.invite_code) |invite_code| blk: {
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;
@ -244,39 +237,42 @@ fn ApiConn(comptime DbConn: type) type {
var buf: [pw_hash_buf_size]u8 = undefined; 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 hash = try PwHash.strHash(info.password, .{ .allocator = self.internal_alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, &buf);
const actor = models.Actor{ const user = models.User{
.id = actor_id, .id = user_id,
.handle = info.username, .username = info.username,
.created_at = now, .created_at = now,
}; };
const user = models.LocalUser{ const actor = models.Actor{
.id = user_id, .user_id = user_id,
.actor_id = actor_id, .public_id = "abc", // TODO
.username = info.username, };
const local_user = models.LocalUser{
.user_id = user_id,
.email = info.email, .email = info.email,
.invite_id = invite_id, .invite_id = invite_id,
.hashed_password = hash, .hashed_password = hash,
.password_changed_at = now, .password_changed_at = now,
.created_at = now,
}; };
try self.db.insert(models.User, user);
try self.db.insert(models.Actor, actor); try self.db.insert(models.Actor, actor);
try self.db.insert(models.LocalUser, user); try self.db.insert(models.LocalUser, local_user);
return actor; 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.LocalUser, .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;
const local_user_info = (try self.db.getBy(models.LocalUser, .user_id, user_info.id, self.arena.allocator())) orelse return error.InvalidLogin;
//defer free(self.arena.allocator(), user_info); //defer free(self.arena.allocator(), user_info);
const Hash = std.crypto.pwhash.scrypt; const Hash = std.crypto.pwhash.scrypt;
Hash.strVerify(user_info.hashed_password, password, .{ .allocator = self.internal_alloc }) catch |err| switch (err) { Hash.strVerify(local_user_info.hashed_password, password, .{ .allocator = self.internal_alloc }) catch |err| switch (err) {
error.PasswordVerificationFailed => return error.InvalidLogin, error.PasswordVerificationFailed => return error.InvalidLogin,
else => return err, else => return err,
}; };
const token = try self.createToken(user_info); const token = try self.createToken(user_info.id);
var token_enc: [token_str_len]u8 = undefined; var token_enc: [token_str_len]u8 = undefined;
_ = std.base64.standard.Encoder.encode(&token_enc, &token.value); _ = std.base64.standard.Encoder.encode(&token_enc, &token.value);
@ -292,7 +288,7 @@ fn ApiConn(comptime DbConn: type) type {
info: models.Token, info: models.Token,
value: [token_len]u8, value: [token_len]u8,
}; };
fn createToken(self: *Self, user: models.LocalUser) !TokenResult { fn createToken(self: *Self, user_id: Uuid) !TokenResult {
var token: [token_len]u8 = undefined; var token: [token_len]u8 = undefined;
std.crypto.random.bytes(&token); std.crypto.random.bytes(&token);
@ -302,7 +298,7 @@ fn ApiConn(comptime DbConn: type) type {
const db_token = models.Token{ const db_token = models.Token{
.id = Uuid.randV4(prng.random()), .id = Uuid.randV4(prng.random()),
.hash = .{ .data = hash }, .hash = .{ .data = hash },
.user_id = user.id, .user_id = user_id,
.issued_at = DateTime.now(), .issued_at = DateTime.now(),
}; };
@ -316,7 +312,7 @@ fn ApiConn(comptime DbConn: type) type {
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite { pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
const id = Uuid.randV4(prng.random()); const id = Uuid.randV4(prng.random());
const user_id = (try self.getAuthenticatedUser()).id; const user_id = (try self.getAuthenticatedLocalUser()).user_id;
var code: [invite_code_len]u8 = undefined; var code: [invite_code_len]u8 = undefined;
std.crypto.random.bytes(&code); std.crypto.random.bytes(&code);

View file

@ -8,11 +8,25 @@ const DateTime = util.DateTime;
const String = []const u8; const String = []const u8;
const comptimePrint = std.fmt.comptimePrint; const comptimePrint = std.fmt.comptimePrint;
fn baseTypeName(comptime T: type) []const u8 {
comptime {
const name = @typeName(T);
const start = for (name) |_, i| {
if (name[name.len - i] == '.') break name.len - i;
} else 0;
return name[start..];
}
}
fn tableName(comptime T: type) String { fn tableName(comptime T: type) String {
//return util.case.pascalToSnake(baseTypeName(T));
return switch (T) { return switch (T) {
models.Note => "note", models.Note => "note",
models.Actor => "actor", models.Actor => "actor",
models.Reaction => "reaction", models.Reaction => "reaction",
models.User => "user",
models.LocalUser => "local_user", models.LocalUser => "local_user",
models.Token => "token", models.Token => "token",
models.Invite => "invite", models.Invite => "invite",
@ -138,33 +152,28 @@ pub const Database = struct {
db: sql.Sqlite, db: sql.Sqlite,
const init_sql_stmts = [_][]const u8{ const init_sql_stmts = [_][]const u8{
\\CREATE TABLE IF NOT EXISTS
\\user(
\\ id TEXT NOT NULL PRIMARY KEY,
\\ username TEXT NOT NULL,
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
\\actor( \\actor(
\\ id TEXT NOT NULL, \\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
\\ \\ public_id TEXT NOT NULL
\\ handle TEXT NOT NULL,
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\
\\ PRIMARY KEY(id)
\\) STRICT; \\) STRICT;
, ,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
\\local_user( \\local_user(
\\ id TEXT NOT NULL, \\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
\\ actor_id TEXT,
\\ \\
\\ username TEXT NOT NULL,
\\ email TEXT, \\ email TEXT,
\\ \\
\\ hashed_password TEXT NOT NULL, \\ hashed_password TEXT NOT NULL,
\\ password_changed_at INTEGER NOT NULL, \\ password_changed_at INTEGER NOT NULL
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\
\\ UNIQUE(actor_id),
\\ FOREIGN KEY(actor_id) REFERENCES actor(id),
\\
\\ PRIMARY KEY(id)
\\) STRICT; \\) STRICT;
, ,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
@ -172,38 +181,29 @@ pub const Database = struct {
\\ id TEXT NOT NULL, \\ id TEXT NOT NULL,
\\ \\
\\ content TEXT NOT NULL, \\ content TEXT NOT NULL,
\\ author_id TEXT NOT NULL, \\ author_id TEXT NOT NULL REFERENCES actor(id),
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\ \\
\\ FOREIGN KEY(author_id) REFERENCES actor(id), \\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\
\\ PRIMARY KEY(id)
\\) STRICT; \\) STRICT;
, ,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
\\reaction( \\reaction(
\\ id TEXT NOT NULL, \\ id TEXT NOT NULL PRIMARY KEY,
\\ \\
\\ reactor_id TEXT NOT NULL, \\ reactor_id TEXT NOT NULL REFERENCES actor(id),
\\ note_id TEXT NOT NULL, \\ note_id TEXT NOT NULL REFERENCES note(id),
\\ \\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, \\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\
\\ FOREIGN KEY(reactor_id) REFERENCES actor(id),
\\ FOREIGN KEY(note_id) REFERENCES note(id),
\\
\\ PRIMARY KEY(id)
\\) STRICT; \\) STRICT;
, ,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
\\token( \\token(
\\ id TEXT NOT NULL, \\ id TEXT NOT NULL PRIMARY KEY,
\\ \\
\\ hash BLOB UNIQUE NOT NULL, \\ hash BLOB UNIQUE NOT NULL,
\\ user_id TEXT NOT NULL REFERENCES local_user(id), \\ user_id TEXT NOT NULL REFERENCES local_user(id),
\\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\ \\
\\ PRIMARY KEY(id) \\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT; \\) STRICT;
, ,
\\CREATE TABLE IF NOT EXISTS \\CREATE TABLE IF NOT EXISTS
@ -211,12 +211,12 @@ pub const Database = struct {
\\ id TEXT NOT NULL PRIMARY KEY, \\ id TEXT NOT NULL PRIMARY KEY,
\\ \\
\\ name TEXT NOT NULL, \\ name TEXT NOT NULL,
\\ invite_code TEXT NOT NULL, \\ invite_code TEXT NOT NULL UNIQUE,
\\ created_by TEXT NOT NULL REFERENCES local_user(id), \\ created_by TEXT NOT NULL REFERENCES local_user(id),
\\ \\
\\ max_uses INTEGER, \\ max_uses INTEGER,
\\ \\
\\ created_at INTEGER NOT NULL, \\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\ expires_at INTEGER \\ expires_at INTEGER
\\) STRICT; \\) STRICT;
, ,

View file

@ -56,31 +56,32 @@ fn Ref(comptime _: type) type {
return Uuid; return Uuid;
} }
pub const Note = struct { pub const User = struct {
id: Uuid, id: Uuid,
content: []const u8, username: []const u8,
author_id: Ref(Actor),
created_at: DateTime, created_at: DateTime,
}; };
pub const Actor = struct { pub const Actor = struct {
id: Uuid, user_id: Ref(User),
handle: []const u8, public_id: []const u8,
created_at: DateTime,
}; };
pub const LocalUser = struct { pub const LocalUser = struct {
id: Uuid, user_id: Ref(User),
actor_id: ?Ref(Actor),
username: []const u8,
email: ?[]const u8, email: ?[]const u8,
invite_id: ?Ref(Invite), invite_id: ?Ref(Invite),
hashed_password: []const u8, // encoded in PHC format, with salt hashed_password: []const u8, // encoded in PHC format, with salt
password_changed_at: DateTime, password_changed_at: DateTime,
};
pub const Note = struct {
id: Uuid,
content: []const u8,
author_id: Ref(Actor),
created_at: DateTime, created_at: DateTime,
}; };

View file

@ -1,8 +1,62 @@
const std = @import("std");
pub const ciutf8 = @import("./ciutf8.zig"); pub const ciutf8 = @import("./ciutf8.zig");
pub const Uuid = @import("./Uuid.zig"); pub const Uuid = @import("./Uuid.zig");
pub const DateTime = @import("./DateTime.zig"); pub const DateTime = @import("./DateTime.zig");
pub const PathIter = @import("./PathIter.zig"); pub const PathIter = @import("./PathIter.zig");
pub const case = struct {
// returns the number of capital letters in a string.
// only works with ascii characters
fn countCaps(str: []const u8) usize {
var count: usize = 0;
for (str) |ch| {
if (std.ascii.isUpper(ch)) {
count += 1;
}
}
return count;
}
// converts a string from PascalCase to snake_case at comptime.
// only works with ascii characters
pub fn PascalToSnake(comptime str: []const u8) Return: {
break :Return if (str.len == 0)
*const [0:0]u8
else
*const [str.len + countCaps(str) - 1:0]u8;
} {
comptime {
if (str.len == 0) return "";
var buf = std.mem.zeroes([str.len + countCaps(str) - 1:0]u8);
var i = 0;
for (str) |ch| {
if (std.ascii.isUpper(ch)) {
if (i != 0) {
buf[i] = '_';
i += 1;
}
buf[i] = std.ascii.toLower(ch);
} else {
buf[i] = ch;
}
i += 1;
}
return &buf;
}
}
};
test "pascalToSnake" {
try std.testing.expectEqual("", case.PascalToSnake(""));
try std.testing.expectEqual("abc", case.PascalToSnake("Abc"));
try std.testing.expectEqual("a_bc", case.PascalToSnake("ABc"));
try std.testing.expectEqual("a_b_c", case.PascalToSnake("ABC"));
try std.testing.expectEqual("ab_c", case.PascalToSnake("AbC"));
}
test { test {
_ = ciutf8; _ = ciutf8;
_ = Uuid; _ = Uuid;