messing w/ stuff
This commit is contained in:
parent
913a84c4fa
commit
ab96b3b734
13 changed files with 584 additions and 324 deletions
11
src/main/README.md
Normal file
11
src/main/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# General overview
|
||||
|
||||
- `/controllers/**`
|
||||
Handles serialization/deserialization of api calls from HTTP requests
|
||||
- `/api.zig`
|
||||
Business rules
|
||||
- `/api/*.zig`
|
||||
Performs the actual actions in the DB associated with a call
|
||||
- `/db.zig`
|
||||
SQL query wrapper
|
||||
|
204
src/main/api.zig
204
src/main/api.zig
|
@ -19,6 +19,12 @@ 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");
|
||||
};
|
||||
|
||||
// Frees an api struct and its fields allocated from alloc
|
||||
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
||||
switch (@typeInfo(@TypeOf(val))) {
|
||||
|
@ -49,46 +55,8 @@ pub fn firstIndexOf(str: []const u8, ch: u8) ?usize {
|
|||
return null;
|
||||
}
|
||||
|
||||
pub fn CreateInfo(comptime T: type) type {
|
||||
const t_fields = std.meta.fields(T);
|
||||
var fields: [t_fields.len - 1]std.builtin.Type.StructField = undefined;
|
||||
var count = 0;
|
||||
|
||||
inline for (t_fields) |f| {
|
||||
if (std.mem.eql(u8, f.name, "id")) continue;
|
||||
|
||||
fields[count] = f;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return @Type(.{ .Struct = .{
|
||||
.layout = .Auto,
|
||||
.fields = &fields,
|
||||
.decls = &[0]std.builtin.Type.Declaration{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
}
|
||||
|
||||
fn reify(comptime T: type, id: Uuid, val: CreateInfo(T)) T {
|
||||
var result: T = undefined;
|
||||
result.id = id;
|
||||
inline for (std.meta.fields(CreateInfo(T))) |f| {
|
||||
@field(result, f.name) = @field(val, f.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pub const NoteCreateInfo = struct {
|
||||
content: []const u8,
|
||||
};
|
||||
|
||||
pub const Scheme = models.Community.Scheme;
|
||||
|
||||
pub const CommunityCreateOptions = struct {
|
||||
name: []const u8,
|
||||
host: []const u8,
|
||||
};
|
||||
|
||||
pub const RegistrationInfo = struct {
|
||||
username: []const u8,
|
||||
password: []const u8,
|
||||
|
@ -115,6 +83,10 @@ pub fn initThreadPrng(seed: u64) void {
|
|||
prng = std.rand.DefaultPrng.init(seed +% std.Thread.getCurrentId());
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -128,9 +100,7 @@ pub const ApiSource = struct {
|
|||
pub const Conn = ApiConn(db.Database);
|
||||
|
||||
const root_username = "root";
|
||||
const root_id = Uuid.nil;
|
||||
const root_password_envvar = "CLUSTER_ROOT_PASSWORD";
|
||||
const cluster_community_id = Uuid.nil;
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, cfg: Config) !ApiSource {
|
||||
var self = ApiSource{
|
||||
|
@ -139,89 +109,63 @@ pub const ApiSource = struct {
|
|||
.config = cfg,
|
||||
};
|
||||
|
||||
if ((try self.db.execRow2(&.{i64}, "SELECT 1 FROM user WHERE id = ? LIMIT 1;", .{root_id}, null)) == null) {
|
||||
if ((try services.users.lookupByUsername(&self.db, root_username, null)) == null) {
|
||||
std.log.info("No cluster root user detected. Creating...", .{});
|
||||
|
||||
const root_password = std.os.getenv(root_password_envvar) orelse {
|
||||
std.log.err(
|
||||
"No root user created and no password specified. Please provide the password for the root user by the ${s} environment variable for initial startup.",
|
||||
"No root user created and no password specified. Please provide the password for the root user by the ${s} environment variable for initial startup. This only needs to be done once",
|
||||
.{root_password_envvar},
|
||||
);
|
||||
@panic("No root password provided");
|
||||
};
|
||||
|
||||
var buf: [pw_hash_buf_size]u8 = undefined;
|
||||
const hash = try hashPassword(root_password, self.internal_alloc, &buf);
|
||||
|
||||
try self.db.insert2("community", .{
|
||||
.id = cluster_community_id,
|
||||
.name = "Cluster System Pseudocommunity",
|
||||
.host = cfg.cluster_host,
|
||||
.scheme = cfg.cluster_scheme,
|
||||
});
|
||||
try self.db.insert2("user", .{
|
||||
.id = root_id,
|
||||
.username = root_username,
|
||||
.community_id = cluster_community_id,
|
||||
});
|
||||
try self.db.insert2("local_user", .{
|
||||
.user_id = root_id,
|
||||
.hashed_password = hash,
|
||||
.invite_id = null,
|
||||
.email = null,
|
||||
});
|
||||
_ = try services.users.create(&self.db, root_username, root_password, null, .{}, alloc);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
|
||||
const community_id = (try self.db.execRow2(
|
||||
fn getCommunityFromHost(self: *ApiSource, host: []const u8) !?Uuid {
|
||||
if (try self.db.execRow2(
|
||||
&.{Uuid},
|
||||
"SELECT id FROM community WHERE host = ?",
|
||||
.{host},
|
||||
null,
|
||||
)) orelse return error.NoCommunity;
|
||||
)) |result| return result[0];
|
||||
|
||||
// Test for cluster admin community
|
||||
if (util.ciutf8.eql(self.config.cluster_host, host)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return error.NoCommunity;
|
||||
}
|
||||
|
||||
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
|
||||
const community_id = try self.getCommunityFromHost(host);
|
||||
|
||||
return Conn{
|
||||
.db = self.db,
|
||||
.internal_alloc = self.internal_alloc,
|
||||
.as_user = null,
|
||||
.on_community = community_id[0],
|
||||
.user_id = null,
|
||||
.community_id = community_id,
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn {
|
||||
var conn = try self.connectUnauthorized(host, alloc);
|
||||
errdefer conn.close();
|
||||
const community_id = try self.getCommunityFromHost(host);
|
||||
|
||||
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(token) catch return error.InvalidToken;
|
||||
if (decoded_len != token_len) return error.InvalidToken;
|
||||
const token_info = try services.auth.tokens.verify(&self.db, token, community_id);
|
||||
|
||||
var decoded: [token_len]u8 = undefined;
|
||||
std.base64.standard.Decoder.decode(&decoded, token) catch return error.InvalidToken;
|
||||
|
||||
var hash: models.ByteArray(models.Token.hash_len) = undefined;
|
||||
models.Token.HashFn.hash(&decoded, &hash.data, .{});
|
||||
|
||||
const token_result = (try self.db.execRow2(
|
||||
&.{Uuid},
|
||||
\\SELECT user.id
|
||||
\\FROM token
|
||||
\\ JOIN user ON token.user_id = user.id
|
||||
\\ JOIN community ON user.community_id = community.id
|
||||
\\ JOIN local_user ON local_user.user_id = user.id
|
||||
\\WHERE token.hash = ?
|
||||
\\LIMIT 1
|
||||
,
|
||||
.{hash},
|
||||
null,
|
||||
)) orelse return error.InvalidToken;
|
||||
|
||||
conn.as_user = token_result[0];
|
||||
|
||||
return conn;
|
||||
return Conn{
|
||||
.db = self.db,
|
||||
.internal_alloc = self.internal_alloc,
|
||||
.user_id = token_info.user_id,
|
||||
.community_id = community_id,
|
||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -231,8 +175,8 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
|
||||
db: DbConn,
|
||||
internal_alloc: std.mem.Allocator, // used *only* for large, internal buffers
|
||||
as_user: ?Uuid,
|
||||
on_community: Uuid,
|
||||
user_id: ?Uuid,
|
||||
community_id: ?Uuid,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
pub fn close(self: *Self) void {
|
||||
|
@ -240,7 +184,7 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
}
|
||||
|
||||
fn getAuthenticatedUser(self: *Self) !models.User {
|
||||
if (self.as_user) |id| {
|
||||
if (self.user_id) |id| {
|
||||
const user = try self.db.getBy(models.User, .id, id, self.arena.allocator());
|
||||
if (user == null) return error.NotAuthorized;
|
||||
|
||||
|
@ -251,7 +195,7 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
}
|
||||
|
||||
fn getAuthenticatedLocalUser(self: *Self) !models.LocalUser {
|
||||
if (self.as_user) |user_id| {
|
||||
if (self.user_id) |user_id| {
|
||||
const local_user = try self.db.getBy(models.LocalUser, .user_id, user_id, self.arena.allocator());
|
||||
if (local_user == null) return error.NotAuthorized;
|
||||
|
||||
|
@ -262,46 +206,21 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
}
|
||||
|
||||
fn getAuthenticatedActor(self: *Self) !models.Actor {
|
||||
return if (self.as_user) |user_id|
|
||||
return if (self.user_id) |user_id|
|
||||
(try self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator())) orelse error.NotAuthorized
|
||||
else
|
||||
error.NotAuthorized;
|
||||
}
|
||||
|
||||
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResult {
|
||||
// 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.User, .username, username, self.arena.allocator())) orelse return error.InvalidLogin;
|
||||
//const local_user_info = (try self.db.getBy(models.LocalUser, .user_id, user_info.id, self.arena.allocator())) 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);
|
||||
|
||||
const user_info = (try self.db.execRow2(
|
||||
&.{ Uuid, []const u8 },
|
||||
\\SELECT user.id, local_user.hashed_password
|
||||
\\FROM user JOIN local_user ON local_user.user_id = user.id
|
||||
\\WHERE user.username = ?
|
||||
,
|
||||
.{username},
|
||||
self.arena.allocator(),
|
||||
)) orelse return error.InvalidLogin;
|
||||
|
||||
const user_id = user_info[0];
|
||||
const hashed_password = user_info[1];
|
||||
|
||||
//defer free(self.arena.allocator(), user_info);
|
||||
|
||||
const Hash = std.crypto.pwhash.scrypt;
|
||||
Hash.strVerify(hashed_password, password, .{ .allocator = self.internal_alloc }) catch |err| switch (err) {
|
||||
error.PasswordVerificationFailed => return error.InvalidLogin,
|
||||
else => return err,
|
||||
};
|
||||
|
||||
const token = try self.createToken(user_id);
|
||||
|
||||
var token_enc: [token_str_len]u8 = undefined;
|
||||
_ = std.base64.standard.Encoder.encode(&token_enc, &token.value);
|
||||
const token = try services.auth.tokens.create(&self.db, user_id);
|
||||
|
||||
return LoginResult{
|
||||
.user_id = user_id,
|
||||
.token = token_enc,
|
||||
.token = token.value,
|
||||
.issued_at = token.info.issued_at,
|
||||
};
|
||||
}
|
||||
|
@ -310,7 +229,7 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
username: []const u8,
|
||||
};
|
||||
pub fn getTokenInfo(self: *Self) !TokenInfo {
|
||||
if (self.as_user) |user_id| {
|
||||
if (self.user_id) |user_id| {
|
||||
const result = (try self.db.execRow2(
|
||||
&.{[]const u8},
|
||||
"SELECT username FROM user WHERE id = ?",
|
||||
|
@ -325,29 +244,12 @@ fn ApiConn(comptime DbConn: type) type {
|
|||
return error.Unauthorized;
|
||||
}
|
||||
|
||||
const TokenResult = struct {
|
||||
info: models.Token,
|
||||
value: [token_len]u8,
|
||||
};
|
||||
fn createToken(self: *Self, user_id: Uuid) !TokenResult {
|
||||
var token: [token_len]u8 = undefined;
|
||||
std.crypto.random.bytes(&token);
|
||||
pub fn createCommunity(self: *Self, origin: []const u8) !services.communities.Community {
|
||||
if (self.community_id != null) {
|
||||
return error.NotAdminHost;
|
||||
}
|
||||
|
||||
var hash: [models.Token.hash_len]u8 = undefined;
|
||||
models.Token.HashFn.hash(&token, &hash, .{});
|
||||
|
||||
const db_token = models.Token{
|
||||
.id = Uuid.randV4(prng.random()),
|
||||
.hash = .{ .data = hash },
|
||||
.user_id = user_id,
|
||||
.issued_at = DateTime.now(),
|
||||
};
|
||||
|
||||
try self.db.insert2("token", db_token);
|
||||
return TokenResult{
|
||||
.info = db_token,
|
||||
.value = token,
|
||||
};
|
||||
return services.communities.create(&self.db, origin, null);
|
||||
}
|
||||
|
||||
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
|
||||
|
|
149
src/main/api/'
Normal file
149
src/main/api/'
Normal file
|
@ -0,0 +1,149 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
pub const passwords = struct {
|
||||
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 PwHashBuf = [pw_hash_buf_size]u8;
|
||||
|
||||
pub const Password = struct {
|
||||
user_id: Uuid,
|
||||
|
||||
hashed_password: []const u8,
|
||||
};
|
||||
|
||||
// Returned slice points into buf
|
||||
fn hashPassword(password: []const u8, alloc: std.mem.Allocator, buf: *PwHashBuf) []const u8 {
|
||||
return PwHash.strHash(password, .{ .allocator = alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, buf) catch unreachable;
|
||||
}
|
||||
|
||||
pub const VerifyError = error{
|
||||
InvalidLogin,
|
||||
DbError,
|
||||
};
|
||||
pub fn verify(db: anytype, user_id: Uuid, password: []const u8, alloc: std.mem.Allocator) VerifyError!void {
|
||||
const hash = (try db.execRow2(
|
||||
&.{PwHashBuf},
|
||||
"SELECT hashed_password FROM account_password WHERE user_id = ? LIMIT 1",
|
||||
.{user_id},
|
||||
null,
|
||||
)) orelse return error.PasswordNotFound;
|
||||
|
||||
try PwHash.strVerify(&hash[0], password, .{ .allocator = alloc });
|
||||
}
|
||||
|
||||
pub const CreateError = error{DbError};
|
||||
pub fn create(db: anytype, user_id: Uuid, password: []const u8, alloc: std.mem.Allocator) CreateError!void {
|
||||
var buf: PwHashBuf = undefined;
|
||||
const hash = PwHash.strHash(password, .{ .allocator = alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, &buf) catch unreachable;
|
||||
|
||||
try db.insert2("account_password", .{
|
||||
.user_id = user_id,
|
||||
.hashed_password = hash,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
pub const tokens = struct {
|
||||
const token_len = 20;
|
||||
pub const Token = struct {
|
||||
pub const Value = [token_len]u8;
|
||||
pub const Info = struct {
|
||||
user_id: Uuid,
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
value: Value,
|
||||
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
const TokenHash = std.crypto.hash.sha2.Sha256;
|
||||
|
||||
const DbToken = struct {
|
||||
hash: []const u8,
|
||||
user_id: Uuid,
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
pub const CreateError = error{DbError};
|
||||
pub fn create(db: anytype, user_id: Uuid) CreateError!Token {
|
||||
var token: [token_len]u8 = undefined;
|
||||
std.crypto.random.bytes(&token);
|
||||
|
||||
var hash: [TokenHash.digest_length]u8 = undefined;
|
||||
TokenHash.hash(&token, &hash, .{});
|
||||
|
||||
const issued_at = DateTime.now();
|
||||
|
||||
db.insert2("token", DbToken{
|
||||
.hash = &hash,
|
||||
.user_id = user_id,
|
||||
.issued_at = issued_at,
|
||||
}) catch return error.DbError;
|
||||
|
||||
return Token{
|
||||
.value = token,
|
||||
.issued_at = issued_at,
|
||||
};
|
||||
}
|
||||
|
||||
fn lookupUserTokenFromHash(db: anytype, hash: []const u8, community_id: Uuid) !?Uuid {
|
||||
return if (try db.execRow2(
|
||||
&.{ Uuid, DateTime },
|
||||
\\SELECT user.id, token.issued_at
|
||||
\\FROM token JOIN user ON token.user_id = user.id
|
||||
\\WHERE user.community_id = ? AND token.hash = ?
|
||||
\\LIMIT 1
|
||||
,
|
||||
.{ community_id, hash },
|
||||
null,
|
||||
)) |result|
|
||||
Token.Info{
|
||||
.user_id = result[0],
|
||||
.issued_at = result[1],
|
||||
}
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
fn lookupSystemTokenFromHash(db: anytype, hash: []const u8) !?Token.Info {
|
||||
return if (try db.execRow2(
|
||||
&.{ Uuid, DateTime },
|
||||
\\SELECT user.id, token.issued_at
|
||||
\\FROM token JOIN user ON token.user_id = user.id
|
||||
\\WHERE user.community_id IS NULL AND token.hash = ?
|
||||
\\LIMIT 1
|
||||
,
|
||||
.{hash},
|
||||
null,
|
||||
)) |result|
|
||||
Token.Info{
|
||||
.user_id = result[0],
|
||||
.issued_at = result[1],
|
||||
}
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
pub const VerifyError = error{ InvalidToken, DbError };
|
||||
pub fn verifyToken(db: anytype, token: []const u8, community_id: ?Uuid) VerifyError!Token.Info {
|
||||
var hash: [TokenHash.digest_length]u8 = undefined;
|
||||
TokenHash.hash(&token, &hash, .{});
|
||||
|
||||
const token_info = if (community_id) |id|
|
||||
lookupUserTokenFromHash(db, &hash, id) catch return error.DbError
|
||||
else
|
||||
lookupSystemTokenFromHash(db, &hash) catch return error.DbError;
|
||||
|
||||
if (token_info) |info| return info;
|
||||
|
||||
return error.InvalidToken;
|
||||
}
|
||||
};
|
155
src/main/api/auth.zig
Normal file
155
src/main/api/auth.zig
Normal file
|
@ -0,0 +1,155 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
pub const passwords = struct {
|
||||
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 PwHashBuf = [pw_hash_buf_size]u8;
|
||||
|
||||
// Returned slice points into buf
|
||||
fn hashPassword(password: []const u8, alloc: std.mem.Allocator, buf: *PwHashBuf) []const u8 {
|
||||
return PwHash.strHash(password, .{ .allocator = alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, buf) catch unreachable;
|
||||
}
|
||||
|
||||
pub const VerifyError = error{
|
||||
InvalidLogin,
|
||||
DbError,
|
||||
};
|
||||
pub fn verify(db: anytype, user_id: Uuid, password: []const u8, alloc: std.mem.Allocator) VerifyError!void {
|
||||
// TODO: This could be done w/o the dynamically allocated hash buf
|
||||
const hash = (db.execRow2(
|
||||
&.{[]const u8},
|
||||
"SELECT hashed_password FROM account_password WHERE user_id = ? LIMIT 1",
|
||||
.{user_id},
|
||||
alloc,
|
||||
) catch return error.DbError) orelse return error.InvalidLogin;
|
||||
errdefer alloc.free(hash[0]);
|
||||
|
||||
PwHash.strVerify(hash[0], password, .{ .allocator = alloc }) catch unreachable;
|
||||
}
|
||||
|
||||
pub const CreateError = error{DbError};
|
||||
pub fn create(db: anytype, user_id: Uuid, password: []const u8, alloc: std.mem.Allocator) CreateError!void {
|
||||
var buf: PwHashBuf = undefined;
|
||||
const hash = PwHash.strHash(password, .{ .allocator = alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, &buf) catch unreachable;
|
||||
|
||||
db.insert2("account_password", .{
|
||||
.user_id = user_id,
|
||||
.hashed_password = hash,
|
||||
}) catch return error.DbError;
|
||||
}
|
||||
};
|
||||
|
||||
pub const tokens = struct {
|
||||
const token_len = 20;
|
||||
const token_str_len = std.base64.standard.Encoder.calcSize(token_len);
|
||||
pub const Token = struct {
|
||||
pub const Value = [token_str_len]u8;
|
||||
pub const Info = struct {
|
||||
user_id: Uuid,
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
value: Value,
|
||||
info: Info,
|
||||
};
|
||||
|
||||
const TokenHash = std.crypto.hash.sha2.Sha256;
|
||||
const TokenDigestBuf = [TokenHash.digest_length]u8;
|
||||
|
||||
const DbToken = struct {
|
||||
hash: []const u8,
|
||||
user_id: Uuid,
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
pub const CreateError = error{DbError};
|
||||
pub fn create(db: anytype, user_id: Uuid) CreateError!Token {
|
||||
var token: [token_len]u8 = undefined;
|
||||
std.crypto.random.bytes(&token);
|
||||
|
||||
var hash: TokenDigestBuf = undefined;
|
||||
TokenHash.hash(&token, &hash, .{});
|
||||
|
||||
const issued_at = DateTime.now();
|
||||
|
||||
db.insert2("token", DbToken{
|
||||
.hash = &hash,
|
||||
.user_id = user_id,
|
||||
.issued_at = issued_at,
|
||||
}) catch return error.DbError;
|
||||
|
||||
var token_enc: [token_str_len]u8 = undefined;
|
||||
_ = std.base64.standard.Encoder.encode(&token_enc, &token);
|
||||
|
||||
return Token{ .value = token_enc, .info = .{
|
||||
.user_id = user_id,
|
||||
.issued_at = issued_at,
|
||||
} };
|
||||
}
|
||||
|
||||
fn lookupUserTokenFromHash(db: anytype, hash: []const u8, community_id: Uuid) !?Token.Info {
|
||||
return if (try db.execRow2(
|
||||
&.{ Uuid, DateTime },
|
||||
\\SELECT user.id, token.issued_at
|
||||
\\FROM token JOIN user ON token.user_id = user.id
|
||||
\\WHERE user.community_id = ? AND token.hash = ?
|
||||
\\LIMIT 1
|
||||
,
|
||||
.{ community_id, hash },
|
||||
null,
|
||||
)) |result|
|
||||
Token.Info{
|
||||
.user_id = result[0],
|
||||
.issued_at = result[1],
|
||||
}
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
fn lookupSystemTokenFromHash(db: anytype, hash: []const u8) !?Token.Info {
|
||||
return if (try db.execRow2(
|
||||
&.{ Uuid, DateTime },
|
||||
\\SELECT user.id, token.issued_at
|
||||
\\FROM token JOIN user ON token.user_id = user.id
|
||||
\\WHERE user.community_id IS NULL AND token.hash = ?
|
||||
\\LIMIT 1
|
||||
,
|
||||
.{hash},
|
||||
null,
|
||||
)) |result|
|
||||
Token.Info{
|
||||
.user_id = result[0],
|
||||
.issued_at = result[1],
|
||||
}
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
pub const VerifyError = error{ InvalidToken, DbError };
|
||||
pub fn verify(db: anytype, token: []const u8, community_id: ?Uuid) VerifyError!Token.Info {
|
||||
const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(token) catch return error.InvalidToken;
|
||||
if (decoded_len != token_len) return error.InvalidToken;
|
||||
|
||||
var decoded: [token_len]u8 = undefined;
|
||||
std.base64.standard.Decoder.decode(&decoded, token) catch return error.InvalidToken;
|
||||
|
||||
var hash: TokenDigestBuf = undefined;
|
||||
TokenHash.hash(&decoded, &hash, .{});
|
||||
|
||||
const token_info = if (community_id) |id|
|
||||
lookupUserTokenFromHash(db, &hash, id) catch return error.DbError
|
||||
else
|
||||
lookupSystemTokenFromHash(db, &hash) catch return error.DbError;
|
||||
|
||||
if (token_info) |info| return info;
|
||||
|
||||
return error.InvalidToken;
|
||||
}
|
||||
};
|
83
src/main/api/communities.zig
Normal file
83
src/main/api/communities.zig
Normal file
|
@ -0,0 +1,83 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const util = @import("util");
|
||||
const models = @import("../db/models.zig");
|
||||
|
||||
const getRandom = @import("../api.zig").getRandom;
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
|
||||
const CreateError = error{
|
||||
InvalidOrigin,
|
||||
UnsupportedScheme,
|
||||
CommunityExists,
|
||||
DbError,
|
||||
};
|
||||
|
||||
pub const Scheme = enum {
|
||||
https,
|
||||
http,
|
||||
|
||||
pub fn jsonStringify(s: Scheme, _: std.json.StringifyOptions, writer: anytype) !void {
|
||||
return std.fmt.format(writer, "\"{s}\"", .{@tagName(s)});
|
||||
}
|
||||
};
|
||||
|
||||
pub const Community = struct {
|
||||
id: Uuid,
|
||||
|
||||
host: []const u8,
|
||||
name: []const u8,
|
||||
|
||||
scheme: Scheme,
|
||||
};
|
||||
|
||||
pub fn create(db: anytype, origin: []const u8, name: ?[]const u8) CreateError!Community {
|
||||
const scheme_len = firstIndexOf(origin, ':') orelse return error.InvalidOrigin;
|
||||
const scheme_str = origin[0..scheme_len];
|
||||
const scheme = std.meta.stringToEnum(Scheme, scheme_str) orelse return error.UnsupportedScheme;
|
||||
|
||||
// host must be in the format "{scheme}://{host}"
|
||||
if (origin.len <= scheme_len + ("://").len or
|
||||
origin[scheme_len] != ':' or
|
||||
origin[scheme_len + 1] != '/' or
|
||||
origin[scheme_len + 2] != '/') return error.InvalidOrigin;
|
||||
|
||||
const host = origin[scheme_len + 3 ..];
|
||||
|
||||
// community cannot use non-default ports (except for testing)
|
||||
// NOTE: Do not add, say localhost and localhost:80 or bugs may happen.
|
||||
// Avoid using non-default ports unless a test can't be conducted without it.
|
||||
if (firstIndexOf(host, ':') != null and builtin.mode != .Debug) return error.InvalidOrigin;
|
||||
|
||||
// community cannot be hosted on a path
|
||||
if (firstIndexOf(host, '/') != null) return error.InvalidOrigin;
|
||||
|
||||
// Require TLS on production builds
|
||||
if (scheme != .https and builtin.mode != .Debug) return error.UnsupportedScheme;
|
||||
|
||||
const id = Uuid.randV4(getRandom());
|
||||
|
||||
const community = Community{
|
||||
.id = id,
|
||||
.host = host,
|
||||
.name = name orelse host,
|
||||
.scheme = scheme,
|
||||
};
|
||||
|
||||
if ((db.execRow2(&.{Uuid}, "SELECT id FROM community WHERE host = ?", .{host}, null) catch return error.DbError) != null) {
|
||||
return error.CommunityExists;
|
||||
}
|
||||
|
||||
db.insert2("community", community) catch return error.DbError;
|
||||
|
||||
return community;
|
||||
}
|
||||
|
||||
pub fn firstIndexOf(str: []const u8, ch: u8) ?usize {
|
||||
for (str) |c, i| {
|
||||
if (c == ch) return i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
95
src/main/api/users.zig
Normal file
95
src/main/api/users.zig
Normal file
|
@ -0,0 +1,95 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const auth = @import("./auth.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const getRandom = @import("../api.zig").getRandom;
|
||||
|
||||
const UserAuthInfo = struct {
|
||||
password: []const u8,
|
||||
email: []const u8,
|
||||
invite_used: ?Uuid,
|
||||
};
|
||||
|
||||
pub const CreateError = error{
|
||||
UsernameTaken,
|
||||
DbError,
|
||||
};
|
||||
|
||||
const User = struct {
|
||||
id: Uuid,
|
||||
|
||||
username: []const u8,
|
||||
community_id: ?Uuid,
|
||||
};
|
||||
|
||||
const LocalUser = struct {
|
||||
user_id: Uuid,
|
||||
|
||||
invite_id: ?Uuid,
|
||||
email: ?[]const u8,
|
||||
};
|
||||
|
||||
pub const CreateOptions = struct {
|
||||
invite_id: ?Uuid = null,
|
||||
email: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
fn lookupSystemUserByUsername(db: anytype, username: []const u8) !?Uuid {
|
||||
return if (try db.execRow2(
|
||||
&.{Uuid},
|
||||
"SELECT user.id FROM user WHERE community_id IS NULL AND username = ?",
|
||||
.{username},
|
||||
null,
|
||||
)) |result|
|
||||
result[0]
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
fn lookupUserByUsername(db: anytype, username: []const u8, community_id: Uuid) !?Uuid {
|
||||
return if (try db.execRow2(
|
||||
&.{Uuid},
|
||||
"SELECT user.id FROM user WHERE community_id = ? AND username = ?",
|
||||
.{ community_id, username },
|
||||
null,
|
||||
)) |result|
|
||||
result[0]
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
pub fn lookupByUsername(db: anytype, username: []const u8, community_id: ?Uuid) !?Uuid {
|
||||
return if (community_id) |id|
|
||||
lookupUserByUsername(db, username, id) catch return error.DbError
|
||||
else
|
||||
lookupSystemUserByUsername(db, username) catch return error.DbError;
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
db: anytype,
|
||||
username: []const u8,
|
||||
password: []const u8,
|
||||
community_id: ?Uuid,
|
||||
options: CreateOptions,
|
||||
alloc: std.mem.Allocator,
|
||||
) CreateError!Uuid {
|
||||
const id = Uuid.randV4(getRandom());
|
||||
if ((try lookupByUsername(db, username, community_id)) != null) {
|
||||
return error.UsernameTaken;
|
||||
}
|
||||
|
||||
db.insert2("user", .{
|
||||
.id = id,
|
||||
.username = username,
|
||||
.community_id = community_id,
|
||||
}) catch return error.DbError;
|
||||
try auth.passwords.create(db, id, password, alloc);
|
||||
db.insert2("local_user", .{
|
||||
.user_id = id,
|
||||
.invite_id = options.invite_id,
|
||||
.email = options.email,
|
||||
}) catch return error.DbError;
|
||||
|
||||
return id;
|
||||
}
|
|
@ -3,19 +3,18 @@ const http = @import("http");
|
|||
const Uuid = @import("util").Uuid;
|
||||
|
||||
const utils = @import("../../controllers.zig").utils;
|
||||
const CreateOptions = @import("../../api.zig").CommunityCreateOptions;
|
||||
|
||||
const RequestServer = root.RequestServer;
|
||||
const RouteArgs = http.RouteArgs;
|
||||
|
||||
pub fn create(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void {
|
||||
const opt = try utils.parseRequestBody(CreateOptions, ctx);
|
||||
const opt = try utils.parseRequestBody(struct { origin: []const u8 }, ctx);
|
||||
defer utils.freeRequestBody(opt, ctx.alloc);
|
||||
|
||||
var api = try utils.getApiConn(srv, ctx);
|
||||
defer api.close();
|
||||
|
||||
const invite = try api.createCommunity(opt);
|
||||
const invite = try api.createCommunity(opt.origin);
|
||||
|
||||
try utils.respondJson(ctx, .created, invite);
|
||||
}
|
||||
|
|
|
@ -17,10 +17,7 @@ pub fn login(srv: *RequestServer, ctx: *http.server.Context, _: RouteArgs) !void
|
|||
var api = try utils.getApiConn(srv, ctx);
|
||||
defer api.close();
|
||||
|
||||
const token = api.login(credentials.username, credentials.password) catch |err| switch (err) {
|
||||
error.PasswordVerificationFailed => return utils.respondError(ctx, .bad_request, "Invalid Login"),
|
||||
else => return err,
|
||||
};
|
||||
const token = try api.login(credentials.username, credentials.password);
|
||||
|
||||
try utils.respondJson(ctx, .ok, token);
|
||||
}
|
||||
|
|
|
@ -195,12 +195,13 @@ fn getAlloc(row: sql.Row, comptime T: type, idx: u15, alloc: ?std.mem.Allocator)
|
|||
try getAlloc(row, std.meta.Child(T), idx, alloc),
|
||||
|
||||
.Struct, .Union, .Opaque => if (@hasDecl(T, "getFromSql"))
|
||||
T.getFromSql(row, idx, alloc orelse return error.AllocatorRequired)
|
||||
T.getFromSql(row, idx, alloc)
|
||||
else
|
||||
@compileError("unknown type " ++ @typeName(T)),
|
||||
|
||||
.Enum => try getEnum(row, T, idx, alloc orelse return error.AllocatorRequired),
|
||||
.Enum => try getEnum(row, T, idx, alloc),
|
||||
|
||||
//else => unreachable,
|
||||
else => @compileError("unknown type " ++ @typeName(T)),
|
||||
},
|
||||
};
|
||||
|
@ -263,8 +264,13 @@ pub const Database = struct {
|
|||
defer results.finish();
|
||||
|
||||
const row = results.row(allocator);
|
||||
std.log.debug("done exec", .{});
|
||||
if (row) |r| return r;
|
||||
if (results.err) |err| return err;
|
||||
if (results.err) |err| {
|
||||
std.log.debug("{}", .{err});
|
||||
std.log.debug("{?}", .{@errorReturnTrace()});
|
||||
return err;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ const create_migration_table =
|
|||
// migrations into a single one. this will require db recreation
|
||||
const migrations: []const Migration = &.{
|
||||
.{
|
||||
.name = "users and actors",
|
||||
.name = "users",
|
||||
.up =
|
||||
\\CREATE TABLE user(
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
|
@ -88,23 +88,21 @@ const migrations: []const Migration = &.{
|
|||
\\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\);
|
||||
\\
|
||||
\\CREATE TABLE actor(
|
||||
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||
\\ public_id TEXT NOT NULL
|
||||
\\);
|
||||
\\
|
||||
\\CREATE TABLE local_user(
|
||||
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||
\\
|
||||
\\ email TEXT,
|
||||
\\ email TEXT
|
||||
\\);
|
||||
\\
|
||||
\\ hashed_password TEXT NOT NULL,
|
||||
\\ password_changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\CREATE TABLE account_password(
|
||||
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||
\\
|
||||
\\ hashed_password BLOB NOT NULL
|
||||
\\);
|
||||
,
|
||||
.down =
|
||||
\\DROP TABLE account_password;
|
||||
\\DROP TABLE local_user;
|
||||
\\DROP TABLE actor;
|
||||
\\DROP TABLE user;
|
||||
,
|
||||
},
|
||||
|
@ -115,10 +113,10 @@ const migrations: []const Migration = &.{
|
|||
\\ id TEXT NOT NULL,
|
||||
\\
|
||||
\\ content TEXT NOT NULL,
|
||||
\\ author_id TEXT NOT NULL REFERENCES actor(id),
|
||||
\\ author_id TEXT NOT NULL REFERENCES user(id),
|
||||
\\
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\) STRICT;
|
||||
\\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\);
|
||||
,
|
||||
.down = "DROP TABLE note;",
|
||||
},
|
||||
|
@ -128,11 +126,11 @@ const migrations: []const Migration = &.{
|
|||
\\CREATE TABLE reaction(
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ reactor_id TEXT NOT NULL REFERENCES actor(id),
|
||||
\\ user_id TEXT NOT NULL REFERENCES user(id),
|
||||
\\ note_id TEXT NOT NULL REFERENCES note(id),
|
||||
\\
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\) STRICT;
|
||||
\\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\);
|
||||
,
|
||||
.down = "DROP TABLE reaction;",
|
||||
},
|
||||
|
@ -140,13 +138,11 @@ const migrations: []const Migration = &.{
|
|||
.name = "user tokens",
|
||||
.up =
|
||||
\\CREATE TABLE token(
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\
|
||||
\\ hash BLOB UNIQUE NOT NULL,
|
||||
\\ hash TEXT NOT NULL PRIMARY KEY,
|
||||
\\ user_id TEXT NOT NULL REFERENCES local_user(id),
|
||||
\\
|
||||
\\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\) STRICT;
|
||||
\\ issued_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\);
|
||||
,
|
||||
.down = "DROP TABLE token;",
|
||||
},
|
||||
|
@ -162,9 +158,9 @@ const migrations: []const Migration = &.{
|
|||
\\
|
||||
\\ max_uses INTEGER,
|
||||
\\
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\\ expires_at INTEGER
|
||||
\\) STRICT;
|
||||
\\ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
\\ expires_at DATETIME
|
||||
\\);
|
||||
\\ALTER TABLE local_user ADD COLUMN invite_id TEXT REFERENCES invite(id);
|
||||
,
|
||||
.down =
|
||||
|
|
|
@ -4,137 +4,3 @@ const sql = @import("sql");
|
|||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
pub fn ByteArray(comptime n: usize) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
data: [n]u8,
|
||||
|
||||
pub fn bindToSql(self: Self, stmt: sql.PreparedStmt, idx: u15) !void {
|
||||
return stmt.bindBlob(idx, &self.data);
|
||||
}
|
||||
|
||||
pub fn getFromSql(row: sql.Row, idx: u15, _: std.mem.Alloc) !Self {
|
||||
var self: Self = undefined;
|
||||
_ = try row.getBlob(idx, &self.data);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn format(self: Self, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
const Encoder = std.base64.standard.Encoder;
|
||||
const buf_len = comptime Encoder.calcSize(n);
|
||||
var buf: [buf_len]u8 = undefined;
|
||||
const str = Encoder.encode(&buf, &self.data);
|
||||
try std.fmt.format(writer, "{s}", .{str});
|
||||
}
|
||||
|
||||
pub fn stringifyJson(self: Self, _: std.json.StringifyOptions, writer: anytype) !void {
|
||||
try self.format("{}", .{}, writer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const ByteSlice = struct {
|
||||
const Self = @This();
|
||||
|
||||
data: []const u8,
|
||||
|
||||
pub fn bindToSql(self: Self, stmt: sql.PreparedStmt, idx: u15) !void {
|
||||
return stmt.bindBlob(idx, self.data);
|
||||
}
|
||||
|
||||
pub fn getFromSql(row: sql.Row, idx: u15, alloc: std.mem.Alloc) !void {
|
||||
return Self{
|
||||
.data = try row.getBlobAlloc(idx, alloc),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Used for documentation purposes
|
||||
fn Ref(comptime _: type) type {
|
||||
return Uuid;
|
||||
}
|
||||
|
||||
// TODO: Should created_at / etc refer to the time the object was created? or the time
|
||||
// the row representing it was created? Matters for federation
|
||||
|
||||
pub const User = struct {
|
||||
id: Uuid,
|
||||
username: []const u8,
|
||||
community_id: ?Ref(Community),
|
||||
};
|
||||
|
||||
pub const Actor = struct {
|
||||
user_id: Ref(User),
|
||||
public_id: []const u8,
|
||||
};
|
||||
|
||||
pub const LocalUser = struct {
|
||||
user_id: Ref(User),
|
||||
|
||||
email: ?[]const u8,
|
||||
invite_id: ?Ref(Invite),
|
||||
|
||||
hashed_password: []const u8, // encoded in PHC format, with salt
|
||||
};
|
||||
|
||||
pub const Note = struct {
|
||||
id: Uuid,
|
||||
content: []const u8,
|
||||
author_id: Ref(Actor),
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
||||
pub const Reaction = struct {
|
||||
id: Uuid,
|
||||
reactor_id: Ref(Actor),
|
||||
note_id: Ref(Note),
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
||||
pub const Token = struct {
|
||||
pub const HashFn = std.crypto.hash.sha2.Sha256;
|
||||
pub const hash_len = HashFn.digest_length;
|
||||
|
||||
id: Uuid,
|
||||
|
||||
hash: ByteArray(hash_len),
|
||||
user_id: Ref(LocalUser),
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
pub const Invite = struct {
|
||||
id: Uuid,
|
||||
|
||||
name: []const u8,
|
||||
invite_code: []const u8,
|
||||
created_by: Ref(LocalUser),
|
||||
to_community: ?Ref(Community),
|
||||
|
||||
max_uses: ?i64,
|
||||
|
||||
created_at: DateTime,
|
||||
expires_at: ?DateTime,
|
||||
};
|
||||
|
||||
pub const Community = struct {
|
||||
pub const Scheme = enum {
|
||||
https,
|
||||
http,
|
||||
|
||||
pub fn jsonStringify(s: Scheme, _: std.json.StringifyOptions, writer: anytype) !void {
|
||||
return std.fmt.format(writer, "\"{}\"", .{s});
|
||||
}
|
||||
};
|
||||
|
||||
id: Uuid,
|
||||
|
||||
name: []const u8,
|
||||
host: []const u8,
|
||||
scheme: Scheme,
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
|
|
@ -22,6 +22,10 @@ const router = Router{
|
|||
Route.new(.POST, "/login", &c.auth.login),
|
||||
Route.new(.GET, "/login", &c.auth.verifyLogin),
|
||||
|
||||
Route.new(.POST, "/communities", &c.admin.communities.create),
|
||||
|
||||
//Route.new(.POST, "/invites", &c.admin.invites.create),
|
||||
|
||||
//Route.new(.POST, "/notes", &c.notes.create),
|
||||
//Route.new(.GET, "/notes/:id", &c.notes.get),
|
||||
|
||||
|
@ -30,10 +34,8 @@ const router = Router{
|
|||
|
||||
//Route.new(.GET, "/actors/:id", &c.actors.get),
|
||||
|
||||
//Route.new(.POST, "/admin/invites", &c.admin.invites.create),
|
||||
//Route.new(.GET, "/admin/invites/:id", &c.admin.invites.get),
|
||||
|
||||
//Route.new(.POST, "/admin/communities", &c.admin.communities.create),
|
||||
//Route.new(.GET, "/admin/communities/:host", &c.admin.communities.get),
|
||||
},
|
||||
};
|
||||
|
@ -67,7 +69,7 @@ pub const RequestServer = struct {
|
|||
router.dispatch(self, &ctx, ctx.request.method, ctx.request.path) catch |err| switch (err) {
|
||||
error.NotFound, error.RouteNotApplicable => c.notFound(self, &ctx),
|
||||
else => {
|
||||
std.log.err("Unhandled error in controller ({s}): {}", .{ ctx.request.path, err });
|
||||
std.log.err("Unhandled error in controller ({s}): {}\nStack Trace\n{?}", .{ ctx.request.path, err, @errorReturnTrace() });
|
||||
c.internalServerError(self, &ctx);
|
||||
},
|
||||
};
|
||||
|
@ -77,7 +79,6 @@ pub const RequestServer = struct {
|
|||
|
||||
pub const Config = struct {
|
||||
cluster_host: []const u8,
|
||||
cluster_scheme: models.Community.Scheme,
|
||||
};
|
||||
|
||||
fn loadConfig(alloc: std.mem.Allocator) !Config {
|
||||
|
|
|
@ -68,7 +68,7 @@ pub const Row = struct {
|
|||
}
|
||||
|
||||
pub fn getBlob(self: Row, idx: u15, buf: []u8) ![]u8 {
|
||||
const ptr = c.sqlite3_column_blob(self.stmt, idx);
|
||||
const ptr = @ptrCast([*]const u8, c.sqlite3_column_blob(self.stmt, idx));
|
||||
|
||||
const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, idx));
|
||||
if (size > buf.len) return error.StreamTooLong;
|
||||
|
|
Loading…
Reference in a new issue