messing w/ stuff

This commit is contained in:
jaina heartles 2022-09-07 16:14:52 -07:00
parent 913a84c4fa
commit ab96b3b734
13 changed files with 584 additions and 324 deletions

11
src/main/README.md Normal file
View 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

View File

@ -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
View 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
View 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;
}
};

View 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
View 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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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 =

View File

@ -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,
};

View File

@ -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 {

View File

@ -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;