Split users into actors and localusers
This commit is contained in:
parent
a4f6fca675
commit
ee0db68c5e
5 changed files with 115 additions and 42 deletions
|
@ -7,6 +7,11 @@ pub const models = @import("./models.zig");
|
||||||
pub const DateTime = util.DateTime;
|
pub const DateTime = util.DateTime;
|
||||||
pub const Uuid = util.Uuid;
|
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
|
// Frees an api struct and its fields allocated from alloc
|
||||||
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
||||||
switch (@typeInfo(@TypeOf(val))) {
|
switch (@typeInfo(@TypeOf(val))) {
|
||||||
|
@ -60,7 +65,7 @@ fn reify(comptime T: type, id: Uuid, val: CreateInfo(T)) T {
|
||||||
|
|
||||||
pub const ApiContext = struct {
|
pub const ApiContext = struct {
|
||||||
user_context: struct {
|
user_context: struct {
|
||||||
user: models.User,
|
user: models.LocalUser,
|
||||||
},
|
},
|
||||||
|
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
|
@ -70,23 +75,31 @@ pub const NoteCreate = struct {
|
||||||
content: []const u8,
|
content: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const RegistrationInfo = struct {
|
||||||
|
username: []const u8,
|
||||||
|
password: []const u8,
|
||||||
|
email: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
pub const ApiServer = struct {
|
pub const ApiServer = struct {
|
||||||
prng: std.rand.DefaultPrng,
|
prng: std.rand.DefaultPrng,
|
||||||
db: db.Database,
|
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{
|
return ApiServer{
|
||||||
.prng = std.rand.DefaultPrng.init(@bitCast(u64, std.time.milliTimestamp())),
|
.prng = std.rand.DefaultPrng.init(@bitCast(u64, std.time.milliTimestamp())),
|
||||||
.db = try db.Database.init(),
|
.db = try db.Database.init(),
|
||||||
|
.internal_alloc = alloc,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn makeApiContext(self: *ApiServer, token: []const u8, alloc: std.mem.Allocator) !ApiContext {
|
pub fn makeApiContext(self: *ApiServer, token: []const u8, alloc: std.mem.Allocator) !ApiContext {
|
||||||
if (token.len == 0) return error.InvalidToken;
|
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{
|
return ApiContext{
|
||||||
.user_context = .{
|
.user_context = .{
|
||||||
|
@ -100,11 +113,10 @@ pub const ApiServer = struct {
|
||||||
pub fn createNoteUser(self: *ApiServer, info: NoteCreate, ctx: ApiContext) !models.Note {
|
pub fn createNoteUser(self: *ApiServer, info: NoteCreate, ctx: ApiContext) !models.Note {
|
||||||
const id = Uuid.randV4(self.prng.random());
|
const id = Uuid.randV4(self.prng.random());
|
||||||
// TODO: check for dupes
|
// TODO: check for dupes
|
||||||
std.debug.print("user {s} making a note\n", .{ctx.user_context.user.handle});
|
|
||||||
|
|
||||||
const note = models.Note{
|
const note = models.Note{
|
||||||
.id = id,
|
.id = id,
|
||||||
.author_id = ctx.user_context.user.id,
|
.author_id = ctx.user_context.user.actor_id.?,
|
||||||
.content = info.content,
|
.content = info.content,
|
||||||
|
|
||||||
.created_at = DateTime.now(),
|
.created_at = DateTime.now(),
|
||||||
|
@ -114,33 +126,72 @@ pub const ApiServer = struct {
|
||||||
return note;
|
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());
|
const id = Uuid.randV4(self.prng.random());
|
||||||
|
// TODO: transaction?
|
||||||
|
|
||||||
if (try self.db.existsWhereEq(models.User, .handle, info.handle)) {
|
if (try self.db.existsWhereEq(models.LocalUser, .username, info.username)) {
|
||||||
return error.HandleNotAvailable;
|
return error.UsernameUnavailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = reify(models.User, id, info);
|
if (try self.db.existsWhereEq(models.Actor, .handle, info.username)) {
|
||||||
try self.db.insert(models.User, user);
|
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 {
|
pub fn getNote(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.Note {
|
||||||
return self.db.getBy(models.Note, .id, id, alloc);
|
return self.db.getBy(models.Note, .id, id, alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getUser(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.User {
|
pub fn getActor(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.Actor {
|
||||||
return self.db.getBy(models.User, .id, id, alloc);
|
return self.db.getBy(models.Actor, .id, id, alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getUserByHandle(self: *ApiServer, handle: []const u8, alloc: std.mem.Allocator) !?models.User {
|
pub fn getActorByHandle(self: *ApiServer, handle: []const u8, alloc: std.mem.Allocator) !?models.Actor {
|
||||||
return self.db.getBy(models.User, .handle, handle, alloc);
|
return self.db.getBy(models.Actor, .handle, handle, alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn react(self: *ApiServer, note_id: Uuid, ctx: ApiContext) !void {
|
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 {
|
pub fn listReacts(self: *ApiServer, note_id: Uuid, ctx: ApiContext) ![]models.Reaction {
|
||||||
|
|
|
@ -76,17 +76,12 @@ pub fn createNote(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs)
|
||||||
try utils.respondJson(ctx, .created, note);
|
try utils.respondJson(ctx, .created, note);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createUser(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
pub fn register(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||||
const credentials = try utils.parseRequestBody(struct { handle: []const u8, password: []const u8 }, ctx);
|
const info = try utils.parseRequestBody(api.RegistrationInfo, ctx);
|
||||||
defer utils.freeRequestBody(credentials, ctx.alloc);
|
defer utils.freeRequestBody(info, ctx.alloc);
|
||||||
|
|
||||||
var hash_buf: [128]u8 = undefined;
|
const user = srv.api.register(info) catch |err| switch (err) {
|
||||||
|
error.UsernameUnavailable => return try utils.respondError(ctx, .bad_request, "Username Unavailable"),
|
||||||
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"),
|
|
||||||
else => return err,
|
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 {
|
pub fn getUser(srv: *RequestServer, ctx: *http.server.Context, args: RouteArgs) !void {
|
||||||
const id_str = args.get("id") orelse return error.NotFound;
|
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 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);
|
defer api.free(ctx.alloc, user);
|
||||||
|
|
||||||
try utils.respondJson(ctx, .ok, 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);
|
const credentials = try utils.parseRequestBody(struct { username: []const u8, password: []const u8 }, ctx);
|
||||||
defer utils.freeRequestBody(credentials, ctx.alloc);
|
defer utils.freeRequestBody(credentials, ctx.alloc);
|
||||||
|
|
||||||
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
|
const actor = srv.api.login(credentials.username, credentials.password, ctx.alloc) catch |err| switch (err) {
|
||||||
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) {
|
|
||||||
error.PasswordVerificationFailed => return utils.respondError(ctx, .bad_request, "Invalid Login"),
|
error.PasswordVerificationFailed => return utils.respondError(ctx, .bad_request, "Invalid Login"),
|
||||||
else => return err,
|
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 {
|
pub fn healthcheck(_: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||||
|
|
|
@ -9,8 +9,9 @@ const comptimePrint = std.fmt.comptimePrint;
|
||||||
fn tableName(comptime T: type) String {
|
fn tableName(comptime T: type) String {
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
models.Note => "note",
|
models.Note => "note",
|
||||||
models.User => "user",
|
models.Actor => "actor",
|
||||||
models.Reaction => "reaction",
|
models.Reaction => "reaction",
|
||||||
|
models.LocalUser => "local_user",
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -102,11 +103,28 @@ pub const Database = struct {
|
||||||
|
|
||||||
const init_sql_stmts = [_][]const u8{
|
const init_sql_stmts = [_][]const u8{
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
\\user(
|
\\actor(
|
||||||
\\ id TEXT PRIMARY KEY,
|
\\ id TEXT,
|
||||||
\\ handle TEXT NOT NULL,
|
\\ 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;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
|
@ -116,7 +134,7 @@ pub const Database = struct {
|
||||||
\\ author_id TEXT NOT NULL,
|
\\ author_id TEXT NOT NULL,
|
||||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
\\
|
\\
|
||||||
\\ FOREIGN KEY (author_id) REFERENCES user(id)
|
\\ FOREIGN KEY (author_id) REFERENCES actor(id)
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
|
@ -124,7 +142,7 @@ pub const Database = struct {
|
||||||
\\ reactor_id TEXT NOT NULL,
|
\\ reactor_id TEXT NOT NULL,
|
||||||
\\ note_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),
|
\\ FOREIGN KEY(note_id) REFERENCES note(id),
|
||||||
\\
|
\\
|
||||||
\\ PRIMARY KEY(reactor_id, note_id)
|
\\ PRIMARY KEY(reactor_id, note_id)
|
||||||
|
|
|
@ -24,7 +24,7 @@ const router = Router{
|
||||||
Route.new(.GET, "/notes/:id/reacts", c.listReacts),
|
Route.new(.GET, "/notes/:id/reacts", c.listReacts),
|
||||||
Route.new(.POST, "/notes/:id/reacts", c.react),
|
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),
|
Route.new(.GET, "/users/:id", c.getUser),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,11 +12,23 @@ pub const Note = struct {
|
||||||
created_at: DateTime,
|
created_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const User = struct {
|
pub const Actor = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
handle: []const u8,
|
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
|
hashed_password: []const u8, // encoded in PHC format, with salt
|
||||||
|
password_changed_at: DateTime,
|
||||||
|
|
||||||
|
created_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Reaction = struct {
|
pub const Reaction = struct {
|
||||||
|
|
Loading…
Reference in a new issue