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 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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue