Notes
This commit is contained in:
parent
8222d651ad
commit
ca123c1cc0
5 changed files with 169 additions and 59 deletions
118
src/main/api.zig
118
src/main/api.zig
|
@ -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
60
src/main/api/notes.zig
Normal 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],
|
||||
};
|
||||
}
|
|
@ -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) .{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue