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;
|
pub const Uuid = util.Uuid;
|
||||||
const Config = @import("./main.zig").Config;
|
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 services = struct {
|
||||||
const communities = @import("./api/communities.zig");
|
const communities = @import("./api/communities.zig");
|
||||||
const users = @import("./api/users.zig");
|
const users = @import("./api/users.zig");
|
||||||
const auth = @import("./api/auth.zig");
|
const auth = @import("./api/auth.zig");
|
||||||
const invites = @import("./api/invites.zig");
|
const invites = @import("./api/invites.zig");
|
||||||
|
const notes = @import("./api/notes.zig");
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const RegistrationRequest = struct {
|
pub const RegistrationRequest = struct {
|
||||||
|
@ -44,6 +34,33 @@ pub const InviteRequest = struct {
|
||||||
to_community: ?[]const u8 = null, // only valid on admin community
|
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
|
// 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))) {
|
||||||
|
@ -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;
|
threadlocal var prng: std.rand.DefaultPrng = undefined;
|
||||||
|
|
||||||
pub fn initThreadPrng(seed: u64) void {
|
pub fn initThreadPrng(seed: u64) void {
|
||||||
|
@ -92,11 +93,6 @@ pub fn getRandom() std.rand.Random {
|
||||||
return prng.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 {
|
pub const ApiSource = struct {
|
||||||
db: db.Database,
|
db: db.Database,
|
||||||
internal_alloc: std.mem.Allocator,
|
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;
|
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;
|
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);
|
try services.auth.passwords.verify(&self.db, user_id, password, self.internal_alloc);
|
||||||
|
|
||||||
const token = try services.auth.tokens.create(&self.db, user_id);
|
const token = try services.auth.tokens.create(&self.db, user_id);
|
||||||
|
|
||||||
return LoginResult{
|
return LoginResponse{
|
||||||
.user_id = user_id,
|
.user_id = user_id,
|
||||||
.token = token.value,
|
.token = token.value,
|
||||||
.issued_at = token.info.issued_at,
|
.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 {
|
pub fn createInvite(self: *Self, options: InviteRequest) !services.invites.Invite {
|
||||||
// Only logged in users can make invites
|
// 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: {
|
const community_id = if (options.to_community) |host| blk: {
|
||||||
// You can only specify a different community if you're on the admin domain
|
// 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());
|
}, 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 });
|
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());
|
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,
|
error.NotFound => error.Unexpected,
|
||||||
else => err,
|
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 communities = @import("./controllers/communities.zig");
|
||||||
pub const invites = @import("./controllers/invites.zig");
|
pub const invites = @import("./controllers/invites.zig");
|
||||||
pub const users = @import("./controllers/users.zig");
|
pub const users = @import("./controllers/users.zig");
|
||||||
|
pub const notes = @import("./controllers/notes.zig");
|
||||||
|
|
||||||
pub const utils = struct {
|
pub const utils = struct {
|
||||||
const json_options = if (builtin.mode == .Debug) .{
|
const json_options = if (builtin.mode == .Debug) .{
|
||||||
|
|
|
@ -8,27 +8,35 @@ const NoteCreateInfo = @import("../api.zig").NoteCreateInfo;
|
||||||
const RequestServer = root.RequestServer;
|
const RequestServer = root.RequestServer;
|
||||||
const RouteArgs = http.RouteArgs;
|
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 {
|
pub const create = struct {
|
||||||
const info = try utils.parseRequestBody(NoteCreateInfo, ctx);
|
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);
|
defer utils.freeRequestBody(info, ctx.alloc);
|
||||||
|
|
||||||
var api = try utils.getApiConn(srv, ctx);
|
var api = try utils.getApiConn(srv, ctx);
|
||||||
defer api.close();
|
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 {
|
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_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");
|
||||||
var api = try utils.getApiConn(srv, ctx);
|
var api = try utils.getApiConn(srv, ctx);
|
||||||
defer api.close();
|
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{
|
.routes = &[_]Route{
|
||||||
Route.new(.GET, "/healthcheck", &c.healthcheck),
|
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.login),
|
||||||
prepare(c.auth.verify_login),
|
prepare(c.auth.verify_login),
|
||||||
|
|
||||||
|
@ -28,8 +25,8 @@ const router = Router{
|
||||||
|
|
||||||
prepare(c.users.create),
|
prepare(c.users.create),
|
||||||
|
|
||||||
//Route.new(.POST, "/notes", &c.notes.create),
|
prepare(c.notes.create),
|
||||||
//Route.new(.GET, "/notes/:id", &c.notes.get),
|
prepare(c.notes.get),
|
||||||
|
|
||||||
//Route.new(.GET, "/notes/:id/reacts", &c.notes.reacts.list),
|
//Route.new(.GET, "/notes/:id/reacts", &c.notes.reacts.list),
|
||||||
//Route.new(.POST, "/notes/:id/reacts", &c.notes.reacts.create),
|
//Route.new(.POST, "/notes/:id/reacts", &c.notes.reacts.create),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue