Clean up db models
This commit is contained in:
parent
f6f24a557e
commit
99192bccdd
4 changed files with 136 additions and 85 deletions
|
@ -156,34 +156,31 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
self.arena.deinit();
|
||||
}
|
||||
|
||||
fn getAuthenticatedUser(self: *Self) !models.LocalUser {
|
||||
fn getAuthenticatedLocalUser(self: *Self) !models.LocalUser {
|
||||
if (self.as_user) |user_id| {
|
||||
const local_user = try self.db.getBy(models.LocalUser, .id, user_id, self.arena.allocator());
|
||||
if (local_user == null) return error.UserNotFound;
|
||||
const local_user = try self.db.getBy(models.LocalUser, .user_id, user_id, self.arena.allocator());
|
||||
if (local_user == null) return error.NotAuthorized;
|
||||
|
||||
return local_user.?;
|
||||
} else {
|
||||
return error.NotAuthenticated;
|
||||
return error.NotAuthorized;
|
||||
}
|
||||
}
|
||||
|
||||
fn getAuthenticatedActor(self: *Self) !models.Actor {
|
||||
const user = try self.getAuthenticatedUser();
|
||||
if (user.actor_id) |actor_id| {
|
||||
const actor = try self.db.getBy(models.Actor, .id, actor_id, self.arena);
|
||||
return actor.?;
|
||||
} else {
|
||||
return error.NoActor;
|
||||
}
|
||||
return if (self.as_user) |user_id|
|
||||
(try self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator())) orelse error.NotAuthorized
|
||||
else
|
||||
error.NotAuthorized;
|
||||
}
|
||||
|
||||
pub fn createNote(self: *Self, info: NoteCreateInfo) !models.Note {
|
||||
const id = Uuid.randV4(prng.random());
|
||||
const user = try self.getAuthenticatedUser();
|
||||
const actor = try self.getAuthenticatedActor();
|
||||
|
||||
const note = models.Note{
|
||||
.id = id,
|
||||
.author_id = user.actor_id orelse return error.NotAuthorized,
|
||||
.author_id = actor.user_id,
|
||||
.content = info.content,
|
||||
|
||||
.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());
|
||||
}
|
||||
|
||||
pub fn getActor(self: *Self, id: Uuid) !?models.Actor {
|
||||
return self.db.getBy(models.Actor, .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 {
|
||||
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 {
|
||||
const id = Uuid.randV4(prng.random());
|
||||
const user = try self.getAuthenticatedUser();
|
||||
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() });
|
||||
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 {
|
||||
|
@ -216,18 +214,13 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
}
|
||||
|
||||
pub fn register(self: *Self, info: RegistrationInfo) !models.Actor {
|
||||
const actor_id = Uuid.randV4(prng.random());
|
||||
const user_id = Uuid.randV4(prng.random());
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (try self.db.existsWhereEq(models.Actor, .handle, info.username)) {
|
||||
return error.InconsistentDb;
|
||||
}
|
||||
|
||||
const now = DateTime.now();
|
||||
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;
|
||||
|
@ -244,39 +237,42 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
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 actor = models.Actor{
|
||||
.id = actor_id,
|
||||
.handle = info.username,
|
||||
const user = models.User{
|
||||
.id = user_id,
|
||||
.username = info.username,
|
||||
.created_at = now,
|
||||
};
|
||||
const user = models.LocalUser{
|
||||
.id = user_id,
|
||||
.actor_id = actor_id,
|
||||
.username = info.username,
|
||||
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,
|
||||
.created_at = now,
|
||||
};
|
||||
try self.db.insert(models.User, user);
|
||||
try self.db.insert(models.Actor, actor);
|
||||
try self.db.insert(models.LocalUser, user);
|
||||
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.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);
|
||||
|
||||
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,
|
||||
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;
|
||||
_ = std.base64.standard.Encoder.encode(&token_enc, &token.value);
|
||||
|
@ -292,7 +288,7 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
info: models.Token,
|
||||
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;
|
||||
std.crypto.random.bytes(&token);
|
||||
|
||||
|
@ -302,7 +298,7 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
const db_token = models.Token{
|
||||
.id = Uuid.randV4(prng.random()),
|
||||
.hash = .{ .data = hash },
|
||||
.user_id = user.id,
|
||||
.user_id = user_id,
|
||||
.issued_at = DateTime.now(),
|
||||
};
|
||||
|
||||
|
@ -316,7 +312,7 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
|
||||
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;
|
||||
std.crypto.random.bytes(&code);
|
||||
|
|
|
@ -8,11 +8,25 @@ const DateTime = util.DateTime;
|
|||
const String = []const u8;
|
||||
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 {
|
||||
//return util.case.pascalToSnake(baseTypeName(T));
|
||||
|
||||
return switch (T) {
|
||||
models.Note => "note",
|
||||
models.Actor => "actor",
|
||||
models.Reaction => "reaction",
|
||||
models.User => "user",
|
||||
models.LocalUser => "local_user",
|
||||
models.Token => "token",
|
||||
models.Invite => "invite",
|
||||
|
@ -138,33 +152,28 @@ pub const Database = struct {
|
|||
db: sql.Sqlite,
|
||||
|
||||
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
|
||||
\\actor(
|
||||
\\ id TEXT NOT NULL,
|
||||
\\
|
||||
\\ handle TEXT NOT NULL,
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\\
|
||||
\\ PRIMARY KEY(id)
|
||||
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||
\\ public_id TEXT NOT NULL
|
||||
\\) STRICT;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\local_user(
|
||||
\\ id TEXT NOT NULL,
|
||||
\\ actor_id TEXT,
|
||||
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||
\\
|
||||
\\ username TEXT NOT NULL,
|
||||
\\ email TEXT,
|
||||
\\
|
||||
\\ hashed_password TEXT 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)
|
||||
\\ password_changed_at INTEGER NOT NULL
|
||||
\\) STRICT;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
|
@ -172,38 +181,29 @@ pub const Database = struct {
|
|||
\\ id TEXT NOT NULL,
|
||||
\\
|
||||
\\ content TEXT NOT NULL,
|
||||
\\ author_id TEXT NOT NULL,
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\\ author_id TEXT NOT NULL REFERENCES actor(id),
|
||||
\\
|
||||
\\ FOREIGN KEY(author_id) REFERENCES actor(id),
|
||||
\\
|
||||
\\ PRIMARY KEY(id)
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\) STRICT;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\reaction(
|
||||
\\ id TEXT NOT NULL,
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ reactor_id TEXT NOT NULL,
|
||||
\\ note_id TEXT NOT NULL,
|
||||
\\ reactor_id TEXT NOT NULL REFERENCES actor(id),
|
||||
\\ note_id TEXT NOT NULL REFERENCES note(id),
|
||||
\\
|
||||
\\ 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)
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\) STRICT;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\token(
|
||||
\\ id TEXT NOT NULL,
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ hash BLOB UNIQUE NOT NULL,
|
||||
\\ 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;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
|
@ -211,12 +211,12 @@ pub const Database = struct {
|
|||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ name TEXT NOT NULL,
|
||||
\\ invite_code TEXT NOT NULL,
|
||||
\\ invite_code TEXT NOT NULL UNIQUE,
|
||||
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
|
||||
\\
|
||||
\\ max_uses INTEGER,
|
||||
\\
|
||||
\\ created_at INTEGER NOT NULL,
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\\ expires_at INTEGER
|
||||
\\) STRICT;
|
||||
,
|
||||
|
|
|
@ -56,31 +56,32 @@ fn Ref(comptime _: type) type {
|
|||
return Uuid;
|
||||
}
|
||||
|
||||
pub const Note = struct {
|
||||
pub const User = struct {
|
||||
id: Uuid,
|
||||
content: []const u8,
|
||||
author_id: Ref(Actor),
|
||||
username: []const u8,
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
||||
pub const Actor = struct {
|
||||
id: Uuid,
|
||||
handle: []const u8,
|
||||
|
||||
created_at: DateTime,
|
||||
user_id: Ref(User),
|
||||
public_id: []const u8,
|
||||
};
|
||||
|
||||
pub const LocalUser = struct {
|
||||
id: Uuid,
|
||||
actor_id: ?Ref(Actor),
|
||||
user_id: Ref(User),
|
||||
|
||||
username: []const u8,
|
||||
email: ?[]const u8,
|
||||
invite_id: ?Ref(Invite),
|
||||
|
||||
hashed_password: []const u8, // encoded in PHC format, with salt
|
||||
password_changed_at: DateTime,
|
||||
};
|
||||
|
||||
pub const Note = struct {
|
||||
id: Uuid,
|
||||
content: []const u8,
|
||||
author_id: Ref(Actor),
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,62 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const ciutf8 = @import("./ciutf8.zig");
|
||||
pub const Uuid = @import("./Uuid.zig");
|
||||
pub const DateTime = @import("./DateTime.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 {
|
||||
_ = ciutf8;
|
||||
_ = Uuid;
|
||||
|
|
Loading…
Reference in a new issue