Split users into actors and localusers

This commit is contained in:
jaina heartles 2022-07-21 21:19:08 -07:00
parent a4f6fca675
commit ee0db68c5e
5 changed files with 115 additions and 42 deletions

View file

@ -7,6 +7,11 @@ pub const models = @import("./models.zig");
pub const DateTime = util.DateTime;
pub const Uuid = util.Uuid;
const PwHash = std.crypto.pwhash.scrypt;
const pw_hash_params = PwHash.Params.interactive;
const pw_hash_encoding = .phc;
const pw_hash_buf_size = 128;
// Frees an api struct and its fields allocated from alloc
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
switch (@typeInfo(@TypeOf(val))) {
@ -60,7 +65,7 @@ fn reify(comptime T: type, id: Uuid, val: CreateInfo(T)) T {
pub const ApiContext = struct {
user_context: struct {
user: models.User,
user: models.LocalUser,
},
alloc: std.mem.Allocator,
@ -70,23 +75,31 @@ pub const NoteCreate = struct {
content: []const u8,
};
pub const RegistrationInfo = struct {
username: []const u8,
password: []const u8,
email: ?[]const u8,
};
pub const ApiServer = struct {
prng: std.rand.DefaultPrng,
db: db.Database,
internal_alloc: std.mem.Allocator,
pub fn init(_: std.mem.Allocator) !ApiServer {
pub fn init(alloc: std.mem.Allocator) !ApiServer {
return ApiServer{
.prng = std.rand.DefaultPrng.init(@bitCast(u64, std.time.milliTimestamp())),
.db = try db.Database.init(),
.internal_alloc = alloc,
};
}
pub fn makeApiContext(self: *ApiServer, token: []const u8, alloc: std.mem.Allocator) !ApiContext {
if (token.len == 0) return error.InvalidToken;
const user_handle = token;
const username = token;
const user = (try self.db.getBy(models.User, .handle, user_handle, alloc)) orelse return error.InvalidToken;
const user = (try self.db.getBy(models.LocalUser, .username, username, alloc)) orelse return error.InvalidToken;
return ApiContext{
.user_context = .{
@ -100,11 +113,10 @@ pub const ApiServer = struct {
pub fn createNoteUser(self: *ApiServer, info: NoteCreate, ctx: ApiContext) !models.Note {
const id = Uuid.randV4(self.prng.random());
// TODO: check for dupes
std.debug.print("user {s} making a note\n", .{ctx.user_context.user.handle});
const note = models.Note{
.id = id,
.author_id = ctx.user_context.user.id,
.author_id = ctx.user_context.user.actor_id.?,
.content = info.content,
.created_at = DateTime.now(),
@ -114,33 +126,72 @@ pub const ApiServer = struct {
return note;
}
pub fn createUser(self: *ApiServer, info: CreateInfo(models.User)) !models.User {
pub fn register(self: *ApiServer, info: RegistrationInfo) !models.Actor {
const id = Uuid.randV4(self.prng.random());
// TODO: transaction?
if (try self.db.existsWhereEq(models.User, .handle, info.handle)) {
return error.HandleNotAvailable;
if (try self.db.existsWhereEq(models.LocalUser, .username, info.username)) {
return error.UsernameUnavailable;
}
const user = reify(models.User, id, info);
try self.db.insert(models.User, user);
if (try self.db.existsWhereEq(models.Actor, .handle, info.username)) {
return error.InconsistentDb;
}
return user;
// 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 now = DateTime.now();
const actor = models.Actor{
.id = id,
.handle = info.username,
.created_at = now,
};
const user = models.LocalUser{
.actor_id = id,
.username = info.username,
.email = info.email,
.hashed_password = hash,
.password_changed_at = now,
.created_at = now,
};
try self.db.insert(models.Actor, actor);
try self.db.insert(models.LocalUser, user);
// TODO: return token instead
return actor;
}
pub fn login(self: *ApiServer, username: []const u8, password: []const u8, alloc: std.mem.Allocator) !?models.Actor {
// 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, alloc)) orelse return error.InvalidLogin;
defer free(alloc, user_info);
const Hash = std.crypto.pwhash.scrypt;
Hash.strVerify(user_info.hashed_password, password, .{ .allocator = alloc }) catch |err| switch (err) {
error.PasswordVerificationFailed => return error.InvalidLogin,
else => return err,
};
// TODO: return token instead
return (try self.db.getBy(models.Actor, .id, user_info.actor_id.?, alloc)) orelse unreachable;
}
pub fn getNote(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.Note {
return self.db.getBy(models.Note, .id, id, alloc);
}
pub fn getUser(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.User {
return self.db.getBy(models.User, .id, id, alloc);
pub fn getActor(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.Actor {
return self.db.getBy(models.Actor, .id, id, alloc);
}
pub fn getUserByHandle(self: *ApiServer, handle: []const u8, alloc: std.mem.Allocator) !?models.User {
return self.db.getBy(models.User, .handle, handle, alloc);
pub fn getActorByHandle(self: *ApiServer, handle: []const u8, alloc: std.mem.Allocator) !?models.Actor {
return self.db.getBy(models.Actor, .handle, handle, alloc);
}
pub fn react(self: *ApiServer, note_id: Uuid, ctx: ApiContext) !void {
try self.db.insert(models.Reaction, .{ .note_id = note_id, .reactor_id = ctx.user_context.user.id });
try self.db.insert(models.Reaction, .{ .note_id = note_id, .reactor_id = ctx.user_context.user.actor_id.? });
}
pub fn listReacts(self: *ApiServer, note_id: Uuid, ctx: ApiContext) ![]models.Reaction {

View file

@ -76,17 +76,12 @@ pub fn createNote(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs)
try utils.respondJson(ctx, .created, note);
}
pub fn createUser(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const credentials = try utils.parseRequestBody(struct { handle: []const u8, password: []const u8 }, ctx);
defer utils.freeRequestBody(credentials, ctx.alloc);
pub fn register(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const info = try utils.parseRequestBody(api.RegistrationInfo, ctx);
defer utils.freeRequestBody(info, ctx.alloc);
var hash_buf: [128]u8 = undefined;
const Hash = std.crypto.pwhash.scrypt;
const hashed_password = try Hash.strHash(credentials.password, .{ .allocator = ctx.alloc, .params = Hash.Params.interactive, .encoding = .phc }, &hash_buf);
const user = srv.api.createUser(.{ .handle = credentials.handle, .hashed_password = hashed_password }) catch |err| switch (err) {
error.HandleNotAvailable => return try utils.respondError(ctx, .bad_request, "handle not available"),
const user = srv.api.register(info) catch |err| switch (err) {
error.UsernameUnavailable => return try utils.respondError(ctx, .bad_request, "Username Unavailable"),
else => return err,
};
@ -105,7 +100,7 @@ pub fn getNote(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs)
pub fn getUser(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void {
const id_str = args.get("id") orelse return error.NotFound;
const id = Uuid.parse(id_str) catch return utils.respondError(ctx, .bad_request, "Invalid UUID");
const user = (try srv.api.getUser(id, ctx.alloc)) orelse return utils.respondError(ctx, .not_found, "Note not found");
const user = (try srv.api.getActor(id, ctx.alloc)) orelse return utils.respondError(ctx, .not_found, "Note not found");
defer api.free(ctx.alloc, user);
try utils.respondJson(ctx, .ok, user);
@ -144,16 +139,13 @@ pub fn login(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void
const credentials = try utils.parseRequestBody(struct { username: []const u8, password: []const u8 }, ctx);
defer utils.freeRequestBody(credentials, ctx.alloc);
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
const user = (try srv.api.getUserByHandle(credentials.username, ctx.alloc)) orelse return utils.respondError(ctx, .bad_request, "Invalid Login");
const Hash = std.crypto.pwhash.scrypt;
Hash.strVerify(user.hashed_password, credentials.password, .{ .allocator = ctx.alloc }) catch |err| switch (err) {
const actor = srv.api.login(credentials.username, credentials.password, ctx.alloc) catch |err| switch (err) {
error.PasswordVerificationFailed => return utils.respondError(ctx, .bad_request, "Invalid Login"),
else => return err,
};
defer api.free(ctx.alloc, actor);
try utils.respondJson(ctx, .ok, .{ .id = user.id });
try utils.respondJson(ctx, .ok, actor);
}
pub fn healthcheck(_: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {

View file

@ -9,8 +9,9 @@ const comptimePrint = std.fmt.comptimePrint;
fn tableName(comptime T: type) String {
return switch (T) {
models.Note => "note",
models.User => "user",
models.Actor => "actor",
models.Reaction => "reaction",
models.LocalUser => "local_user",
else => unreachable,
};
}
@ -102,11 +103,28 @@ pub const Database = struct {
const init_sql_stmts = [_][]const u8{
\\CREATE TABLE IF NOT EXISTS
\\user(
\\ id TEXT PRIMARY KEY,
\\actor(
\\ id TEXT,
\\ handle TEXT NOT NULL,
\\ created_at INTEGER NOT NULL,
\\
\\ hashed_password TEXT NOT NULL
\\ PRIMARY KEY (id)
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\local_user(
\\ username TEXT NOT NULL,
\\ actor_id TEXT,
\\ email TEXT,
\\ hashed_password TEXT NOT NULL,
\\
\\ created_at INTEGER NOT NULL,
\\ password_changed_at INTEGER NOT NULL,
\\
\\ UNIQUE(actor_id),
\\ FOREIGN KEY (actor_id) REFERENCES actor(id),
\\
\\ PRIMARY KEY (username)
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
@ -116,7 +134,7 @@ pub const Database = struct {
\\ author_id TEXT NOT NULL,
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\
\\ FOREIGN KEY (author_id) REFERENCES user(id)
\\ FOREIGN KEY (author_id) REFERENCES actor(id)
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
@ -124,7 +142,7 @@ pub const Database = struct {
\\ reactor_id TEXT NOT NULL,
\\ note_id TEXT NOT NULL,
\\
\\ FOREIGN KEY(reactor_id) REFERENCES user(id),
\\ FOREIGN KEY(reactor_id) REFERENCES actor(id),
\\ FOREIGN KEY(note_id) REFERENCES note(id),
\\
\\ PRIMARY KEY(reactor_id, note_id)

View file

@ -24,7 +24,7 @@ const router = Router{
Route.new(.GET, "/notes/:id/reacts", c.listReacts),
Route.new(.POST, "/notes/:id/reacts", c.react),
Route.new(.POST, "/users", c.createUser),
Route.new(.POST, "/auth/register", c.register),
Route.new(.GET, "/users/:id", c.getUser),
},
};

View file

@ -12,11 +12,23 @@ pub const Note = struct {
created_at: DateTime,
};
pub const User = struct {
pub const Actor = struct {
id: Uuid,
handle: []const u8,
created_at: DateTime,
};
pub const LocalUser = struct {
actor_id: ?Uuid,
username: []const u8,
email: ?[]const u8,
hashed_password: []const u8, // encoded in PHC format, with salt
password_changed_at: DateTime,
created_at: DateTime,
};
pub const Reaction = struct {