Compare commits

..

No commits in common. "main" and "api-refactor" have entirely different histories.

35 changed files with 1431 additions and 2101 deletions

View file

@ -99,16 +99,10 @@ pub fn build(b: *std.build.Builder) !void {
exe.addSystemIncludePath("/usr/include/");
exe.addSystemIncludePath("/usr/include/postgresql"); // HACK
const unittest_options = b.addOptions();
unittest_options.addOption(bool, "enable_sqlite", false);
unittest_options.addOption(bool, "enable_postgres", false);
const unittest_pkgs = makePkgs(b, unittest_options.getPackage("build_options"));
const unittest_http_cmd = b.step("unit:http", "Run tests for http package");
const unittest_http = b.addTest("src/http/lib.zig");
unittest_http_cmd.dependOn(&unittest_http.step);
unittest_http.addPackage(unittest_pkgs.util);
unittest_http.addPackage(pkgs.util);
const unittest_util_cmd = b.step("unit:util", "Run tests for util package");
const unittest_util = b.addTest("src/util/lib.zig");
@ -117,8 +111,7 @@ pub fn build(b: *std.build.Builder) !void {
const unittest_sql_cmd = b.step("unit:sql", "Run tests for sql package");
const unittest_sql = b.addTest("src/sql/lib.zig");
unittest_sql_cmd.dependOn(&unittest_sql.step);
unittest_sql.addPackage(unittest_pkgs.util);
//unittest_sql.linkLibC();
unittest_sql.addPackage(pkgs.util);
const unittest_template_cmd = b.step("unit:template", "Run tests for template package");
const unittest_template = b.addTest("src/template/lib.zig");
@ -127,9 +120,8 @@ pub fn build(b: *std.build.Builder) !void {
const unittest_api_cmd = b.step("unit:api", "Run tests for api package");
const unittest_api = b.addTest("src/api/lib.zig");
unittest_api_cmd.dependOn(&unittest_api.step);
unittest_api.addPackage(unittest_pkgs.util);
unittest_api.addPackage(unittest_pkgs.sql);
//unittest_api.linkLibC();
unittest_api.addPackage(pkgs.util);
unittest_api.addPackage(pkgs.sql);
//const util_tests = b.addTest("src/util/lib.zig");
//const sql_tests = b.addTest("src/sql/lib.zig");

View file

@ -1,18 +1,39 @@
const std = @import("std");
const util = @import("util");
const sql = @import("sql");
const services = @import("./services.zig");
const types = @import("./types.zig");
const DateTime = util.DateTime;
const Uuid = util.Uuid;
pub usingnamespace types;
pub const Actor = types.actors.Actor;
pub const Community = types.communities.Community;
pub const Invite = types.invites.Invite;
pub const Note = types.notes.Note;
pub const Token = types.tokens.Token;
const default_avatar = "static/default_avi.png";
const services = struct {
pub const communities = @import("./services/communities.zig");
pub const actors = @import("./services/actors.zig");
pub const drive = @import("./services/drive.zig");
pub const files = @import("./services/files.zig");
pub const invites = @import("./services/invites.zig");
pub const notes = @import("./services/notes.zig");
pub const follows = @import("./services/follows.zig");
pub const accounts = @import("./services/accounts.zig");
pub const tokens = @import("./services/tokens.zig");
};
test {
_ = @import("./methods/auth.zig");
}
const types = @import("./types.zig");
pub const QueryResult = types.QueryResult;
pub const Account = types.Account;
pub const Actor = types.Actor;
pub const Community = types.Community;
pub const Invite = types.Invite;
pub const Note = types.Note;
pub const Token = types.Token;
pub const ClusterMeta = struct {
community_count: usize,
@ -20,43 +41,234 @@ pub const ClusterMeta = struct {
note_count: usize,
};
pub const RegistrationOptions = struct {
invite_code: ?[]const u8 = null,
email: ?[]const u8 = null,
};
pub const InviteOptions = struct {
pub const Kind = Invite.Kind;
name: ?[]const u8 = null,
lifespan: ?DateTime.Duration = null,
max_uses: ?usize = null,
// admin only options
kind: Kind = .user,
to_community: ?Uuid = null,
};
pub const UserResponse = struct {
id: Uuid,
username: []const u8,
host: []const u8,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
avatar_url: []const u8,
header_file_id: ?Uuid,
header_url: ?[]const u8,
profile_fields: []const Actor.ProfileField,
community_id: Uuid,
created_at: DateTime,
updated_at: DateTime,
};
pub const NoteResponse = struct {
id: Uuid,
author: struct {
id: Uuid,
username: []const u8,
host: []const u8,
},
content: []const u8,
created_at: DateTime,
};
pub const TimelineArgs = struct {
pub const PageDirection = Note.QueryArgs.PageDirection;
pub const Prev = Note.QueryArgs.Prev;
max_items: usize = 20,
created_before: ?DateTime = null,
created_after: ?DateTime = null,
prev: ?Prev = null,
page_direction: PageDirection = .forward,
fn from(args: Note.QueryArgs) TimelineArgs {
return .{
.max_items = args.max_items,
.created_before = args.created_before,
.created_after = args.created_after,
.prev = args.prev,
.page_direction = args.page_direction,
};
}
};
pub const TimelineResult = struct {
items: []Note,
prev_page: TimelineArgs,
next_page: TimelineArgs,
};
const FollowQueryArgs = struct {
pub const OrderBy = services.follows.QueryArgs.OrderBy;
pub const Direction = services.follows.QueryArgs.Direction;
pub const PageDirection = services.follows.QueryArgs.PageDirection;
pub const Prev = services.follows.QueryArgs.Prev;
max_items: usize = 20,
order_by: OrderBy = .created_at,
direction: Direction = .descending,
prev: ?Prev = null,
page_direction: PageDirection = .forward,
fn from(args: services.follows.QueryArgs) FollowQueryArgs {
return .{
.max_items = args.max_items,
.order_by = args.order_by,
.direction = args.direction,
.prev = args.prev,
.page_direction = args.page_direction,
};
}
};
const FollowQueryResult = struct {
items: []services.follows.Follow,
prev_page: FollowQueryArgs,
next_page: FollowQueryArgs,
};
pub const FollowerQueryArgs = FollowQueryArgs;
pub const FollowerQueryResult = FollowQueryResult;
pub const FollowingQueryArgs = FollowQueryArgs;
pub const FollowingQueryResult = FollowQueryResult;
pub const UploadFileArgs = struct {
filename: []const u8,
dir: []const u8,
description: ?[]const u8,
content_type: []const u8,
sensitive: bool,
};
pub const DriveEntry = union(enum) {
const Kind = services.drive.Kind;
dir: struct {
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
kind: Kind = .dir,
// If null = not enumerated
children: ?[]const DriveEntry,
},
file: struct {
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
kind: Kind = .file,
meta: FileUpload,
},
};
pub const FileUpload = types.FileUpload;
pub const DriveGetResult = union(services.drive.Kind) {
dir: struct {
entry: DriveEntry,
children: []DriveEntry,
},
file: struct {
entry: DriveEntry,
file: FileUpload,
},
};
pub const FileResult = struct {
meta: FileUpload,
data: []const u8,
};
pub const InviteResponse = struct {
code: []const u8,
kind: Invite.Kind,
name: []const u8,
creator: UserResponse,
url: []const u8,
community_id: Uuid,
created_at: DateTime,
expires_at: ?DateTime,
times_used: usize,
max_uses: ?usize,
};
pub fn isAdminSetup(db: sql.Db) !bool {
const svc = services.Services(sql.Db){ .db = db };
_ = svc.getAdminCommunityId() catch |err| switch (err) {
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
error.NotFound => return false,
else => |e| return e,
else => return err,
};
return true;
}
pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password: []const u8, allocator: std.mem.Allocator) anyerror!void {
const svc = @import("./services.zig").Services(sql.Db){ .db = db };
const tx = try svc.beginTx();
errdefer tx.rollbackTx();
const tx = try db.begin();
errdefer tx.rollback();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const community_id = try tx.createCommunity(
arena.allocator(),
try tx.setConstraintMode(.deferred);
const community_id = try services.communities.create(
tx,
origin,
.{ .name = "Cluster Admin", .kind = .admin },
);
const user = try @import("./methods/auth.zig").createLocalAccount(
arena.allocator(),
tx,
.{
.username = username,
.password = password,
.community_id = community_id,
.role = .admin,
},
);
try tx.transferCommunityOwnership(community_id, user);
const user = try @import("./methods/auth.zig").methods(services).createLocalAccount(
tx,
username,
password,
community_id,
.{ .role = .admin },
arena.allocator(),
);
try tx.commitTx();
try services.communities.transferOwnership(tx, community_id, user);
try tx.commit();
std.log.info(
"Created admin user {s} (id {}) with cluster admin origin {s} (id {})",
@ -67,7 +279,7 @@ pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password
pub const ApiSource = struct {
db_conn_pool: *sql.ConnPool,
pub const Conn = ApiConn(sql.Db, method_impl);
pub const Conn = ApiConn(sql.Db, services);
const root_username = "root";
@ -80,23 +292,21 @@ pub const ApiSource = struct {
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
const db = try self.db_conn_pool.acquire();
errdefer db.releaseConnection();
var conn = Conn{
const community = try services.communities.getByHost(db, host, alloc);
return Conn{
.db = db,
.context = .{
.community = undefined,
.community = community,
},
.allocator = alloc,
};
conn.context.community = try conn.getServices().getCommunityByHost(alloc, host);
return conn;
}
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();
conn.context.token_info = try @import("./methods/auth.zig").verifyToken(alloc, conn.context, conn.getServices(), token);
conn.context.token_info = try conn.verifyToken(token);
return conn;
}
};
@ -106,29 +316,13 @@ pub const ApiContext = struct {
community: Community,
pub fn userId(self: ApiContext) ?Uuid {
if (self.token_info) |t| return t.account_id else return null;
}
pub fn isAdmin(self: ApiContext) bool {
return self.userId() != null and self.community.kind == .admin;
if (self.token_info) |t| return t.user_id else return null;
}
};
const method_impl = struct {
const actors = @import("./methods/actors.zig");
const auth = @import("./methods/auth.zig");
const communities = @import("./methods/communities.zig");
const drive = @import("./methods/drive.zig");
const follows = @import("./methods/follows.zig");
const invites = @import("./methods/invites.zig");
const notes = @import("./methods/notes.zig");
const timelines = @import("./methods/timelines.zig");
};
fn ApiConn(comptime DbConn: type, comptime methods: anytype) type {
fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
return struct {
const Self = @This();
const Services = @import("./services.zig").Services(DbConn);
db: DbConn,
context: ApiContext,
@ -140,87 +334,332 @@ fn ApiConn(comptime DbConn: type, comptime methods: anytype) type {
self.db.releaseConnection();
}
fn getServices(self: *Self) Services {
return Services{ .db = self.db };
fn isAdmin(self: *Self) bool {
// TODO
return self.context.userId() != null and self.context.community.kind == .admin;
}
pub fn getCommunity(self: *Self, id: Uuid) !types.communities.Community {
return try methods.communities.get(self.allocator, self.context, self.getServices(), id);
}
pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Uuid {
return try methods.communities.create(
self.allocator,
self.context,
self.getServices(),
origin,
name,
);
}
pub fn createInvite(self: *Self, options: types.invites.CreateOptions) !Uuid {
return methods.invites.create(self.allocator, self.context, self.getServices(), options);
}
pub fn getInvite(self: *Self, id: Uuid) !Invite {
return try methods.invites.get(self.allocator, self.context, self.getServices(), id);
}
pub fn login(self: *Self, username: []const u8, password: []const u8) !Token {
return methods.auth.login(self.allocator, self.context, self.getServices(), username, password);
}
pub fn register(
self: *Self,
pub const AuthorizationInfo = struct {
id: Uuid,
username: []const u8,
password: []const u8,
opt: types.auth.RegistrationOptions,
) !Uuid {
return methods.auth.register(self.allocator, self.context, self.getServices(), username, password, opt);
community_id: Uuid,
host: []const u8,
issued_at: DateTime,
};
pub fn verifyAuthorization(self: *Self) !AuthorizationInfo {
if (self.context.token_info) |info| {
const user = try models.actors.get(self.db, info.user_id, self.allocator);
defer util.deepFree(self.allocator, user);
const username = try util.deepClone(self.allocator, user.username);
errdefer util.deepFree(self.allocator, username);
return AuthorizationInfo{
.id = user.id,
.username = username,
.community_id = self.context.community.id,
.host = try util.deepClone(self.allocator, self.context.community.host),
.issued_at = info.issued_at,
};
}
return error.TokenRequired;
}
pub fn getActor(self: *Self, user_id: Uuid) !Actor {
return methods.actors.get(self.allocator, self.context, self.getServices(), user_id);
pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Community {
if (!self.isAdmin()) {
return error.PermissionDenied;
}
const tx = try self.db.begin();
errdefer tx.rollback();
const community_id = try models.communities.create(
tx,
origin,
.{ .name = name },
self.allocator,
);
const community = models.communities.get(
tx,
community_id,
self.allocator,
) catch |err| return switch (err) {
error.NotFound => error.DatabaseError,
else => |err2| err2,
};
try tx.commit();
return community;
}
pub fn createNote(self: *Self, content: []const u8) !Uuid {
return methods.notes.create(self.allocator, self.context, self.getServices(), content);
pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse {
// Only logged in users can make invites
const user_id = self.context.userId() orelse return error.TokenRequired;
const community_id = if (options.to_community) |id| blk: {
// Only admins can send invites for other communities
if (!self.isAdmin()) return error.PermissionDenied;
break :blk id;
} else self.context.community.id;
// Users can only make user invites
if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied;
const invite_id = try models.invites.create(self.db, user_id, community_id, options.name orelse "", .{
.lifespan = options.lifespan,
.max_uses = options.max_uses,
.kind = options.kind,
}, self.allocator);
const invite = try models.invites.get(self.db, invite_id, self.allocator);
errdefer util.deepFree(self.allocator, invite);
const url = if (options.to_community) |cid| blk: {
const community = try models.communities.get(self.db, cid, self.allocator);
defer util.deepFree(self.allocator, community);
break :blk try std.fmt.allocPrint(
self.allocator,
"{s}://{s}/invite/{s}",
.{ @tagName(community.scheme), community.host, invite.code },
);
} else try std.fmt.allocPrint(
self.allocator,
"{s}://{s}/invite/{s}",
.{ @tagName(self.context.community.scheme), self.context.community.host, invite.code },
);
errdefer util.deepFree(self.allocator, url);
const user = try self.getUserUnchecked(self.db, user_id);
return InviteResponse{
.code = invite.code,
.kind = invite.kind,
.name = invite.name,
.creator = user,
.url = url,
.community_id = invite.community_id,
.created_at = invite.created_at,
.expires_at = invite.expires_at,
.times_used = invite.times_used,
.max_uses = invite.max_uses,
};
}
fn isInviteValid(invite: Invite) bool {
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false;
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false;
return true;
}
pub usingnamespace @import("./methods/auth.zig").methods(models);
// pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse {
// const tx = try self.db.beginOrSavepoint();
// const maybe_invite = if (opt.invite_code) |code|
// try models.invites.getByCode(tx, code, self.context.community.id, self.allocator)
// else
// null;
// defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv);
// if (maybe_invite) |invite| {
// if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity;
// if (!isInviteValid(invite)) return error.InvalidInvite;
// }
// const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
// if (self.context.community.kind == .admin) @panic("Unimplmented");
// const user_id = try models.auth.register(
// tx,
// username,
// password,
// self.context.community.id,
// .{
// .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null,
// .email = opt.email,
// },
// self.allocator,
// );
// switch (invite_kind) {
// .user => {},
// .system => @panic("System user invites unimplemented"),
// .community_owner => {
// try models.communities.transferOwnership(tx, self.context.community.id, user_id);
// },
// }
// const user = self.getUserUnchecked(tx, user_id) catch |err| switch (err) {
// error.NotFound => return error.Unexpected,
// else => |e| return e,
// };
// errdefer util.deepFree(self.allocator, user);
// try tx.commit();
// return user;
// }
fn getUserUnchecked(self: *Self, db: anytype, user_id: Uuid) !UserResponse {
const user = try models.actors.get(db, user_id, self.allocator);
const avatar_url = if (user.avatar_file_id) |fid|
try std.fmt.allocPrint(
self.allocator,
"{s}://{s}/media/{}",
.{ @tagName(self.context.community.scheme), self.context.community.host, fid },
)
else
try std.fmt.allocPrint(
self.allocator,
"{s}://{s}/{s}",
.{ @tagName(self.context.community.scheme), self.context.community.host, default_avatar },
);
errdefer self.allocator.free(avatar_url);
const header_url = if (user.header_file_id) |fid|
try std.fmt.allocPrint(
self.allocator,
"{s}://{s}/media/{}",
.{ @tagName(self.context.community.scheme), self.context.community.host, fid },
)
else
null;
return UserResponse{
.id = user.id,
.username = user.username,
.host = user.host,
.display_name = user.display_name,
.bio = user.bio,
.avatar_file_id = user.avatar_file_id,
.avatar_url = avatar_url,
.header_file_id = user.header_file_id,
.header_url = header_url,
.profile_fields = user.profile_fields,
.community_id = user.community_id,
.created_at = user.created_at,
.updated_at = user.updated_at,
};
}
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
const user = try self.getUserUnchecked(self.db, user_id);
errdefer util.deepFree(self.allocator, user);
if (self.context.userId() == null) {
if (!Uuid.eql(self.context.community.id, user.community_id)) return error.NotFound;
}
return user;
}
pub fn createNote(self: *Self, content: []const u8) !Note {
// You cannot post on admin accounts
if (self.context.community.kind == .admin) return error.WrongCommunity;
// Only authenticated users can post
const user_id = self.context.userId() orelse return error.TokenRequired;
const note_id = try models.notes.create(self.db, user_id, content, self.allocator);
return self.getNote(note_id) catch |err| switch (err) {
error.NotFound => error.Unexpected,
else => err,
};
}
pub fn getNote(self: *Self, note_id: Uuid) !Note {
return methods.notes.get(self.allocator, self.context, self.getServices(), note_id);
const note = try models.notes.get(self.db, note_id, self.allocator);
errdefer util.deepFree(self.allocator, note);
// Only serve community-specific notes on unauthenticated requests
if (self.context.userId() == null) {
if (!Uuid.eql(self.context.community.id, note.author.community_id)) return error.NotFound;
}
return note;
}
pub fn queryCommunities(self: *Self, args: types.communities.QueryArgs) !types.communities.QueryResult {
return methods.communities.query(self.allocator, self.context, self.getServices(), args);
pub fn queryCommunities(self: *Self, args: Community.QueryArgs) !QueryResult(Community) {
if (!self.isAdmin()) return error.PermissionDenied;
return try models.communities.query(self.db, args, self.allocator);
}
pub fn globalTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult {
return methods.timelines.globalTimeline(self.allocator, self.context, self.getServices(), args);
pub fn globalTimeline(self: *Self, args: TimelineArgs) !TimelineResult {
const all_args = std.mem.zeroInit(Note.QueryArgs, args);
const result = try models.notes.query(self.db, all_args, self.allocator);
return TimelineResult{
.items = result.items,
.prev_page = TimelineArgs.from(result.prev_page),
.next_page = TimelineArgs.from(result.next_page),
};
}
pub fn localTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult {
return methods.timelines.localTimeline(self.allocator, self.context, self.getServices(), args);
pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult {
var all_args = std.mem.zeroInit(Note.QueryArgs, args);
all_args.community_id = self.context.community.id;
const result = try models.notes.query(self.db, all_args, self.allocator);
return TimelineResult{
.items = result.items,
.prev_page = TimelineArgs.from(result.prev_page),
.next_page = TimelineArgs.from(result.next_page),
};
}
pub fn homeTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult {
return methods.timelines.homeTimeline(self.allocator, self.context, self.getServices(), args);
pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult {
if (self.context.userId() == null) return error.NoToken;
var all_args = std.mem.zeroInit(Note.QueryArgs, args);
all_args.followed_by = self.context.userId();
const result = try models.notes.query(self.db, all_args, self.allocator);
return TimelineResult{
.items = result.items,
.prev_page = TimelineArgs.from(result.prev_page),
.next_page = TimelineArgs.from(result.next_page),
};
}
pub fn queryFollowers(self: *Self, user_id: Uuid, args: types.follows.FollowerQueryArgs) !types.follows.FollowerQueryResult {
return methods.follows.queryFollowers(self.allocator, self.context, self.getServices(), user_id, args);
pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult {
var all_args = std.mem.zeroInit(models.follows.QueryArgs, args);
all_args.followee_id = user_id;
const result = try models.follows.query(self.db, all_args, self.allocator);
return FollowerQueryResult{
.items = result.items,
.prev_page = FollowQueryArgs.from(result.prev_page),
.next_page = FollowQueryArgs.from(result.next_page),
};
}
pub fn queryFollowing(self: *Self, user_id: Uuid, args: types.follows.FollowingQueryArgs) !types.follows.FollowingQueryResult {
return methods.follows.queryFollowing(self.allocator, self.context, self.getServices(), user_id, args);
pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult {
var all_args = std.mem.zeroInit(models.follows.QueryArgs, args);
all_args.followed_by_id = user_id;
const result = try models.follows.query(self.db, all_args, self.allocator);
return FollowingQueryResult{
.items = result.items,
.prev_page = FollowQueryArgs.from(result.prev_page),
.next_page = FollowQueryArgs.from(result.next_page),
};
}
pub fn follow(self: *Self, followee: Uuid) !void {
try methods.follows.follow(self.allocator, self.context, self.getServices(), followee);
const result = try models.follows.create(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator);
defer util.deepFree(self.allocator, result);
}
pub fn unfollow(self: *Self, followee: Uuid) !void {
try methods.follows.unfollow(self.allocator, self.context, self.getServices(), followee);
const result = try models.follows.delete(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator);
defer util.deepFree(self.allocator, result);
}
pub fn getClusterMeta(self: *Self) !ClusterMeta {
@ -240,48 +679,201 @@ fn ApiConn(comptime DbConn: type, comptime methods: anytype) type {
);
}
pub fn driveUpload(self: *Self, meta: types.drive.UploadArgs, body: []const u8) !Uuid {
return try methods.drive.upload(self.allocator, self.context, self.getServices(), meta, body);
fn backendDriveEntryToFrontend(self: *Self, entry: models.drive.Entry, recurse: bool) !DriveEntry {
return if (entry.file_id) |file_id| .{
.file = .{
.id = entry.id,
.owner_id = entry.owner_id,
.name = entry.name,
.path = entry.path,
.parent_directory_id = entry.parent_directory_id,
.meta = try models.files.get(self.db, file_id, self.allocator),
},
} else .{
.dir = .{
.id = entry.id,
.owner_id = entry.owner_id,
.name = entry.name,
.path = entry.path,
.parent_directory_id = entry.parent_directory_id,
.children = blk: {
if (!recurse) break :blk null;
const children = try models.drive.list(self.db, entry.id, self.allocator);
const result = self.allocator.alloc(DriveEntry, children.len) catch |err| {
util.deepFree(self.allocator, children);
return err;
};
var count: usize = 0;
errdefer for (children) |child, i| {
if (i < count)
util.deepFree(self.allocator, result[i])
else
util.deepFree(self.allocator, child);
};
defer self.allocator.free(children);
errdefer self.allocator.free(result);
for (children) |child, i| {
result[i] = try backendDriveEntryToFrontend(self, child, false);
count += 1;
}
break :blk result;
},
},
};
}
pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !Uuid {
return try methods.drive.mkdir(self.allocator, self.context, self.getServices(), parent_path, name);
pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry {
const user_id = self.context.userId() orelse return error.NoToken;
const file_id = try models.files.create(self.db, user_id, .{
.filename = meta.filename,
.description = meta.description,
.content_type = meta.content_type,
.sensitive = meta.sensitive,
}, body, self.allocator);
const entry = entry: {
errdefer models.files.delete(self.db, file_id, self.allocator) catch |err| {
std.log.err("Unable to delete file {}: {}", .{ file_id, err });
};
break :entry models.drive.create(
self.db,
user_id,
meta.dir,
meta.filename,
file_id,
self.allocator,
) catch |err| switch (err) {
error.PathAlreadyExists => {
var buf: [256]u8 = undefined;
var split = std.mem.splitBackwards(u8, meta.filename, ".");
const ext = split.first();
const name = split.rest();
const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext });
break :entry try models.drive.create(
self.db,
user_id,
meta.dir,
new_name,
file_id,
self.allocator,
);
},
else => |e| return e,
};
};
errdefer util.deepFree(self.allocator, entry);
return try self.backendDriveEntryToFrontend(entry, true);
}
pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry {
const user_id = self.context.userId() orelse return error.NoToken;
const entry = try models.drive.create(self.db, user_id, parent_path, name, null, self.allocator);
errdefer util.deepFree(self.allocator, entry);
return try self.backendDriveEntryToFrontend(entry, true);
}
pub fn driveDelete(self: *Self, path: []const u8) !void {
return try methods.drive.delete(self.allocator, self.context, self.getServices(), path);
const user_id = self.context.userId() orelse return error.NoToken;
const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
defer util.deepFree(self.allocator, entry);
try models.drive.delete(self.db, entry.id, self.allocator);
if (entry.file_id) |file_id| try models.files.delete(self.db, file_id, self.allocator);
}
pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !void {
return try methods.drive.move(self.allocator, self.context, self.getServices(), src, dest);
pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry {
const user_id = self.context.userId() orelse return error.NoToken;
try models.drive.move(self.db, user_id, src, dest, self.allocator);
return try self.driveGet(dest);
}
pub fn driveGet(self: *Self, path: []const u8) !types.drive.DriveEntry {
return try methods.drive.get(self.allocator, self.context, self.getServices(), path);
pub fn driveGet(self: *Self, path: []const u8) !DriveEntry {
const user_id = self.context.userId() orelse return error.NoToken;
const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
errdefer util.deepFree(self.allocator, entry);
return try self.backendDriveEntryToFrontend(entry, true);
}
pub fn driveGetEntryById(self: *Self, id: Uuid) !types.drive.DriveEntry {
return try methods.drive.getById(self.allocator, self.context, self.getServices(), id);
pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry {
const user_id = self.context.userId() orelse return error.NoToken;
std.log.debug("{s}", .{path});
const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
defer util.deepFree(self.allocator, entry);
std.log.debug("{}", .{entry.id});
try models.files.update(self.db, entry.file_id orelse return error.NotAFile, meta, self.allocator);
return try self.driveGet(path);
}
pub fn driveUpdate(self: *Self, path: []const u8, meta: types.files.UpdateArgs) !void {
return try methods.drive.update(self.allocator, self.context, self.getServices(), path, meta);
pub fn fileDereference(self: *Self, id: Uuid) !FileResult {
const meta = try models.files.get(self.db, id, self.allocator);
errdefer util.deepFree(self.allocator, meta);
return FileResult{
.meta = meta,
.data = try models.files.deref(self.allocator, id),
};
}
pub fn fileDereference(self: *Self, id: Uuid) !types.files.DerefResult {
return try methods.drive.dereference(self.allocator, self.context, self.getServices(), id);
pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void {
if (!Uuid.eql(id, self.context.userId() orelse return error.NoToken)) return error.AccessDenied;
try models.actors.updateProfile(self.db, id, data, self.allocator);
}
pub fn updateUserProfile(self: *Self, id: Uuid, data: types.actors.ProfileUpdateArgs) !void {
try methods.actors.updateProfile(self.allocator, self.context, self.getServices(), id, data);
}
pub fn validateInvite(self: *Self, code: []const u8) !InviteResponse {
const invite = models.invites.getByCode(
self.db,
code,
self.context.community.id,
self.allocator,
) catch |err| switch (err) {
error.NotFound => return error.InvalidInvite,
else => return error.DatabaseFailure,
};
errdefer util.deepFree(self.allocator, invite);
pub fn validateInvite(self: *Self, code: []const u8) !Invite {
return try methods.invites.getByCode(self.allocator, self.context, self.getServices(), code);
if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.InvalidInvite;
if (!isInviteValid(invite)) return error.InvalidInvite;
const url = try std.fmt.allocPrint(
self.allocator,
"{s}://{s}/invite/{s}",
.{ @tagName(self.context.community.scheme), self.context.community.host, invite.code },
);
errdefer util.deepFree(self.allocator, url);
const creator = self.getUserUnchecked(self.db, invite.created_by) catch |err| switch (err) {
error.NotFound => return error.Unexpected,
else => return error.DatabaseFailure,
};
return InviteResponse{
.code = invite.code,
.name = invite.name,
.kind = invite.kind,
.creator = creator,
.url = url,
.community_id = invite.community_id,
.created_at = invite.created_at,
.expires_at = invite.expires_at,
.times_used = invite.times_used,
.max_uses = invite.max_uses,
};
}
};
}
test {
std.testing.refAllDecls(@This());
}

View file

@ -1,79 +0,0 @@
const std = @import("std");
const util = @import("util");
const services = @import("../services.zig");
const pkg = @import("../lib.zig");
const Uuid = util.Uuid;
const ApiContext = pkg.ApiContext;
const default_avatar = "static/default_avi.png";
pub fn get(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
id: Uuid,
) !pkg.Actor {
const actor = try svcs.getActor(alloc, id);
errdefer util.deepFree(alloc, actor);
if (!Uuid.eql(actor.community_id, ctx.community.id) and ctx.userId() == null) {
return error.NotFound;
}
const avatar_url = if (actor.avatar_file_id) |fid|
try std.fmt.allocPrint(
alloc,
"{s}://{s}/media/{}",
.{ @tagName(ctx.community.scheme), ctx.community.host, fid },
)
else
try std.fmt.allocPrint(
alloc,
"{s}://{s}/{s}",
.{ @tagName(ctx.community.scheme), ctx.community.host, default_avatar },
);
errdefer alloc.free(avatar_url);
const header_url = if (actor.header_file_id) |fid|
try std.fmt.allocPrint(
alloc,
"{s}://{s}/media/{}",
.{ @tagName(ctx.community.scheme), ctx.community.host, fid },
)
else
null;
errdefer alloc.free(header_url);
return pkg.Actor{
.id = actor.id,
.username = actor.username,
.host = actor.host,
.display_name = actor.display_name,
.bio = actor.bio,
.avatar_file_id = actor.avatar_file_id,
.avatar_url = avatar_url,
.header_file_id = actor.header_file_id,
.header_url = header_url,
.profile_fields = actor.profile_fields,
.community_id = actor.community_id,
.created_at = actor.created_at,
.updated_at = actor.updated_at,
};
}
pub fn updateProfile(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
id: Uuid,
data: pkg.actors.ProfileUpdateArgs,
) !void {
if (!Uuid.eql(id, ctx.userId() orelse return error.NoToken)) return error.AccessDenied;
try svcs.updateActorProfile(alloc, id, data);
}

View file

@ -1,165 +1,156 @@
const std = @import("std");
const util = @import("util");
const pkg = @import("../lib.zig");
const services = @import("../services.zig");
const invites = @import("./invites.zig");
const types = @import("../types.zig");
const Allocator = std.mem.Allocator;
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const ApiContext = pkg.ApiContext;
const Token = pkg.tokens.Token;
const RegistrationOptions = pkg.auth.RegistrationOptions;
const Invite = @import("../lib.zig").Invite;
pub const Token = types.Token;
const Invite = services.invites.Invite;
pub fn register(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
opt: RegistrationOptions,
) !Uuid {
const tx = try svcs.beginTx();
errdefer tx.rollbackTx();
const maybe_invite = if (opt.invite_code) |code|
tx.getInviteByCode(alloc, code, ctx.community.id) catch |err| switch (err) {
error.NotFound => return error.InvalidInvite,
else => |e| return e,
}
else
null;
defer if (maybe_invite) |inv| util.deepFree(alloc, inv);
if (maybe_invite) |invite| {
if (!Uuid.eql(invite.community_id, ctx.community.id)) return error.WrongCommunity;
if (!invites.isValid(invite)) return error.InvalidInvite;
}
const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
if (ctx.community.kind == .admin) @panic("Unimplmented");
const account_id = try createLocalAccount(
alloc,
tx,
.{
.username = opt.username,
.password = opt.password,
.community_id = ctx.community.id,
.invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null,
.email = opt.email,
},
);
switch (invite_kind) {
.user => {},
.system => @panic("System user invites unimplemented"),
.community_owner => {
try tx.transferCommunityOwnership(ctx.community.id, account_id);
},
}
try tx.commitTx();
return account_id;
}
pub const AccountCreateArgs = struct {
username: []const u8,
password: []const u8,
community_id: Uuid,
invite_id: ?Uuid = null,
pub const RegistrationOptions = struct {
invite_code: ?[]const u8 = null,
email: ?[]const u8 = null,
role: services.accounts.Role = .user,
};
pub fn createLocalAccount(
alloc: std.mem.Allocator,
svcs: anytype,
args: AccountCreateArgs,
) !Uuid {
const tx = try svcs.beginTx();
errdefer tx.rollbackTx();
pub const AccountCreateOptions = @import("../services/accounts.zig").CreateOptions;
const hash = try hashPassword(args.password, alloc);
defer alloc.free(hash);
pub fn methods(comptime models: type) type {
return struct {
fn isInviteValid(invite: Invite) bool {
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false;
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false;
return true;
}
pub fn register(self: anytype, username: []const u8, password: []const u8, opt: RegistrationOptions) !types.Actor {
const tx = try self.db.beginOrSavepoint();
const maybe_invite = if (opt.invite_code) |code|
try models.invites.getByCode(tx, code, self.context.community.id, self.allocator)
else
null;
defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv);
const id = try tx.createActor(alloc, args.username, args.community_id, false);
try tx.createAccount(alloc, .{
.for_actor = id,
.password_hash = hash,
.invite_id = args.invite_id,
.email = args.email,
.role = args.role,
});
if (maybe_invite) |invite| {
if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity;
if (!isInviteValid(invite)) return error.InvalidInvite;
}
try tx.commitTx();
const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
return id;
}
if (self.context.community.kind == .admin) @panic("Unimplmented");
pub fn verifyToken(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, token: []const u8) !Token.Info {
const hash = try hashToken(token, alloc);
defer alloc.free(hash);
const user_id = try createLocalAccount(
tx,
username,
password,
self.context.community.id,
.{
.invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null,
.email = opt.email,
},
self.allocator,
);
const info = try svcs.getTokenByHash(alloc, hash, ctx.community.id);
defer util.deepFree(alloc, info);
switch (invite_kind) {
.user => {},
.system => @panic("System user invites unimplemented"),
.community_owner => {
try models.communities.transferOwnership(tx, self.context.community.id, user_id);
},
}
return .{ .account_id = info.account_id, .issued_at = info.issued_at };
}
const user = models.actors.get(tx, user_id, self.allocator) catch |err| switch (err) {
error.NotFound => return error.Unexpected,
else => |e| return e,
};
errdefer util.deepFree(self.allocator, user);
pub fn login(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
username: []const u8,
password: []const u8,
) !Token {
const community_id = ctx.community.id;
const credentials = try svcs.getCredentialsByUsername(
alloc,
username,
community_id,
);
defer util.deepFree(alloc, credentials);
try tx.commitOrRelease();
return user;
}
try verifyPassword(credentials.password_hash, password, alloc);
// Only for internal use
pub fn createLocalAccount(
db: anytype,
username: []const u8,
password: []const u8,
community_id: Uuid,
opt: AccountCreateOptions,
alloc: std.mem.Allocator,
) !Uuid {
const tx = try db.beginOrSavepoint();
errdefer tx.rollback();
const token = try generateToken(alloc);
errdefer util.deepFree(alloc, token);
const token_hash = hashToken(token, alloc) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => unreachable,
};
defer util.deepFree(alloc, token_hash);
const hash = try hashPassword(password, alloc);
defer alloc.free(hash);
const tx = try svcs.beginTx();
errdefer tx.rollbackTx();
const id = try models.actors.create(tx, username, community_id, false, alloc);
try models.accounts.create(tx, id, hash, opt, alloc);
// ensure that the password has not changed in the meantime
{
const updated_info = try tx.getCredentialsByUsername(
alloc,
username,
community_id,
);
defer util.deepFree(alloc, updated_info);
try tx.commitOrRelease();
if (!std.mem.eql(u8, credentials.password_hash, updated_info.password_hash)) return error.InvalidLogin;
}
return id;
}
try tx.createToken(alloc, credentials.account_id, token_hash);
pub fn login(self: anytype, username: []const u8, password: []const u8) !Token {
const community_id = self.context.community.id;
const credentials = try models.accounts.getCredentialsByUsername(
self.db,
username,
community_id,
self.allocator,
);
defer util.deepFree(self.allocator, credentials);
try tx.commitTx();
const info = try svcs.getTokenByHash(alloc, token_hash, community_id);
defer util.deepFree(alloc, info);
try verifyPassword(credentials.password_hash, password, self.allocator);
return .{
.value = token,
.info = .{
.account_id = info.account_id,
.issued_at = info.issued_at,
},
const token = try generateToken(self.allocator);
errdefer util.deepFree(self.allocator, token);
const token_hash = hashToken(token, self.allocator) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => unreachable,
};
defer util.deepFree(self.allocator, token_hash);
const tx = try self.db.begin();
errdefer tx.rollback();
// ensure that the password has not changed in the meantime
{
const updated_info = try models.accounts.getCredentialsByUsername(
tx,
username,
community_id,
self.allocator,
);
defer util.deepFree(self.allocator, updated_info);
if (!std.mem.eql(u8, credentials.password_hash, updated_info.password_hash)) return error.InvalidLogin;
}
try models.tokens.create(tx, credentials.account_id, token_hash, self.allocator);
try tx.commit();
const info = try models.tokens.getByHash(self.db, token_hash, community_id, self.allocator);
defer util.deepFree(self.allocator, info);
return .{
.value = token,
.info = .{
.user_id = info.account_id,
.issued_at = info.issued_at,
},
};
}
pub fn verifyToken(self: anytype, token: []const u8) !Token.Info {
const hash = try hashToken(token, self.allocator);
defer self.allocator.free(hash);
const info = try models.tokens.getByHash(self.db, hash, self.context.community.id, self.allocator);
defer util.deepFree(self.allocator, info);
return .{ .user_id = info.account_id, .issued_at = info.issued_at };
}
};
}
@ -168,7 +159,7 @@ pub fn login(
// password hashing.
// Attempting to calculate/verify a hash will use about 50mb of work space.
const scrypt = std.crypto.pwhash.scrypt;
const max_password_hash_len = 128;
const password_hash_len = 128;
fn verifyPassword(
hash: []const u8,
password: []const u8,
@ -179,33 +170,23 @@ fn verifyPassword(
password,
.{ .allocator = alloc },
) catch |err| return switch (err) {
error.PasswordVerificationFailed => return error.InvalidLogin,
error.OutOfMemory => return error.OutOfMemory,
else => |e| return e,
error.PasswordVerificationFailed => error.InvalidLogin,
else => error.HashFailure,
};
}
const scrypt_params = if (!@import("builtin").is_test)
scrypt.Params.interactive
else
scrypt.Params{
.ln = 8,
.r = 8,
.p = 1,
};
fn hashPassword(password: []const u8, alloc: std.mem.Allocator) ![]const u8 {
var buf: [max_password_hash_len]u8 = undefined;
const hash = try scrypt.strHash(
const buf = try alloc.alloc(u8, password_hash_len);
errdefer alloc.free(buf);
return scrypt.strHash(
password,
.{
.allocator = alloc,
.params = scrypt_params,
.params = scrypt.Params.interactive,
.encoding = .phc,
},
&buf,
);
return util.deepClone(alloc, hash);
buf,
) catch error.HashFailure;
}
/// A raw token is a sequence of N random bytes, base64 encoded.
@ -248,190 +229,99 @@ fn hashToken(token_b64: []const u8, alloc: std.mem.Allocator) ![]const u8 {
const hash_b64 = try alloc.alloc(u8, hash_b64_len);
return Base64Encoder.encode(hash_b64, &hash);
}
const TestDb = struct {
tx_level: usize = 0,
rolled_back: bool = false,
committed: bool = false,
fn beginOrSavepoint(self: *TestDb) !*TestDb {
self.tx_level += 1;
return self;
}
fn rollback(self: *TestDb) void {
self.rolled_back = true;
self.tx_level -= 1;
}
fn commitOrRelease(self: *TestDb) !void {
self.committed = true;
self.tx_level -= 1;
}
};
test "register" {
const testCase = struct {
const test_invite_code = "xyz";
const test_invite_id = Uuid.parse("d24e7f2a-7e6e-4e2a-8e9d-987538a04a40") catch unreachable;
const test_acc_id = Uuid.parse("e8e21e1d-7b80-4e48-876d-9929326af511") catch unreachable;
const test_community_id = Uuid.parse("8bf88bd7-fb07-492d-a89a-6350c036183f") catch unreachable;
comptime var exp_code = "code";
comptime var exp_community = Uuid.parse("a210c035-c9e1-4361-82a2-aaeac8e40dc6") catch unreachable;
comptime var uid = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable;
const Args = struct {
username: []const u8 = "username",
password: []const u8 = "password1234",
const MockSvc = struct {
const invites = struct {
fn getByCode(db: *TestDb, code: []const u8, community_id: Uuid, alloc: std.mem.Allocator) !Invite {
try std.testing.expectEqual(db.tx_level, 1);
try std.testing.expectEqualStrings(exp_code, code);
try std.testing.expectEqual(exp_community, community_id);
use_invite: bool = false,
invite_community_id: Uuid = test_community_id,
invite_kind: services.invites.Kind = .user,
invite_max_uses: ?usize = null,
invite_current_uses: usize = 0,
invite_expires_at: ?DateTime = null,
return try util.deepClone(alloc, Invite{
.id = Uuid.parse("eac18f43-4dcc-489f-9fb5-4c1633e7b4e0") catch unreachable,
get_invite_error: ?anyerror = null,
create_account_error: ?anyerror = null,
create_actor_error: ?anyerror = null,
transfer_error: ?anyerror = null,
.created_by = Uuid.parse("6d951fcc-1c9f-497b-9c96-31dfb9873708") catch unreachable,
.community_id = exp_community,
.name = "test invite",
.code = exp_code,
expect_error: ?anyerror = null,
expect_transferred: bool = false,
};
.kind = .user,
fn runCaseOnce(allocator: std.mem.Allocator, test_args: Args) anyerror!void {
const Svc = struct {
test_args: Args,
tx_level: usize = 0,
rolled_back: bool = false,
committed: bool = false,
.created_at = DateTime.parse("2022-12-21T09:05:50Z") catch unreachable,
.times_used = 0,
account_created: bool = false,
actor_created: bool = false,
community_transferred: bool = false,
fn beginTx(self: *@This()) !*@This() {
self.tx_level += 1;
return self;
}
fn rollbackTx(self: *@This()) void {
self.tx_level -= 1;
self.rolled_back = true;
}
fn commitTx(self: *@This()) !void {
self.tx_level -= 1;
self.committed = true;
}
fn getInviteByCode(self: *@This(), alloc: Allocator, code: []const u8, community_id: Uuid) anyerror!services.invites.Invite {
try std.testing.expect(self.tx_level > 0);
try std.testing.expectEqualStrings(test_invite_code, code);
try std.testing.expectEqual(test_community_id, community_id);
if (self.test_args.get_invite_error) |err| return err;
return try util.deepClone(alloc, std.mem.zeroInit(services.invites.Invite, .{
.id = test_invite_id,
.community_id = self.test_args.invite_community_id,
.code = code,
.kind = self.test_args.invite_kind,
.times_used = self.test_args.invite_current_uses,
.max_uses = self.test_args.invite_max_uses,
.expires_at = self.test_args.invite_expires_at,
}));
}
fn createActor(self: *@This(), _: Allocator, username: []const u8, community_id: Uuid, _: bool) anyerror!Uuid {
try std.testing.expect(self.tx_level > 0);
if (self.test_args.create_actor_error) |err| return err;
try std.testing.expectEqualStrings(self.test_args.username, username);
try std.testing.expectEqual(test_community_id, community_id);
self.actor_created = true;
return test_acc_id;
}
fn createAccount(self: *@This(), alloc: Allocator, args: services.accounts.CreateArgs) anyerror!void {
try std.testing.expect(self.tx_level > 0);
if (self.test_args.create_account_error) |err| return err;
try verifyPassword(args.password_hash, self.test_args.password, alloc);
if (self.test_args.use_invite)
try std.testing.expectEqual(@as(?Uuid, test_invite_id), args.invite_id)
else
try std.testing.expect(args.invite_id == null);
try std.testing.expectEqual(services.accounts.Role.user, args.role);
self.account_created = true;
}
fn transferCommunityOwnership(self: *@This(), community_id: Uuid, account_id: Uuid) !void {
try std.testing.expect(self.tx_level > 0);
if (self.test_args.transfer_error) |err| return err;
self.community_transferred = true;
try std.testing.expectEqual(test_community_id, community_id);
try std.testing.expectEqual(test_acc_id, account_id);
}
};
var svc = Svc{ .test_args = test_args };
const community = std.mem.zeroInit(pkg.Community, .{ .kind = .local, .id = test_community_id });
const result = register(
allocator,
.{ .community = community },
&svc,
.{
.username = test_args.username,
.password = test_args.password,
.invite_code = if (test_args.use_invite) test_invite_code else null,
},
// shortcut out of memory errors to test allocation
) catch |err| if (err == error.OutOfMemory) return err else err;
if (test_args.expect_error) |err| {
try std.testing.expectError(err, result);
try std.testing.expect(!svc.committed);
if (svc.account_created or svc.actor_created or svc.community_transferred) {
try std.testing.expect(svc.rolled_back);
}
} else {
try std.testing.expectEqual(test_acc_id, try result);
try std.testing.expect(svc.committed);
try std.testing.expect(!svc.rolled_back);
try std.testing.expect(svc.account_created);
try std.testing.expect(svc.actor_created);
try std.testing.expectEqual(test_args.expect_transferred, svc.community_transferred);
.expires_at = null,
.max_uses = null,
});
}
}
};
const auth = struct {
fn register(
db: *TestDb,
username: []const u8,
password: []const u8,
community_id: Uuid,
_: AccountCreateOptions,
_: std.mem.Allocator,
) anyerror!Uuid {
try std.testing.expectEqual(db.tx_level, 1);
try std.testing.expectEqualStrings("root", username);
try std.testing.expectEqualStrings("password", password);
try std.testing.expectEqual(exp_community, community_id);
fn case(args: Args) !void {
try std.testing.checkAllAllocationFailures(std.testing.allocator, runCaseOnce, .{args});
}
}.case;
return uid;
}
};
const actors = struct {
fn get(_: *TestDb, id: Uuid, alloc: std.mem.Allocator) anyerror!types.Actor {
try std.testing.expectEqual(uid, id);
return try util.deepClone(alloc, std.mem.zeroInit(types.Actor, .{
.id = id,
.username = "root",
.host = "example.com",
.community_id = exp_community,
}));
}
};
const communities = struct {
fn transferOwnership(_: *TestDb, _: Uuid, _: Uuid) anyerror!void {}
};
};
// regular registration
try testCase(.{});
// registration with invite
try testCase(.{ .use_invite = true });
// registration with invite for a different community
try testCase(.{
.invite_community_id = Uuid.parse("11111111-1111-1111-1111-111111111111") catch unreachable,
.use_invite = true,
.expect_error = error.WrongCommunity,
});
// registration as a new community owner
try testCase(.{
.use_invite = true,
.invite_kind = .community_owner,
.expect_transferred = true,
});
// invite with expiration info
try testCase(.{
.use_invite = true,
.invite_max_uses = 100,
.invite_current_uses = 10,
.invite_expires_at = DateTime{ .seconds_since_epoch = DateTime.test_now_timestamp + 3600 },
});
// missing invite
try testCase(.{
.use_invite = true,
.get_invite_error = error.NotFound,
.expect_error = error.InvalidInvite,
});
// expired invite
try testCase(.{
.use_invite = true,
.invite_expires_at = DateTime{ .seconds_since_epoch = DateTime.test_now_timestamp - 3600 },
.expect_error = error.InvalidInvite,
});
// used invite
try testCase(.{
.use_invite = true,
.invite_max_uses = 100,
.invite_current_uses = 110,
.expect_error = error.InvalidInvite,
});
var db = TestDb{};
util.deepFree(std.testing.allocator, try methods(MockSvc).register(.{
.db = &db,
.allocator = std.testing.allocator,
.community = .{
.id = exp_community,
.kind = .local,
},
}, "root", "password", .{}));
try std.testing.expectEqual(false, db.rolled_back);
try std.testing.expectEqual(true, db.committed);
try std.testing.expectEqual(@as(usize, 0), db.tx_level);
}

View file

@ -1,48 +0,0 @@
const std = @import("std");
const util = @import("util");
const types = @import("../types.zig");
const pkg = @import("../lib.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const ApiContext = pkg.ApiContext;
const Community = types.communities.Community;
const QueryArgs = types.communities.QueryArgs;
const QueryResult = types.communities.QueryResult;
pub fn create(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
origin: []const u8,
name: ?[]const u8,
) !Uuid {
if (!ctx.isAdmin()) {
return error.PermissionDenied;
}
return try svcs.createCommunity(
alloc,
origin,
.{ .name = name },
);
}
pub fn get(
alloc: std.mem.Allocator,
_: ApiContext,
svcs: anytype,
id: Uuid,
) !Community {
return try svcs.getCommunity(alloc, id);
}
pub fn query(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
args: QueryArgs,
) !QueryResult {
if (!ctx.isAdmin()) return error.PermissionDenied;
return try svcs.queryCommunities(alloc, args);
}

View file

@ -1,206 +0,0 @@
const std = @import("std");
const util = @import("util");
const pkg = @import("../lib.zig");
const services = @import("../services.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const ApiContext = pkg.ApiContext;
const DriveEntry = pkg.drive.DriveEntry;
pub fn upload(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
args: pkg.drive.UploadArgs,
body: []const u8,
) !Uuid {
const owner = ctx.userId() orelse return error.NoToken;
const file_id = try svcs.createFile(alloc, owner, .{
.filename = args.filename,
.description = args.description,
.content_type = args.content_type,
.sensitive = args.sensitive,
}, body);
const entry_id = entry: {
errdefer svcs.deleteFile(alloc, file_id) catch |err| {
std.log.err("Unable to delete file {}: {}", .{ file_id, err });
};
break :entry svcs.createDriveEntry(
alloc,
owner,
args.dir,
args.filename,
file_id,
) catch |err| switch (err) {
error.PathAlreadyExists => {
var buf: [256]u8 = undefined;
var split = std.mem.splitBackwards(u8, args.filename, ".");
const ext = split.first();
const name = split.rest();
const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext });
break :entry try svcs.createDriveEntry(
alloc,
owner,
args.dir,
new_name,
file_id,
);
},
else => |e| return e,
};
};
return entry_id;
}
pub fn mkdir(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
parent_path: []const u8,
name: []const u8,
) !Uuid {
const user_id = ctx.userId() orelse return error.NoToken;
return try svcs.createDriveEntry(alloc, user_id, parent_path, name, null);
}
pub fn delete(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
path: []const u8,
) !void {
const user_id = ctx.userId() orelse return error.NoToken;
const entry = try svcs.statDriveEntry(alloc, user_id, path);
defer util.deepFree(alloc, entry);
try svcs.deleteDriveEntry(alloc, entry.id);
if (entry.file_id) |file_id| try svcs.deleteFile(alloc, file_id);
}
pub fn move(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
src: []const u8,
dest: []const u8,
) !void {
const user_id = ctx.userId() orelse return error.NoToken;
try svcs.moveDriveEntry(alloc, user_id, src, dest);
}
pub fn get(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
path: []const u8,
) !pkg.drive.DriveEntry {
const user_id = ctx.userId() orelse return error.NoToken;
const entry = try svcs.statDriveEntry(alloc, user_id, path);
defer util.deepFree(alloc, entry);
return try convert(alloc, svcs, entry, true);
}
pub fn getById(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
id: Uuid,
) !pkg.drive.DriveEntry {
const user_id = ctx.userId() orelse return error.NoToken;
const entry = try svcs.getDriveEntry(alloc, id);
defer util.deepFree(alloc, entry);
if (!Uuid.eql(entry.owner_id, user_id)) return error.NotFound;
return try convert(alloc, svcs, entry, true);
}
// TODO: These next two functions are more about files than drive entries, consider refactor?
pub fn update(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
path: []const u8,
meta: pkg.files.UpdateArgs,
) !void {
const user_id = ctx.userId() orelse return error.NoToken;
const entry = try svcs.statDriveEntry(alloc, user_id, path);
defer util.deepFree(alloc, entry);
try svcs.updateFileMetadata(alloc, entry.file_id orelse return error.NotAFile, meta);
}
pub fn dereference(
alloc: std.mem.Allocator,
_: ApiContext,
svcs: anytype,
file_id: Uuid,
) !pkg.files.DerefResult {
const meta = try svcs.statFile(alloc, file_id);
errdefer util.deepFree(alloc, meta);
return .{
.meta = meta,
.data = try svcs.derefFile(alloc, file_id),
};
}
fn convert(
alloc: std.mem.Allocator,
svcs: anytype,
entry: services.drive.DriveEntry,
recurse: bool,
) !DriveEntry {
if (entry.file_id) |file_id| return .{
.file = .{
.id = entry.id,
.owner_id = entry.owner_id,
.name = entry.name,
.path = entry.path,
.parent_directory_id = entry.parent_directory_id,
.meta = try svcs.statFile(alloc, file_id),
},
} else return .{
.dir = .{
.id = entry.id,
.owner_id = entry.owner_id,
.name = entry.name,
.path = entry.path,
.parent_directory_id = entry.parent_directory_id,
.children = blk: {
if (!recurse) break :blk null;
const children = try svcs.listDriveEntry(alloc, entry.id);
const result = alloc.alloc(DriveEntry, children.len) catch |err| {
util.deepFree(alloc, children);
return err;
};
var count: usize = 0;
errdefer for (children) |child, i| {
if (i < count)
util.deepFree(alloc, result[i])
else
util.deepFree(alloc, child);
};
defer alloc.free(children);
errdefer alloc.free(result);
for (children) |child, i| {
result[i] = try convert(alloc, svcs, child, false);
count += 1;
}
break :blk result;
},
},
};
}

View file

@ -1,83 +0,0 @@
const std = @import("std");
const util = @import("util");
const pkg = @import("../lib.zig");
const services = @import("../services.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const ApiContext = pkg.ApiContext;
const QueryArgs = services.follows.QueryArgs;
const FollowerQueryArgs = pkg.follows.FollowerQueryArgs;
const FollowingQueryArgs = pkg.follows.FollowingQueryArgs;
pub fn follow(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
followee: Uuid,
) !void {
const user_id = ctx.userId() orelse return error.NoToken;
try svcs.createFollow(alloc, user_id, followee);
}
pub fn unfollow(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
followee: Uuid,
) !void {
const user_id = ctx.userId() orelse return error.NoToken;
try svcs.deleteFollow(alloc, user_id, followee);
}
pub fn queryFollowers(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
of: Uuid,
args: FollowerQueryArgs,
) !pkg.follows.FollowerQueryResult {
const user = try svcs.getActor(alloc, of);
defer util.deepFree(alloc, user);
if (!Uuid.eql(user.community_id, ctx.community.id) and ctx.userId() == null) return error.NotFound;
var all_args = std.mem.zeroInit(QueryArgs, args);
all_args.followee_id = of;
const result = try svcs.queryFollows(alloc, all_args);
return .{
.items = result.items,
.prev_page = convert(FollowerQueryArgs, result.prev_page),
.next_page = convert(FollowerQueryArgs, result.next_page),
};
}
pub fn queryFollowing(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
of: Uuid,
args: FollowingQueryArgs,
) !pkg.follows.FollowingQueryResult {
const user = try svcs.getActor(alloc, of);
defer util.deepFree(alloc, user);
if (!Uuid.eql(user.community_id, ctx.community.id) and ctx.userId() == null) return error.NotFound;
var all_args = std.mem.zeroInit(QueryArgs, args);
all_args.followed_by_id = of;
const result = try svcs.queryFollows(alloc, all_args);
return .{
.items = result.items,
.prev_page = convert(FollowingQueryArgs, result.prev_page),
.next_page = convert(FollowingQueryArgs, result.next_page),
};
}
fn convert(comptime T: type, args: QueryArgs) T {
return .{
.max_items = args.max_items,
.order_by = args.order_by,
.direction = args.direction,
.prev = args.prev,
.page_direction = args.page_direction,
};
}

View file

@ -1,107 +0,0 @@
const std = @import("std");
const util = @import("util");
const services = @import("../services.zig");
const pkg = @import("../lib.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const ApiContext = pkg.ApiContext;
const CreateOptions = pkg.invites.CreateOptions;
const Invite = pkg.invites.Invite;
pub fn create(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
options: CreateOptions,
) !Uuid {
// Only logged in users can make invites
const user_id = ctx.userId() orelse return error.TokenRequired;
const community_id = if (options.to_community) |id| blk: {
// Only admins can send invites for other communities
if (!ctx.isAdmin()) return error.PermissionDenied;
break :blk id;
} else ctx.community.id;
// Users can only make user invites
if (options.kind != .user and !ctx.isAdmin()) return error.PermissionDenied;
return try svcs.createInvite(alloc, .{
.created_by = user_id,
.community_id = community_id,
.name = options.name,
.lifespan = options.lifespan,
.max_uses = options.max_uses,
.kind = options.kind,
});
}
pub fn isValid(invite: services.invites.Invite) bool {
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false;
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false;
return true;
}
fn getInviteImpl(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
invite: services.invites.Invite,
) !Invite {
errdefer util.deepFree(alloc, invite);
if (!Uuid.eql(invite.community_id, ctx.community.id) and !ctx.isAdmin()) return error.NotFound;
if (!isValid(invite)) return error.NotFound;
const community = try svcs.getCommunity(alloc, invite.community_id);
defer util.deepFree(alloc, community);
const url = try std.fmt.allocPrint(
alloc,
"{s}://{s}/invite/{s}",
.{ @tagName(community.scheme), community.host, invite.code },
);
errdefer util.deepFree(alloc, url);
return Invite{
.id = invite.id,
.created_by = invite.created_by,
.community_id = invite.community_id,
.name = invite.name,
.code = invite.code,
.url = url,
.created_at = invite.created_at,
.times_used = invite.times_used,
.expires_at = invite.expires_at,
.max_uses = invite.max_uses,
.kind = invite.kind,
};
}
pub fn get(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
id: Uuid,
) !Invite {
const invite = try svcs.getInvite(alloc, id);
return getInviteImpl(alloc, ctx, svcs, invite);
}
pub fn getByCode(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
code: []const u8,
) !Invite {
const invite = try svcs.getInviteByCode(alloc, code, ctx.community.id);
return getInviteImpl(alloc, ctx, svcs, invite);
}

View file

@ -1,38 +0,0 @@
const std = @import("std");
const util = @import("util");
const services = @import("../services.zig");
const pkg = @import("../lib.zig");
const Uuid = util.Uuid;
const ApiContext = pkg.ApiContext;
pub fn create(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
content: []const u8,
) !Uuid {
// You cannot post on admin accounts
if (ctx.community.kind == .admin) return error.WrongCommunity;
// Only authenticated users can post
const user_id = ctx.userId() orelse return error.TokenRequired;
return try svcs.createNote(alloc, user_id, content);
}
pub fn get(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
note_id: Uuid,
) !pkg.Note {
const note = try svcs.getNote(alloc, note_id);
errdefer util.deepFree(alloc, note);
// Only serve community-specific notes on unauthenticated requests
if (ctx.userId() == null) {
if (!Uuid.eql(ctx.community.id, note.author.community_id)) return error.NotFound;
}
return note;
}

View file

@ -1,80 +0,0 @@
const std = @import("std");
const util = @import("util");
const pkg = @import("../lib.zig");
const types = @import("../types.zig");
const ApiContext = pkg.ApiContext;
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const services = @import("../services.zig");
const Note = services.Note;
const QueryArgs = services.notes.QueryArgs;
const TimelineArgs = types.timelines.TimelineArgs;
fn timelineArgs(args: services.notes.QueryArgs) TimelineArgs {
return .{
.max_items = args.max_items,
.created_before = args.created_before,
.created_after = args.created_after,
.prev = args.prev,
.page_direction = args.page_direction,
};
}
pub const TimelineResult = struct {
items: []Note,
prev_page: TimelineArgs,
next_page: TimelineArgs,
};
pub fn globalTimeline(
alloc: std.mem.Allocator,
_: ApiContext,
svcs: anytype,
args: TimelineArgs,
) !TimelineResult {
const all_args = std.mem.zeroInit(QueryArgs, args);
const result = try svcs.queryNotes(alloc, all_args);
return TimelineResult{
.items = result.items,
.prev_page = timelineArgs(result.prev_page),
.next_page = timelineArgs(result.next_page),
};
}
pub fn localTimeline(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
args: TimelineArgs,
) !TimelineResult {
var all_args = std.mem.zeroInit(QueryArgs, args);
all_args.community_id = ctx.community.id;
const result = try svcs.queryNotes(alloc, all_args);
return TimelineResult{
.items = result.items,
.prev_page = timelineArgs(result.prev_page),
.next_page = timelineArgs(result.next_page),
};
}
pub fn homeTimeline(
alloc: std.mem.Allocator,
ctx: ApiContext,
svcs: anytype,
args: TimelineArgs,
) !TimelineResult {
if (ctx.userId() == null) return error.NoToken;
var all_args = std.mem.zeroInit(QueryArgs, args);
all_args.followed_by = ctx.userId();
const result = try svcs.queryNotes(alloc, all_args);
return TimelineResult{
.items = result.items,
.prev_page = timelineArgs(result.prev_page),
.next_page = timelineArgs(result.next_page),
};
}

View file

@ -1,338 +0,0 @@
const std = @import("std");
const util = @import("util");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const impl = struct {
const communities = @import("./services/communities.zig");
const actors = @import("./services/actors.zig");
const drive = @import("./services/drive.zig");
const files = @import("./services/files.zig");
const invites = @import("./services/invites.zig");
const notes = @import("./services/notes.zig");
const follows = @import("./services/follows.zig");
const accounts = @import("./services/accounts.zig");
const tokens = @import("./services/tokens.zig");
};
const types = @import("./services/types.zig");
pub usingnamespace types;
pub const Account = types.accounts.Account;
pub const Credentials = types.accounts.Credentials;
pub const Actor = types.actors.Actor;
pub const Community = types.communities.Community;
pub const DriveEntry = types.drive.DriveEntry;
pub const FileUpload = types.files.FileUpload;
pub const Invite = types.invites.Invite;
pub const Note = types.notes.Note;
pub const Token = types.tokens.Token;
pub fn Services(comptime Db: type) type {
return struct {
const Self = @This();
db: Db,
pub fn beginTx(self: Self) !Services(Db.BeginOrSavepoint) {
return Services(Db.BeginOrSavepoint){
.db = try self.db.beginOrSavepoint(),
};
}
pub fn commitTx(self: Self) !void {
return try self.db.commitOrRelease();
}
pub fn rollbackTx(self: Self) void {
return self.db.rollback();
}
pub fn createAccount(
self: Self,
alloc: std.mem.Allocator,
args: types.accounts.CreateArgs,
) !void {
return try impl.accounts.create(self.db, args, alloc);
}
pub fn getCredentialsByUsername(
self: Self,
alloc: std.mem.Allocator,
username: []const u8,
community_id: Uuid,
) !Credentials {
return try impl.accounts.getCredentialsByUsername(self.db, username, community_id, alloc);
}
pub fn createActor(
self: Self,
alloc: std.mem.Allocator,
username: []const u8,
community_id: Uuid,
lax_username: bool, // TODO: remove this
) !Uuid {
return try impl.actors.create(self.db, username, community_id, lax_username, alloc);
}
pub fn getActor(
self: Self,
alloc: std.mem.Allocator,
user_id: Uuid,
) !Actor {
return try impl.actors.get(self.db, user_id, alloc);
}
pub fn lookupActorByUsername(
self: Self,
alloc: std.mem.Allocator,
username: []const u8,
community_id: Uuid,
) !Actor {
return try impl.actors.lookupByUsername(self.db, username, community_id, alloc);
}
pub fn updateActorProfile(
self: Self,
alloc: std.mem.Allocator,
actor_id: Uuid,
new: types.actors.ProfileUpdateArgs,
) !void {
return try impl.actors.updateProfile(self.db, actor_id, new, alloc);
}
pub fn createCommunity(
self: Self,
alloc: std.mem.Allocator,
origin: []const u8,
options: types.communities.CreateOptions,
) !Uuid {
return try impl.communities.create(self.db, origin, options, alloc);
}
pub fn getCommunity(
self: Self,
alloc: std.mem.Allocator,
id: Uuid,
) !Community {
return try impl.communities.get(self.db, id, alloc);
}
pub fn getCommunityByHost(
self: Self,
alloc: std.mem.Allocator,
host: []const u8,
) !Community {
return try impl.communities.getByHost(self.db, host, alloc);
}
pub fn getAdminCommunityId(self: Self) !Uuid {
return try impl.communities.adminCommunityId(self.db);
}
pub fn transferCommunityOwnership(self: Self, community_id: Uuid, owner_id: Uuid) !void {
return try impl.communities.transferOwnership(self.db, community_id, owner_id);
}
pub fn queryCommunities(
self: Self,
alloc: std.mem.Allocator,
args: types.communities.QueryArgs,
) !types.communities.QueryResult {
return try impl.communities.query(self.db, args, alloc);
}
pub fn statDriveEntry(
self: Self,
alloc: std.mem.Allocator,
owner_id: Uuid,
path: []const u8,
) !DriveEntry {
return try impl.drive.stat(self.db, owner_id, path, alloc);
}
pub fn getDriveEntry(
self: Self,
alloc: std.mem.Allocator,
id: Uuid,
) !DriveEntry {
return try impl.drive.get(self.db, id, alloc);
}
pub fn createDriveEntry(
self: Self,
alloc: std.mem.Allocator,
owner_id: Uuid,
containing_path: []const u8,
name: []const u8,
file_id: ?Uuid,
) !Uuid {
return try impl.drive.create(self.db, owner_id, containing_path, name, file_id, alloc);
}
pub fn deleteDriveEntry(
self: Self,
alloc: std.mem.Allocator,
entry_id: Uuid,
) !void {
return try impl.drive.delete(self.db, entry_id, alloc);
}
pub fn moveDriveEntry(
self: Self,
alloc: std.mem.Allocator,
owner_id: Uuid,
src: []const u8,
dest: []const u8,
) !void {
return try impl.drive.move(self.db, owner_id, src, dest, alloc);
}
// TODO: paginate
pub fn listDriveEntry(
self: Self,
alloc: std.mem.Allocator,
entry_id: Uuid,
) ![]DriveEntry {
return try impl.drive.list(self.db, entry_id, alloc);
}
pub fn createFile(
self: Self,
alloc: std.mem.Allocator,
owner_id: Uuid,
meta: types.files.CreateOptions,
data: []const u8,
) !Uuid {
return try impl.files.create(self.db, owner_id, meta, data, alloc);
}
pub fn deleteFile(
self: Self,
alloc: std.mem.Allocator,
id: Uuid,
) !void {
return try impl.files.delete(self.db, id, alloc);
}
pub fn statFile(
self: Self,
alloc: std.mem.Allocator,
id: Uuid,
) !FileUpload {
return try impl.files.get(self.db, id, alloc);
}
pub fn derefFile(
_: Self,
alloc: std.mem.Allocator,
id: Uuid,
) ![]const u8 {
return try impl.files.deref(alloc, id);
}
pub fn updateFileMetadata(
self: Self,
alloc: std.mem.Allocator,
id: Uuid,
meta: types.files.UpdateArgs,
) !void {
return try impl.files.update(self.db, id, meta, alloc);
}
pub fn createFollow(
self: Self,
alloc: std.mem.Allocator,
followed_by: Uuid,
followee: Uuid,
) !void {
return try impl.follows.create(self.db, followed_by, followee, alloc);
}
pub fn deleteFollow(
self: Self,
alloc: std.mem.Allocator,
followed_by: Uuid,
followee: Uuid,
) !void {
return try impl.follows.delete(self.db, followed_by, followee, alloc);
}
pub fn queryFollows(
self: Self,
alloc: std.mem.Allocator,
args: types.follows.QueryArgs,
) !types.follows.QueryResult {
return try impl.follows.query(self.db, args, alloc);
}
pub fn createInvite(
self: Self,
alloc: std.mem.Allocator,
options: types.invites.CreateOptions,
) !Uuid {
return try impl.invites.create(self.db, options, alloc);
}
pub fn getInvite(
self: Self,
alloc: std.mem.Allocator,
invite_id: Uuid,
) !Invite {
return try impl.invites.get(self.db, invite_id, alloc);
}
pub fn createNote(
self: Self,
alloc: std.mem.Allocator,
author_id: Uuid,
content: []const u8,
) !Uuid {
return try impl.notes.create(self.db, author_id, content, alloc);
}
pub fn getNote(
self: Self,
alloc: std.mem.Allocator,
id: Uuid,
) !Note {
return try impl.notes.get(self.db, id, alloc);
}
pub fn queryNotes(
self: Self,
alloc: std.mem.Allocator,
args: types.notes.QueryArgs,
) !types.notes.QueryResult {
return try impl.notes.query(self.db, args, alloc);
}
pub fn getInviteByCode(
self: Self,
alloc: std.mem.Allocator,
code: []const u8,
community_id: Uuid,
) !Invite {
return try impl.invites.getByCode(self.db, code, community_id, alloc);
}
pub fn createToken(
self: Self,
alloc: std.mem.Allocator,
account_id: Uuid,
hash: []const u8,
) !void {
return try impl.tokens.create(self.db, account_id, hash, alloc);
}
pub fn getTokenByHash(
self: Self,
alloc: std.mem.Allocator,
hash: []const u8,
community_id: Uuid,
) !Token {
return try impl.tokens.getByHash(self.db, hash, community_id, alloc);
}
};
}

View file

@ -1,37 +1,50 @@
const std = @import("std");
const util = @import("util");
const types = @import("./types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const CreateArgs = types.accounts.CreateArgs;
const Credentials = types.accounts.Credentials;
pub const Role = enum {
user,
admin,
};
pub const CreateOptions = struct {
invite_id: ?Uuid = null,
email: ?[]const u8 = null,
role: Role = .user,
};
/// Creates a local account with the given information
pub fn create(
db: anytype,
args: CreateArgs,
for_actor: Uuid,
password_hash: []const u8,
options: CreateOptions,
alloc: std.mem.Allocator,
) !void {
const tx = try db.beginOrSavepoint();
errdefer tx.rollback();
tx.insert("account", .{
.id = args.for_actor,
.invite_id = args.invite_id,
.email = args.email,
.kind = args.role,
.id = for_actor,
.invite_id = options.invite_id,
.email = options.email,
.kind = options.role,
}, alloc) catch return error.DatabaseFailure;
tx.insert("password", .{
.account_id = args.for_actor,
.hash = args.password_hash,
.account_id = for_actor,
.hash = password_hash,
.changed_at = DateTime.now(),
}, alloc) catch return error.DatabaseFailure;
tx.commitOrRelease() catch return error.DatabaseFailure;
}
pub const Credentials = struct {
account_id: Uuid,
password_hash: []const u8,
};
pub fn getCredentialsByUsername(db: anytype, username: []const u8, community_id: Uuid, alloc: std.mem.Allocator) !Credentials {
return db.queryRow(
Credentials,

View file

@ -3,11 +3,11 @@ const util = @import("util");
const sql = @import("sql");
const common = @import("./common.zig");
const files = @import("./files.zig");
const types = @import("./types.zig");
const types = @import("../types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
pub const Actor = types.actors.Actor;
const Actor = types.Actor;
pub const CreateError = error{
UsernameTaken,
@ -124,7 +124,7 @@ pub const max_fields = 32;
pub const max_display_name_len = 128;
pub const max_bio = 1 << 16;
pub fn updateProfile(db: anytype, id: Uuid, new: types.actors.ProfileUpdateArgs, alloc: std.mem.Allocator) !void {
pub fn updateProfile(db: anytype, id: Uuid, new: Actor.ProfileUpdateArgs, alloc: std.mem.Allocator) !void {
var builder = sql.QueryBuilder.init(alloc);
defer builder.deinit();

View file

@ -3,15 +3,14 @@ const builtin = @import("builtin");
const util = @import("util");
const sql = @import("sql");
const actors = @import("./actors.zig");
const types = @import("./types.zig");
const types = @import("../types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Community = types.communities.Community;
const Scheme = types.communities.Scheme;
const CreateOptions = types.communities.CreateOptions;
const QueryArgs = types.communities.QueryArgs;
const QueryResult = types.communities.QueryResult;
const Community = types.Community;
const CreateOptions = Community.CreateOptions;
const QueryArgs = Community.QueryArgs;
const QueryResult = types.QueryResult(Community);
pub const CreateError = error{
UnsupportedScheme,
@ -22,7 +21,7 @@ pub const CreateError = error{
pub fn create(db: anytype, origin: []const u8, options: CreateOptions, alloc: std.mem.Allocator) CreateError!Uuid {
const scheme_len = std.mem.indexOfScalar(u8, origin, ':') orelse return error.InvalidOrigin;
const scheme_str = origin[0..scheme_len];
const scheme = std.meta.stringToEnum(Scheme, scheme_str) orelse return error.UnsupportedScheme;
const scheme = std.meta.stringToEnum(Community.Scheme, scheme_str) orelse return error.UnsupportedScheme;
// host must be in the format "{scheme}://{host}"
if (origin.len <= scheme_len + ("://").len or

View file

@ -1,46 +1,48 @@
const std = @import("std");
const util = @import("util");
const sql = @import("sql");
const types = @import("./types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Entry = types.drive.DriveEntry;
fn doGetQuery(db: anytype, comptime clause: []const u8, args: anytype, alloc: std.mem.Allocator) !Entry {
const q = std.fmt.comptimePrint(
\\SELECT id, path, owner_id, name, file_id, kind, parent_directory_id
\\FROM drive_entry_path
\\WHERE {s}
\\LIMIT 1
,
.{clause},
);
pub const DriveOwner = union(enum) {
user_id: Uuid,
community_id: Uuid,
};
return db.queryRow(Entry, q, args, alloc) catch |err| switch (err) {
error.NoRows => return error.NotFound,
else => |e| return e,
};
}
pub const Entry = struct {
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
file_id: ?Uuid,
kind: Kind,
};
pub const Kind = enum {
dir,
file,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator) !Entry {
return try doGetQuery(
db,
"owner_id = $1 AND path = ('/' || $2)",
.{
owner,
std.mem.trim(u8, path, "/"),
},
alloc,
);
}
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !Entry {
return try doGetQuery(db, "id = $1", .{id}, alloc);
return (db.queryRow(Entry,
\\SELECT id, path, owner_id, name, file_id, kind, parent_directory_id
\\FROM drive_entry_path
\\WHERE owner_id = $1 AND path = ('/' || $2)
\\LIMIT 1
, .{
owner,
std.mem.trim(u8, path, "/"),
}, alloc) catch |err| switch (err) {
error.NoRows => return error.NotFound,
else => |e| return e,
});
}
/// Creates a file or directory
pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !Uuid {
pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !Entry {
if (name.len == 0) return error.EmptyName;
const id = Uuid.randV4(util.getThreadPrng());
@ -64,7 +66,18 @@ pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_
try tx.commit();
return id;
const path = try std.mem.join(alloc, "/", if (dir.len == 0) &.{ "", name } else &.{ "", dir, name });
errdefer alloc.free(path);
return Entry{
.id = id,
.owner_id = owner,
.name = try util.deepClone(alloc, name),
.path = path,
.parent_directory_id = parent.id,
.file_id = file_id,
.kind = if (file_id) |_| .file else .dir,
};
}
pub fn delete(db: anytype, id: Uuid, alloc: std.mem.Allocator) !void {

View file

@ -1,13 +1,11 @@
const std = @import("std");
const sql = @import("sql");
const util = @import("util");
const types = @import("./types.zig");
const types = @import("../types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const FileUpload = types.files.FileUpload;
const CreateOptions = types.files.CreateOptions;
const UpdateArgs = types.files.UpdateArgs;
const FileUpload = types.FileUpload;
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload {
return try db.queryRow(
@ -32,7 +30,7 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload {
);
}
pub fn update(db: anytype, id: Uuid, meta: UpdateArgs, alloc: std.mem.Allocator) !void {
pub fn update(db: anytype, id: Uuid, meta: FileUpload.UpdateArgs, alloc: std.mem.Allocator) !void {
var builder = sql.QueryBuilder.init(alloc);
defer builder.deinit();
@ -59,7 +57,7 @@ pub fn update(db: anytype, id: Uuid, meta: UpdateArgs, alloc: std.mem.Allocator)
}, alloc);
}
pub fn create(db: anytype, owner_id: Uuid, meta: CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid {
pub fn create(db: anytype, owner_id: Uuid, meta: FileUpload.CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid {
const id = Uuid.randV4(util.getThreadPrng());
const now = DateTime.now();
try db.insert("file_upload", .{

View file

@ -1,13 +1,20 @@
const std = @import("std");
const util = @import("util");
const sql = @import("sql");
const types = @import("./types.zig");
const common = @import("./common.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const QueryArgs = types.follows.QueryArgs;
const QueryResult = types.follows.QueryResult;
const Follow = types.follows.Follow;
pub const Follow = struct {
id: Uuid,
followed_by_id: Uuid,
followee_id: Uuid,
created_at: DateTime,
};
pub fn create(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.mem.Allocator) !void {
if (Uuid.eql(followed_by_id, followee_id)) return error.SelfFollow;
@ -39,6 +46,41 @@ pub fn delete(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.m
const max_max_items = 100;
pub const QueryArgs = struct {
pub const Direction = common.Direction;
pub const PageDirection = common.PageDirection;
pub const Prev = std.meta.Child(std.meta.fieldInfo(@This(), .prev).field_type);
pub const OrderBy = enum {
created_at,
};
max_items: usize = 20,
followed_by_id: ?Uuid = null,
followee_id: ?Uuid = null,
order_by: OrderBy = .created_at,
direction: Direction = .descending,
prev: ?struct {
id: Uuid,
order_val: union(OrderBy) {
created_at: DateTime,
},
} = null,
page_direction: PageDirection = .forward,
};
pub const QueryResult = struct {
items: []Follow,
prev_page: QueryArgs,
next_page: QueryArgs,
};
pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult {
var builder = sql.QueryBuilder.init(alloc);
defer builder.deinit();

View file

@ -1,11 +1,11 @@
const std = @import("std");
const builtin = @import("builtin");
const util = @import("util");
const types = @import("./types.zig");
const types = @import("../types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Invite = types.invites.Invite;
const Invite = types.Invite;
// 9 random bytes = 12 random b64
const rand_len = 8;
@ -16,7 +16,10 @@ const Decoder = std.base64.url_safe.Decoder;
pub fn create(
db: anytype,
options: types.invites.CreateOptions,
created_by: Uuid,
community_id: Uuid,
name: []const u8,
options: Invite.InternalCreateOptions,
alloc: std.mem.Allocator,
) !Uuid {
const id = Uuid.randV4(util.getThreadPrng());
@ -35,9 +38,9 @@ pub fn create(
.{
.id = id,
.created_by = options.created_by,
.community_id = options.community_id,
.name = options.name,
.created_by = created_by,
.community_id = community_id,
.name = name,
.code = code,
.max_uses = options.max_uses,

View file

@ -1,13 +1,14 @@
const std = @import("std");
const util = @import("util");
const sql = @import("sql");
const types = @import("./types.zig");
const common = @import("./common.zig");
const types = @import("../types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Note = types.notes.Note;
const QueryArgs = types.notes.QueryArgs;
const QueryResult = types.notes.QueryResult;
const Note = types.Note;
const QueryArgs = Note.QueryArgs;
const QueryResult = types.QueryResult(Note);
pub const CreateError = error{
DatabaseFailure,

View file

@ -4,7 +4,12 @@ const util = @import("util");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Token = @import("./types.zig").tokens.Token;
pub const Token = struct {
account_id: Uuid,
issued_at: DateTime,
hash: []const u8,
};
pub fn create(db: anytype, account_id: Uuid, hash: []const u8, alloc: std.mem.Allocator) !void {
const now = DateTime.now();

View file

@ -1,370 +0,0 @@
const util = @import("util");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
const common = struct {
const Direction = enum {
ascending,
descending,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
const PageDirection = enum {
forward,
backward,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
fn QueryResult(comptime R: type, comptime A: type) type {
return struct {
items: []R,
next_page: A,
prev_page: A,
};
}
};
pub const accounts = struct {
pub const Role = enum {
user,
admin,
};
pub const CreateArgs = struct {
for_actor: Uuid,
password_hash: []const u8,
invite_id: ?Uuid = null,
email: ?[]const u8 = null,
role: Role = .user,
};
pub const Credentials = struct {
account_id: Uuid,
password_hash: []const u8,
};
};
pub const actors = struct {
pub const Actor = struct {
id: Uuid,
username: []const u8,
host: []const u8,
community_id: Uuid,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: []const ProfileField,
created_at: DateTime,
updated_at: DateTime,
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
pub const ProfileField = struct {
key: []const u8,
value: []const u8,
};
// TODO: get rid of this
pub const Profile = struct {
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: []const ProfileField,
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
pub const ProfileUpdateArgs = struct {
display_name: ??[]const u8,
bio: ?[]const u8,
avatar_file_id: ??Uuid,
header_file_id: ??Uuid,
profile_fields: ?[]const ProfileField,
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
};
pub const communities = struct {
pub const Community = struct {
id: Uuid,
owner_id: ?Uuid,
host: []const u8,
name: []const u8,
scheme: Scheme,
kind: Kind,
created_at: DateTime,
};
pub const Kind = enum {
admin,
local,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const Scheme = enum {
https,
http,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const CreateOptions = struct {
name: ?[]const u8 = null,
kind: Kind = .local,
};
pub const QueryArgs = struct {
pub const OrderBy = enum {
name,
host,
created_at,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const Direction = common.Direction;
pub const PageDirection = common.PageDirection;
pub const Prev = struct {
id: Uuid,
order_val: OrderVal,
};
pub const OrderVal = union(OrderBy) {
name: []const u8,
host: []const u8,
created_at: DateTime,
};
// Max items to fetch
max_items: usize = 20,
// Selection filters
owner_id: ?Uuid = null, // searches for communities owned by this user
like: ?[]const u8 = null, // searches for communities with host or name LIKE '%?%'
created_before: ?DateTime = null,
created_after: ?DateTime = null,
// Ordering parameter
order_by: OrderBy = .created_at,
direction: Direction = .ascending,
// Page start parameter(s)
// This struct is a reference to the last value scanned
// If prev is present, then prev.order_val must have the same tag as order_by
// "prev" here refers to it being the previous value returned. It may be that
// prev refers to the item directly after the results you are about to recieve,
// if you are querying the previous page.
prev: ?Prev = null,
// What direction to scan the page window
// If "forward", then "prev" is interpreted as the item directly before the items
// to query, in the direction of "direction" above. If "backward", then the opposite
page_direction: PageDirection = .forward,
};
pub const QueryResult = common.QueryResult(Community, QueryArgs);
};
pub const drive = struct {
pub const DriveEntry = struct {
pub const Kind = enum {
dir,
file,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
file_id: ?Uuid,
kind: Kind,
};
};
pub const files = struct {
pub const FileUpload = struct {
pub const Status = enum {
uploading,
uploaded,
external,
deleted,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
id: Uuid,
owner_id: Uuid,
size: usize,
filename: []const u8,
description: ?[]const u8,
content_type: ?[]const u8,
sensitive: bool,
status: Status,
created_at: DateTime,
updated_at: DateTime,
};
pub const CreateOptions = struct {
filename: []const u8,
description: ?[]const u8,
content_type: ?[]const u8,
sensitive: bool,
};
pub const UpdateArgs = struct {
filename: ?[]const u8,
description: ?[]const u8,
content_type: ?[]const u8,
sensitive: ?bool,
};
};
pub const invites = struct {
pub const UseCount = usize;
pub const Invite = struct {
id: Uuid,
created_by: Uuid, // User ID
community_id: Uuid,
name: []const u8,
code: []const u8,
created_at: DateTime,
times_used: UseCount,
expires_at: ?DateTime,
max_uses: ?UseCount,
kind: Kind,
};
pub const Kind = enum {
system,
community_owner,
user,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const CreateOptions = struct {
created_by: Uuid,
community_id: Uuid,
name: ?[]const u8 = null,
max_uses: ?UseCount = null,
lifespan: ?DateTime.Duration = null,
kind: Kind = .user,
};
};
pub const follows = struct {
pub const Follow = struct {
id: Uuid,
followed_by_id: Uuid,
followee_id: Uuid,
created_at: DateTime,
};
pub const QueryArgs = struct {
pub const OrderBy = enum {
created_at,
};
pub const Direction = common.Direction;
pub const PageDirection = common.PageDirection;
pub const Prev = struct {
id: Uuid,
order_val: union(OrderBy) {
created_at: DateTime,
},
};
max_items: usize = 20,
followed_by_id: ?Uuid = null,
followee_id: ?Uuid = null,
order_by: OrderBy = .created_at,
direction: Direction = .descending,
prev: ?Prev = null,
page_direction: PageDirection = .forward,
};
pub const QueryResult = common.QueryResult(Follow, QueryArgs);
};
pub const notes = struct {
pub const Note = struct {
id: Uuid,
author: actors.Actor, // TODO
content: []const u8,
created_at: DateTime,
// TODO: This sucks
pub const sql_serialize = struct {
pub const @"author.profile_fields" = .json;
};
};
pub const QueryArgs = struct {
pub const PageDirection = common.PageDirection;
pub const Prev = struct {
id: Uuid,
created_at: DateTime,
};
max_items: usize = 20,
created_before: ?DateTime = null,
created_after: ?DateTime = null,
community_id: ?Uuid = null,
followed_by: ?Uuid = null,
prev: ?Prev = null,
page_direction: PageDirection = .forward,
};
pub const QueryResult = common.QueryResult(Note, QueryArgs);
};
pub const tokens = struct {
pub const Token = struct {
account_id: Uuid,
issued_at: DateTime,
hash: []const u8,
};
};

View file

@ -1,118 +1,314 @@
const util = @import("util");
const services = @import("./services.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
fn QueryResult(comptime R: type, comptime A: type) type {
const common = struct {
const Direction = enum {
ascending,
descending,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
const PageDirection = enum {
forward,
backward,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
};
pub fn QueryResult(comptime T: type) type {
return QueryResultArguments(T, T.QueryArgs);
}
pub fn QueryResultArguments(comptime T: type, comptime A: type) type {
return struct {
items: []R,
items: []T,
next_page: A,
prev_page: A,
};
}
pub const auth = struct {
pub const RegistrationOptions = struct {
username: []const u8,
password: []const u8,
invite_code: ?[]const u8 = null,
email: ?[]const u8 = null,
};
};
pub const actors = struct {
pub const Actor = struct {
id: Uuid,
username: []const u8,
host: []const u8,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
avatar_url: []const u8,
header_file_id: ?Uuid,
header_url: ?[]const u8,
profile_fields: []const ProfileField,
community_id: Uuid,
created_at: DateTime,
pub const Account = struct {
pub const Auth = struct {
password_hash: []const u8,
updated_at: DateTime,
};
pub const ProfileField = services.actors.ProfileField;
pub const ProfileUpdateArgs = services.actors.ProfileUpdateArgs;
};
pub const communities = struct {
pub const Community = services.communities.Community;
pub const QueryArgs = services.communities.QueryArgs;
pub const QueryResult = services.communities.QueryResult;
};
pub const drive = struct {
pub const DriveEntry = union(enum) {
pub const Kind = services.drive.DriveEntry.Kind;
dir: struct {
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
kind: Kind = .dir,
// If null = not enumerated
children: ?[]const DriveEntry,
},
file: struct {
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
kind: Kind = .file,
meta: files.FileUpload,
},
pub const Kind = enum {
user,
admin,
};
pub const UploadArgs = struct {
id: Uuid,
invite_id: ?Uuid,
email: ?[]const u8,
kind: Kind,
};
pub const Actor = struct {
pub const ProfileField = struct {
key: []const u8,
value: []const u8,
};
id: Uuid,
username: []const u8,
host: []const u8,
community_id: Uuid,
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: []const ProfileField,
created_at: DateTime,
updated_at: DateTime,
// TODO: get rid of this
pub const Profile = struct {
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: []const ProfileField,
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
pub const ProfileUpdateArgs = struct {
display_name: ??[]const u8,
bio: ?[]const u8,
avatar_file_id: ??Uuid,
header_file_id: ??Uuid,
profile_fields: ?[]const ProfileField,
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
pub const sql_serialize = struct {
pub const profile_fields = .json;
};
};
pub const Community = struct {
pub const Kind = enum {
admin,
local,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const Scheme = enum {
https,
http,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
id: Uuid,
owner_id: ?Uuid,
host: []const u8,
name: []const u8,
scheme: Scheme,
kind: Kind,
created_at: DateTime,
pub const CreateOptions = struct {
name: ?[]const u8 = null,
kind: Kind = .local,
};
pub const QueryArgs = struct {
pub const OrderBy = enum {
name,
host,
created_at,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
pub const Direction = common.Direction;
pub const PageDirection = common.PageDirection;
pub const Prev = struct {
id: Uuid,
order_val: OrderVal,
};
pub const OrderVal = union(OrderBy) {
name: []const u8,
host: []const u8,
created_at: DateTime,
};
// Max items to fetch
max_items: usize = 20,
// Selection filters
owner_id: ?Uuid = null, // searches for communities owned by this user
like: ?[]const u8 = null, // searches for communities with host or name LIKE '%?%'
created_before: ?DateTime = null,
created_after: ?DateTime = null,
// Ordering parameter
order_by: OrderBy = .created_at,
direction: Direction = .ascending,
// Page start parameter(s)
// This struct is a reference to the last value scanned
// If prev is present, then prev.order_val must have the same tag as order_by
// "prev" here refers to it being the previous value returned. It may be that
// prev refers to the item directly after the results you are about to recieve,
// if you are querying the previous page.
prev: ?Prev = null,
// What direction to scan the page window
// If "forward", then "prev" is interpreted as the item directly before the items
// to query, in the direction of "direction" above. If "backward", then the opposite
page_direction: PageDirection = .forward,
};
};
pub const DriveEntry = struct {
pub const Kind = enum {
dir,
file,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
id: Uuid,
owner_id: Uuid,
name: ?[]const u8,
path: []const u8,
parent_directory_id: ?Uuid,
file_id: ?Uuid,
kind: Kind,
};
pub const FileUpload = struct {
pub const Status = enum {
uploading,
uploaded,
external,
deleted,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
id: Uuid,
owner_id: Uuid,
size: usize,
filename: []const u8,
description: ?[]const u8,
content_type: ?[]const u8,
sensitive: bool,
status: Status,
created_at: DateTime,
updated_at: DateTime,
pub const CreateOptions = struct {
filename: []const u8,
dir: []const u8,
description: ?[]const u8,
content_type: []const u8,
content_type: ?[]const u8,
sensitive: bool,
};
};
pub const files = struct {
pub const FileUpload = services.files.FileUpload;
pub const UpdateArgs = services.files.UpdateArgs;
pub const DerefResult = struct {
meta: FileUpload,
data: []const u8,
pub const UpdateArgs = struct {
filename: ?[]const u8,
description: ?[]const u8,
content_type: ?[]const u8,
sensitive: ?bool,
};
};
pub const follows = struct {
pub const Follow = services.follows.Follow;
pub const Invite = struct {
const UseCount = usize;
const QueryArgs = struct {
pub const OrderBy = services.follows.QueryArgs.OrderBy;
pub const Direction = services.follows.QueryArgs.Direction;
pub const PageDirection = services.follows.QueryArgs.PageDirection;
pub const Prev = services.follows.QueryArgs.Prev;
pub const Kind = enum {
system,
community_owner,
user,
pub const jsonStringify = util.jsonSerializeEnumAsString;
};
id: Uuid,
created_by: Uuid, // User ID
community_id: Uuid,
name: []const u8,
code: []const u8,
created_at: DateTime,
times_used: UseCount,
expires_at: ?DateTime,
max_uses: ?UseCount,
kind: Kind,
pub const CreateOptions = struct {
name: ?[]const u8 = null,
max_uses: ?UseCount = null,
lifespan: ?DateTime.Duration = null,
kind: Kind = .user,
to_community: ?Uuid = null,
};
pub const InternalCreateOptions = struct {
name: ?[]const u8 = null,
max_uses: ?UseCount = null,
lifespan: ?DateTime.Duration = null,
kind: Kind = .user,
};
};
pub const Follow = struct {
id: Uuid,
followed_by_id: Uuid,
followee_id: Uuid,
created_at: DateTime,
pub const QueryArgs = struct {
pub const OrderBy = enum {
created_at,
};
pub const Direction = common.Direction;
pub const PageDirection = common.PageDirection;
pub const Prev = struct {
id: Uuid,
order_val: union(OrderBy) {
created_at: DateTime,
},
};
max_items: usize = 20,
followed_by_id: ?Uuid = null,
followee_id: ?Uuid = null,
order_by: OrderBy = .created_at,
direction: Direction = .descending,
@ -121,81 +317,46 @@ pub const follows = struct {
page_direction: PageDirection = .forward,
};
pub const FollowerQueryArgs = QueryArgs;
pub const FollowingQueryArgs = QueryArgs;
pub const FollowerQueryResult = QueryResult(Follow, FollowerQueryArgs);
pub const FollowingQueryResult = QueryResult(Follow, FollowingQueryArgs);
};
pub const invites = struct {
pub const UseCount = services.invites.UseCount;
pub const Invite = struct {
id: Uuid,
pub const Note = struct {
id: Uuid,
created_by: Uuid, // User ID
community_id: Uuid,
name: []const u8,
code: []const u8,
url: []const u8,
author: Actor,
content: []const u8,
created_at: DateTime,
created_at: DateTime,
times_used: UseCount,
expires_at: ?DateTime,
max_uses: ?UseCount,
kind: Kind,
};
pub const Kind = services.invites.Kind;
pub const CreateOptions = struct {
name: ?[]const u8 = null,
lifespan: ?DateTime.Duration = null,
max_uses: ?usize = null,
// admin only options
kind: Kind = .user,
to_community: ?Uuid = null,
};
};
pub const notes = struct {
pub const Note = services.notes.Note;
pub const QueryArgs = services.notes.QueryArgs;
};
pub const timelines = struct {
pub const TimelineArgs = struct {
pub const PageDirection = notes.QueryArgs.PageDirection;
pub const Prev = notes.QueryArgs.Prev;
pub const QueryArgs = struct {
pub const PageDirection = common.PageDirection;
pub const Prev = struct {
id: Uuid,
created_at: DateTime,
};
max_items: usize = 20,
created_before: ?DateTime = null,
created_after: ?DateTime = null,
community_id: ?Uuid = null,
followed_by: ?Uuid = null,
prev: ?Prev = null,
page_direction: PageDirection = .forward,
};
pub const TimelineResult = struct {
items: []notes.Note,
prev_page: TimelineArgs,
next_page: TimelineArgs,
// TODO: This sucks
pub const sql_serialize = struct {
pub const @"author.profile_fields" = .json;
};
};
pub const tokens = struct {
pub const Token = struct {
pub const Info = struct {
account_id: Uuid,
issued_at: DateTime,
};
value: []const u8,
info: Info,
pub const Token = struct {
pub const Info = struct {
user_id: Uuid,
issued_at: DateTime,
};
value: []const u8,
info: Info,
};

View file

@ -282,7 +282,7 @@ pub const Response = struct {
pub fn template(self: *Self, status_code: http.Status, srv: anytype, comptime templ: []const u8, data: anytype) !void {
try self.headers.put("Content-Type", "text/html");
const user = if (srv.context.userId()) |uid| try srv.getActor(uid) else null;
const user = if (srv.context.userId()) |uid| try srv.getUser(uid) else null;
defer util.deepFree(srv.allocator, user);
var stream = try self.open(status_code);

View file

@ -24,10 +24,8 @@ pub const verify_login = struct {
pub const path = "/auth/login";
pub fn handler(_: anytype, res: anytype, srv: anytype) !void {
if (srv.context.token_info) |token| {
return try res.json(.ok, token);
} else {
return try res.status(.unauthorized);
}
const info = try srv.verifyAuthorization();
try res.json(.ok, info);
}
};

View file

@ -2,7 +2,7 @@ const api = @import("api");
const util = @import("util");
const controller_utils = @import("../../controllers.zig").helpers;
const QueryArgs = api.communities.QueryArgs;
const QueryArgs = api.Community.QueryArgs;
pub const create = struct {
pub const method = .POST;
@ -25,9 +25,9 @@ pub const query = struct {
pub const path = "/communities";
pub const Query = struct {
const OrderBy = QueryArgs.OrderBy;
const Direction = QueryArgs.Direction;
const PageDirection = QueryArgs.PageDirection;
const OrderBy = api.Community.QueryArgs.OrderBy;
const Direction = api.Community.QueryArgs.Direction;
const PageDirection = api.Community.QueryArgs.PageDirection;
// Max items to fetch
max_items: usize = 20,
@ -80,7 +80,7 @@ pub const query = struct {
});
const convert = struct {
fn func(args: QueryArgs) Query {
fn func(args: api.Community.QueryArgs) Query {
return .{
.max_items = args.max_items,
.owner_id = args.owner_id,

View file

@ -98,14 +98,12 @@ pub const update = struct {
};
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
try srv.driveUpdate(req.args.path, .{
const result = try srv.driveUpdate(req.args.path, .{
.filename = req.body.meta.filename,
.description = req.body.meta.description,
.content_type = req.body.meta.content_type,
.sensitive = req.body.meta.sensitive,
});
const result = try srv.driveGet(req.args.path);
defer util.deepFree(srv.allocator, result);
try res.json(.ok, result);
}
@ -119,9 +117,7 @@ pub const move = struct {
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const destination = req.headers.get("Destination") orelse return error.NoDestination;
try srv.driveMove(req.args.path, destination);
const result = try srv.driveGet(req.args.path);
const result = try srv.driveMove(req.args.path, destination);
defer util.deepFree(srv.allocator, result);
try res.headers.put("Location", destination);

View file

@ -4,7 +4,7 @@ pub const create = struct {
pub const method = .POST;
pub const path = "/invites";
pub const Body = api.invites.CreateOptions;
pub const Body = api.InviteOptions;
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
// No need to free because it will be freed when the api conn

View file

@ -2,13 +2,11 @@ const std = @import("std");
const api = @import("api");
const controller_utils = @import("../../controllers.zig").helpers;
const TimelineArgs = api.timelines.TimelineArgs;
pub const global = struct {
pub const method = .GET;
pub const path = "/timelines/global";
pub const Query = TimelineArgs;
pub const Query = api.TimelineArgs;
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const results = try srv.globalTimeline(req.query);
@ -20,7 +18,7 @@ pub const local = struct {
pub const method = .GET;
pub const path = "/timelines/local";
pub const Query = TimelineArgs;
pub const Query = api.TimelineArgs;
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const results = try srv.localTimeline(req.query);
@ -32,7 +30,7 @@ pub const home = struct {
pub const method = .GET;
pub const path = "/timelines/home";
pub const Query = TimelineArgs;
pub const Query = api.TimelineArgs;
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const results = try srv.homeTimeline(req.query);

View file

@ -14,12 +14,10 @@ pub const create = struct {
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const options = .{
.username = req.body.username,
.password = req.body.password,
.invite_code = req.body.invite_code,
.email = req.body.email,
};
const user = srv.register(options) catch |err| switch (err) {
const user = srv.register(req.body.username, req.body.password, options) catch |err| switch (err) {
error.UsernameTaken => return res.err(.unprocessable_entity, "Username Unavailable", {}),
else => return err,
};
@ -37,7 +35,7 @@ pub const get = struct {
};
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const result = try srv.getActor(req.args.id);
const result = try srv.getUser(req.args.id);
defer util.deepFree(srv.allocator, result);
try res.json(.ok, result);
@ -52,13 +50,13 @@ pub const update_profile = struct {
id: util.Uuid,
};
pub const Body = api.actors.ProfileUpdateArgs;
pub const Body = api.Actor.ProfileUpdateArgs;
// TODO: I don't like that the request body and response body are substantially different
// TODO: I don't like that the request body dn response body are substantially different
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
try srv.updateUserProfile(req.args.id, req.body);
const result = try srv.getActor(req.args.id);
const result = try srv.getUser(req.args.id);
defer util.deepFree(srv.allocator, result);
try res.json(.ok, result);

View file

@ -42,7 +42,7 @@ pub const query_followers = struct {
id: Uuid,
};
pub const Query = api.follows.FollowingQueryArgs;
pub const Query = api.FollowingQueryArgs;
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const results = try srv.queryFollowers(req.args.id, req.query);
@ -59,7 +59,7 @@ pub const query_following = struct {
id: Uuid,
};
pub const Query = api.follows.FollowerQueryArgs;
pub const Query = api.FollowerQueryArgs;
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const results = try srv.queryFollowing(req.args.id, req.query);

View file

@ -114,19 +114,14 @@ const signup = struct {
srv: anytype,
) !void {
const invite = if (invite_code) |code| srv.validateInvite(code) catch |err| switch (err) {
//error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv),
error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv),
else => |e| return e,
} else null;
defer util.deepFree(srv.allocator, invite);
const creator = if (invite) |inv| try srv.getActor(inv.created_by) else null;
defer util.deepFree(srv.allocator, creator);
try res.template(status, srv, tmpl, .{
.error_msg = error_msg,
.invite = if (invite) |inv| .{
.meta = inv,
.creator = creator.?,
} else null,
.invite = invite,
});
}
@ -228,7 +223,7 @@ const user_details = struct {
};
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const user = try srv.getActor(req.args.id);
const user = try srv.getUser(req.args.id);
defer util.deepFree(srv.allocator, user);
try res.template(.ok, srv, tmpl, user);
@ -341,7 +336,7 @@ const drive = struct {
return res.status(.see_other);
},
.upload => |body| {
const entry_id = try srv.driveUpload(
const entry = try srv.driveUpload(
.{
.filename = body.file.filename,
.dir = trimmed_path,
@ -351,7 +346,6 @@ const drive = struct {
},
body.file.data,
);
const entry = try srv.driveGetEntryById(entry_id);
defer util.deepFree(srv.allocator, entry);
const url = try std.fmt.allocPrint(srv.allocator, "{s}/drive/{s}", .{
@ -403,17 +397,15 @@ const cluster = struct {
};
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
const comm_id = try srv.createCommunity(req.body.origin, req.body.name);
const community = try srv.getCommunity(comm_id);
const community = try srv.createCommunity(req.body.origin, req.body.name);
defer util.deepFree(srv.allocator, community);
const invite_id = try srv.createInvite(.{
const invite = try srv.createInvite(.{
.max_uses = 1,
.kind = .community_owner,
.to_community = community.id,
});
const invite = try srv.getInvite(invite_id);
defer util.deepFree(srv.allocator, invite);
try res.template(.ok, srv, success_tmpl, .{ .community = community, .invite = invite });

View file

@ -10,7 +10,7 @@
<div>
<div>You are about to accept an invite from:</div>
{#template mini-user $invite.creator}
{#if @isTag($invite.meta.kind, community_owner) =}
{#if @isTag($invite.kind, community_owner) =}
<div>This act will make your new account the owner of { %community.name }.</div>
{/if =}
</div>
@ -39,7 +39,7 @@
</div>
</label>
{#if .invite |$invite| =}
<input style="display: none" type="text" name="invite_code" value="{$invite.meta.code}" />
<input style="display: none" type="text" name="invite_code" value="{$invite.code}" />
{/if =}
<button type="submit">Sign up</button>
</form>

View file

@ -84,11 +84,7 @@ pub fn main() !void {
var api_src = try api.ApiSource.init(&pool);
var srv = http.Server.init();
defer srv.deinit();
const addr = "::1";
const port = 8080;
try srv.listen(std.net.Address.parseIp(addr, port) catch unreachable);
std.log.info("Listening on {s}:{}", .{ addr, port });
try srv.listen(std.net.Address.parseIp("::1", 8080) catch unreachable);
var i: usize = 0;
while (i < cfg.worker_threads - 1) : (i += 1) {

View file

@ -505,7 +505,6 @@ fn Tx(comptime tx_level: u8) type {
};
}
pub const BeginOrSavepoint = Tx(tx_level + 1);
pub const beginOrSavepoint = if (tx_level == 0) begin else savepoint;
pub const commitOrRelease = if (tx_level < 2) commit else release;

View file

@ -67,14 +67,7 @@ pub fn parseRfc3339(str: []const u8) !DateTime {
};
}
const is_test = @import("builtin").is_test;
const test_utils = struct {
pub threadlocal var test_now_timestamp: i64 = 1356076800;
};
pub usingnamespace if (is_test) test_utils else struct {};
pub fn now() DateTime {
if (comptime is_test) return .{ .seconds_since_epoch = test_utils.test_now_timestamp };
return .{ .seconds_since_epoch = std.time.timestamp() };
}