This commit is contained in:
jaina heartles 2022-09-08 00:52:23 -07:00
parent 8222d651ad
commit ca123c1cc0
5 changed files with 169 additions and 59 deletions

View File

@ -8,22 +8,12 @@ pub const DateTime = util.DateTime;
pub const Uuid = util.Uuid;
const Config = @import("./main.zig").Config;
const PwHash = std.crypto.pwhash.scrypt;
const pw_hash_params = PwHash.Params.interactive;
const pw_hash_encoding = .phc;
const pw_hash_buf_size = 128;
const token_len = 20;
const token_str_len = std.base64.standard.Encoder.calcSize(token_len);
const invite_code_len = 16;
const invite_code_str_len = std.base64.url_safe.Encoder.calcSize(invite_code_len);
const services = struct {
const communities = @import("./api/communities.zig");
const users = @import("./api/users.zig");
const auth = @import("./api/auth.zig");
const invites = @import("./api/invites.zig");
const notes = @import("./api/notes.zig");
};
pub const RegistrationRequest = struct {
@ -44,6 +34,33 @@ pub const InviteRequest = struct {
to_community: ?[]const u8 = null, // only valid on admin community
};
pub const LoginResponse = struct {
token: services.auth.tokens.Token.Value,
user_id: Uuid,
issued_at: DateTime,
};
pub const UserResponse = struct {
id: Uuid,
username: []const u8,
host: []const u8,
created_at: DateTime,
};
pub const NoteResponse = struct {
id: Uuid,
author: struct {
id: Uuid,
username: []const u8,
host: []const u8,
},
content: []const u8,
created_at: DateTime,
};
// Frees an api struct and its fields allocated from alloc
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
switch (@typeInfo(@TypeOf(val))) {
@ -66,22 +83,6 @@ pub fn free(alloc: std.mem.Allocator, val: anytype) void {
}
}
pub fn firstIndexOf(str: []const u8, ch: u8) ?usize {
for (str) |c, i| {
if (c == ch) return i;
}
return null;
}
pub const Scheme = models.Community.Scheme;
pub const LoginResult = struct {
user_id: Uuid,
token: [token_str_len]u8,
issued_at: DateTime,
};
threadlocal var prng: std.rand.DefaultPrng = undefined;
pub fn initThreadPrng(seed: u64) void {
@ -92,11 +93,6 @@ pub fn getRandom() std.rand.Random {
return prng.random();
}
// Returned slice points into buf
fn hashPassword(password: []const u8, alloc: std.mem.Allocator, buf: *[pw_hash_buf_size]u8) ![]const u8 {
return PwHash.strHash(password, .{ .allocator = alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, buf);
}
pub const ApiSource = struct {
db: db.Database,
internal_alloc: std.mem.Allocator,
@ -193,13 +189,13 @@ fn ApiConn(comptime DbConn: type) type {
return self.user_id != null and self.community_id == null;
}
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResult {
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResponse {
const user_id = (try services.users.lookupByUsername(&self.db, username, self.community_id)) orelse return error.InvalidLogin;
try services.auth.passwords.verify(&self.db, user_id, password, self.internal_alloc);
const token = try services.auth.tokens.create(&self.db, user_id);
return LoginResult{
return LoginResponse{
.user_id = user_id,
.token = token.value,
.issued_at = token.info.issued_at,
@ -235,7 +231,7 @@ fn ApiConn(comptime DbConn: type) type {
pub fn createInvite(self: *Self, options: InviteRequest) !services.invites.Invite {
// Only logged in users can make invites
const user_id = self.user_id orelse return error.PermissionDenied;
const user_id = self.user_id orelse return error.TokenRequired;
const community_id = if (options.to_community) |host| blk: {
// You can only specify a different community if you're on the admin domain
@ -258,7 +254,7 @@ fn ApiConn(comptime DbConn: type) type {
}, self.arena.allocator());
}
pub fn register(self: *Self, request: RegistrationRequest) !services.users.User {
pub fn register(self: *Self, request: RegistrationRequest) !UserResponse {
std.log.debug("registering user {s} with code {s}", .{ request.username, request.invite_code });
const invite = try services.invites.getByCode(&self.db, request.invite_code, self.arena.allocator());
@ -278,10 +274,58 @@ fn ApiConn(comptime DbConn: type) type {
},
}
return services.users.get(&self.db, user_id, self.arena.allocator()) catch |err| switch (err) {
return self.getUser(user_id) catch |err| switch (err) {
error.NotFound => error.Unexpected,
else => err,
};
}
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
const user = try services.users.get(&self.db, user_id, self.arena.allocator());
if (self.user_id == null) {
if (!Uuid.eql(self.community_id, user.community_id)) return error.NotFound;
}
return UserResponse{
.id = user.id,
.username = user.username,
.host = user.host,
.created_at = user.created_at,
};
}
pub fn createNote(self: *Self, content: []const u8) !NoteResponse {
if (self.community_id == null) return error.WrongCommunity;
const user_id = self.user_id orelse return error.TokenRequired;
const note_id = try services.notes.create(&self.db, user_id, content);
return self.getNote(note_id) catch |err| switch (err) {
error.NotFound => error.Unexpected,
else => err,
};
}
pub fn getNote(self: *Self, note_id: Uuid) !NoteResponse {
const note = try services.notes.get(&self.db, note_id, self.arena.allocator());
const user = try services.users.get(&self.db, note.author_id, self.arena.allocator());
// Only serve community-specific notes on unauthenticated requests
if (self.user_id == null) {
if (!Uuid.eql(self.community_id, user.community_id)) return error.NotFound;
}
return NoteResponse{
.id = note.id,
.author = .{
.id = user.id,
.username = user.username,
.host = user.host,
},
.content = note.content,
.created_at = note.created_at,
};
}
};
}

60
src/main/api/notes.zig Normal file
View File

@ -0,0 +1,60 @@
const std = @import("std");
const util = @import("util");
const auth = @import("./auth.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const getRandom = @import("../api.zig").getRandom;
pub const Note = struct {
id: Uuid,
author_id: Uuid,
content: []const u8,
created_at: DateTime,
};
const DbModel = struct {
id: Uuid,
author_id: Uuid,
content: []const u8,
created_at: DateTime,
};
pub fn create(
db: anytype,
author: Uuid,
content: []const u8,
) !Uuid {
const id = Uuid.randV4(getRandom());
try db.insert("note", .{
.id = id,
.author_id = author,
.content = content,
.created_at = DateTime.now(),
});
return id;
}
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !Note {
const result = (try db.execRow(
&.{ Uuid, []const u8, DateTime },
\\SELECT author_id, content, created_at
\\FROM note
\\WHERE id = ?
\\LIMIT 1
,
.{id},
alloc,
)) orelse return error.NotFound;
return Note{
.id = id,
.author_id = result[0],
.content = result[1],
.created_at = result[2],
};
}

View File

@ -9,6 +9,7 @@ pub const auth = @import("./controllers/auth.zig");
pub const communities = @import("./controllers/communities.zig");
pub const invites = @import("./controllers/invites.zig");
pub const users = @import("./controllers/users.zig");
pub const notes = @import("./controllers/notes.zig");
pub const utils = struct {
const json_options = if (builtin.mode == .Debug) .{

View File

@ -8,27 +8,35 @@ const NoteCreateInfo = @import("../api.zig").NoteCreateInfo;
const RequestServer = root.RequestServer;
const RouteArgs = http.RouteArgs;
pub const reacts = @import("./notes/reacts.zig");
//pub const reacts = @import("./notes/reacts.zig");
pub fn create(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const info = try utils.parseRequestBody(NoteCreateInfo, ctx);
defer utils.freeRequestBody(info, ctx.alloc);
pub const create = struct {
pub const method = .POST;
pub const path = "/notes";
pub fn handler(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
const info = try utils.parseRequestBody(struct { content: []const u8 }, ctx);
defer utils.freeRequestBody(info, ctx.alloc);
var api = try utils.getApiConn(srv, ctx);
defer api.close();
var api = try utils.getApiConn(srv, ctx);
defer api.close();
const note = try api.createNote(info);
const note = try api.createNote(info.content);
try utils.respondJson(ctx, .created, note);
}
try utils.respondJson(ctx, .created, note);
}
};
pub fn get(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");
var api = try utils.getApiConn(srv, ctx);
defer api.close();
pub const get = struct {
pub const method = .GET;
pub const path = "/notes/:id";
pub fn handler(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");
var api = try utils.getApiConn(srv, ctx);
defer api.close();
const note = (try api.getNote(id)) orelse return utils.respondError(ctx, .not_found, "Note not found");
const note = try api.getNote(id);
try utils.respondJson(ctx, .ok, note);
}
try utils.respondJson(ctx, .ok, note);
}
};

View File

@ -16,9 +16,6 @@ const router = Router{
.routes = &[_]Route{
Route.new(.GET, "/healthcheck", &c.healthcheck),
//Route.new(.POST, "/users", c.users.create),
//Route.new(.POST, "/auth/register", &c.auth.register),
prepare(c.auth.login),
prepare(c.auth.verify_login),
@ -28,8 +25,8 @@ const router = Router{
prepare(c.users.create),
//Route.new(.POST, "/notes", &c.notes.create),
//Route.new(.GET, "/notes/:id", &c.notes.get),
prepare(c.notes.create),
prepare(c.notes.get),
//Route.new(.GET, "/notes/:id/reacts", &c.notes.reacts.list),
//Route.new(.POST, "/notes/:id/reacts", &c.notes.reacts.create),