Create API Context

This commit is contained in:
jaina heartles 2023-01-01 15:58:17 -08:00
parent 62d47d9d2f
commit 6f30696d30
4 changed files with 64 additions and 55 deletions

View File

@ -287,8 +287,9 @@ pub const ApiSource = struct {
return Conn{ return Conn{
.db = db, .db = db,
.user_id = null, .context = .{
.community = community, .community = community,
},
.allocator = alloc, .allocator = alloc,
}; };
} }
@ -307,40 +308,48 @@ pub const ApiSource = struct {
return Conn{ return Conn{
.db = db, .db = db,
.token_info = token_info, .context = .{
.user_id = token_info.user_id, .community = community,
.community = community, .token_info = token_info,
},
.allocator = alloc, .allocator = alloc,
}; };
} }
}; };
pub const ApiContext = struct {
token_info: ?Token.Info = null,
community: Community,
pub fn userId(self: ApiContext) ?Uuid {
if (self.token_info) |t| return t.user_id else return null;
}
};
fn ApiConn(comptime DbConn: type, comptime models: anytype) type { fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
return struct { return struct {
const Self = @This(); const Self = @This();
db: DbConn, db: DbConn,
token_info: ?Token.Info = null, context: ApiContext,
user_id: ?Uuid = null,
community: Community,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
pub fn close(self: *Self) void { pub fn close(self: *Self) void {
util.deepFree(self.allocator, self.community); util.deepFree(self.allocator, self.context.community);
if (self.token_info) |info| util.deepFree(self.allocator, info); if (self.context.token_info) |info| util.deepFree(self.allocator, info);
self.db.releaseConnection(); self.db.releaseConnection();
} }
fn isAdmin(self: *Self) bool { fn isAdmin(self: *Self) bool {
// TODO // TODO
return self.user_id != null and self.community.kind == .admin; return self.context.userId() != null and self.context.community.kind == .admin;
} }
pub fn login(self: *Self, username: []const u8, password: []const u8) !Token { pub fn login(self: *Self, username: []const u8, password: []const u8) !Token {
return models.auth.login( return models.auth.login(
self.db, self.db,
username, username,
self.community.id, self.context.community.id,
password, password,
self.allocator, self.allocator,
); );
@ -355,7 +364,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
issued_at: DateTime, issued_at: DateTime,
}; };
pub fn verifyAuthorization(self: *Self) !AuthorizationInfo { pub fn verifyAuthorization(self: *Self) !AuthorizationInfo {
if (self.token_info) |info| { if (self.context.token_info) |info| {
const user = try models.actors.get(self.db, info.user_id, self.allocator); const user = try models.actors.get(self.db, info.user_id, self.allocator);
defer util.deepFree(self.allocator, user); defer util.deepFree(self.allocator, user);
@ -365,8 +374,8 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
return AuthorizationInfo{ return AuthorizationInfo{
.id = user.id, .id = user.id,
.username = username, .username = username,
.community_id = self.community.id, .community_id = self.context.community.id,
.host = try util.deepClone(self.allocator, self.community.host), .host = try util.deepClone(self.allocator, self.context.community.host),
.issued_at = info.issued_at, .issued_at = info.issued_at,
}; };
@ -405,14 +414,14 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse { pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse {
// Only logged in users can make invites // Only logged in users can make invites
const user_id = self.user_id orelse return error.TokenRequired; const user_id = self.context.userId() orelse return error.TokenRequired;
const community_id = if (options.to_community) |id| blk: { const community_id = if (options.to_community) |id| blk: {
// Only admins can send invites for other communities // Only admins can send invites for other communities
if (!self.isAdmin()) return error.PermissionDenied; if (!self.isAdmin()) return error.PermissionDenied;
break :blk id; break :blk id;
} else self.community.id; } else self.context.community.id;
// Users can only make user invites // Users can only make user invites
if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied; if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied;
@ -438,7 +447,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} else try std.fmt.allocPrint( } else try std.fmt.allocPrint(
self.allocator, self.allocator,
"{s}://{s}/invite/{s}", "{s}://{s}/invite/{s}",
.{ @tagName(self.community.scheme), self.community.host, invite.code }, .{ @tagName(self.context.community.scheme), self.context.community.host, invite.code },
); );
errdefer util.deepFree(self.allocator, url); errdefer util.deepFree(self.allocator, url);
@ -468,25 +477,25 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
// pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse { // pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse {
// const tx = try self.db.beginOrSavepoint(); // const tx = try self.db.beginOrSavepoint();
// const maybe_invite = if (opt.invite_code) |code| // const maybe_invite = if (opt.invite_code) |code|
// try models.invites.getByCode(tx, code, self.community.id, self.allocator) // try models.invites.getByCode(tx, code, self.context.community.id, self.allocator)
// else // else
// null; // null;
// defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); // defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv);
// if (maybe_invite) |invite| { // if (maybe_invite) |invite| {
// if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; // if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity;
// if (!isInviteValid(invite)) return error.InvalidInvite; // if (!isInviteValid(invite)) return error.InvalidInvite;
// } // }
// const invite_kind = if (maybe_invite) |inv| inv.kind else .user; // const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
// if (self.community.kind == .admin) @panic("Unimplmented"); // if (self.context.community.kind == .admin) @panic("Unimplmented");
// const user_id = try models.auth.register( // const user_id = try models.auth.register(
// tx, // tx,
// username, // username,
// password, // password,
// self.community.id, // self.context.community.id,
// .{ // .{
// .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, // .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null,
// .email = opt.email, // .email = opt.email,
@ -498,7 +507,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
// .user => {}, // .user => {},
// .system => @panic("System user invites unimplemented"), // .system => @panic("System user invites unimplemented"),
// .community_owner => { // .community_owner => {
// try models.communities.transferOwnership(tx, self.community.id, user_id); // try models.communities.transferOwnership(tx, self.context.community.id, user_id);
// }, // },
// } // }
@ -519,20 +528,20 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
try std.fmt.allocPrint( try std.fmt.allocPrint(
self.allocator, self.allocator,
"{s}://{s}/media/{}", "{s}://{s}/media/{}",
.{ @tagName(self.community.scheme), self.community.host, fid }, .{ @tagName(self.context.community.scheme), self.context.community.host, fid },
) )
else else
try std.fmt.allocPrint( try std.fmt.allocPrint(
self.allocator, self.allocator,
"{s}://{s}/{s}", "{s}://{s}/{s}",
.{ @tagName(self.community.scheme), self.community.host, default_avatar }, .{ @tagName(self.context.community.scheme), self.context.community.host, default_avatar },
); );
errdefer self.allocator.free(avatar_url); errdefer self.allocator.free(avatar_url);
const header_url = if (user.header_file_id) |fid| const header_url = if (user.header_file_id) |fid|
try std.fmt.allocPrint( try std.fmt.allocPrint(
self.allocator, self.allocator,
"{s}://{s}/media/{}", "{s}://{s}/media/{}",
.{ @tagName(self.community.scheme), self.community.host, fid }, .{ @tagName(self.context.community.scheme), self.context.community.host, fid },
) )
else else
null; null;
@ -565,8 +574,8 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
const user = try self.getUserUnchecked(self.db, user_id); const user = try self.getUserUnchecked(self.db, user_id);
errdefer util.deepFree(self.allocator, user); errdefer util.deepFree(self.allocator, user);
if (self.user_id == null) { if (self.context.userId() == null) {
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound; if (!Uuid.eql(self.context.community.id, user.community_id)) return error.NotFound;
} }
return user; return user;
@ -574,10 +583,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
pub fn createNote(self: *Self, content: []const u8) !Note { pub fn createNote(self: *Self, content: []const u8) !Note {
// You cannot post on admin accounts // You cannot post on admin accounts
if (self.community.kind == .admin) return error.WrongCommunity; if (self.context.community.kind == .admin) return error.WrongCommunity;
// Only authenticated users can post // Only authenticated users can post
const user_id = self.user_id orelse return error.TokenRequired; const user_id = self.context.userId() orelse return error.TokenRequired;
const note_id = try models.notes.create(self.db, user_id, content, self.allocator); const note_id = try models.notes.create(self.db, user_id, content, self.allocator);
return self.getNote(note_id) catch |err| switch (err) { return self.getNote(note_id) catch |err| switch (err) {
@ -591,8 +600,8 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
errdefer util.deepFree(self.allocator, note); errdefer util.deepFree(self.allocator, note);
// Only serve community-specific notes on unauthenticated requests // Only serve community-specific notes on unauthenticated requests
if (self.user_id == null) { if (self.context.userId() == null) {
if (!Uuid.eql(self.community.id, note.author.community_id)) return error.NotFound; if (!Uuid.eql(self.context.community.id, note.author.community_id)) return error.NotFound;
} }
return note; return note;
@ -615,7 +624,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult { pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult {
var all_args = std.mem.zeroInit(Note.QueryArgs, args); var all_args = std.mem.zeroInit(Note.QueryArgs, args);
all_args.community_id = self.community.id; all_args.community_id = self.context.community.id;
const result = try models.notes.query(self.db, all_args, self.allocator); const result = try models.notes.query(self.db, all_args, self.allocator);
return TimelineResult{ return TimelineResult{
.items = result.items, .items = result.items,
@ -625,10 +634,10 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult { pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult {
if (self.user_id == null) return error.NoToken; if (self.context.userId() == null) return error.NoToken;
var all_args = std.mem.zeroInit(Note.QueryArgs, args); var all_args = std.mem.zeroInit(Note.QueryArgs, args);
all_args.followed_by = self.user_id; all_args.followed_by = self.context.userId();
const result = try models.notes.query(self.db, all_args, self.allocator); const result = try models.notes.query(self.db, all_args, self.allocator);
return TimelineResult{ return TimelineResult{
.items = result.items, .items = result.items,
@ -660,12 +669,12 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn follow(self: *Self, followee: Uuid) !void { pub fn follow(self: *Self, followee: Uuid) !void {
const result = try models.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); const result = try models.follows.create(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator);
defer util.deepFree(self.allocator, result); defer util.deepFree(self.allocator, result);
} }
pub fn unfollow(self: *Self, followee: Uuid) !void { pub fn unfollow(self: *Self, followee: Uuid) !void {
const result = try models.follows.delete(self.db, self.user_id orelse return error.NoToken, followee, self.allocator); const result = try models.follows.delete(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator);
defer util.deepFree(self.allocator, result); defer util.deepFree(self.allocator, result);
} }
@ -736,7 +745,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry { pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry {
const user_id = self.user_id orelse return error.NoToken; const user_id = self.context.userId() orelse return error.NoToken;
const file_id = try models.files.create(self.db, user_id, .{ const file_id = try models.files.create(self.db, user_id, .{
.filename = meta.filename, .filename = meta.filename,
.description = meta.description, .description = meta.description,
@ -782,14 +791,14 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry { pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry {
const user_id = self.user_id orelse return error.NoToken; 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); const entry = try models.drive.create(self.db, user_id, parent_path, name, null, self.allocator);
errdefer util.deepFree(self.allocator, entry); errdefer util.deepFree(self.allocator, entry);
return try self.backendDriveEntryToFrontend(entry, true); return try self.backendDriveEntryToFrontend(entry, true);
} }
pub fn driveDelete(self: *Self, path: []const u8) !void { pub fn driveDelete(self: *Self, path: []const u8) !void {
const user_id = self.user_id orelse return error.NoToken; const user_id = self.context.userId() orelse return error.NoToken;
const entry = try models.drive.stat(self.db, user_id, path, self.allocator); const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
defer util.deepFree(self.allocator, entry); defer util.deepFree(self.allocator, entry);
try models.drive.delete(self.db, entry.id, self.allocator); try models.drive.delete(self.db, entry.id, self.allocator);
@ -797,14 +806,14 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry { pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry {
const user_id = self.user_id orelse return error.NoToken; const user_id = self.context.userId() orelse return error.NoToken;
try models.drive.move(self.db, user_id, src, dest, self.allocator); try models.drive.move(self.db, user_id, src, dest, self.allocator);
return try self.driveGet(dest); return try self.driveGet(dest);
} }
pub fn driveGet(self: *Self, path: []const u8) !DriveEntry { pub fn driveGet(self: *Self, path: []const u8) !DriveEntry {
const user_id = self.user_id orelse return error.NoToken; const user_id = self.context.userId() orelse return error.NoToken;
const entry = try models.drive.stat(self.db, user_id, path, self.allocator); const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
errdefer util.deepFree(self.allocator, entry); errdefer util.deepFree(self.allocator, entry);
@ -812,7 +821,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry { pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry {
const user_id = self.user_id orelse return error.NoToken; const user_id = self.context.userId() orelse return error.NoToken;
std.log.debug("{s}", .{path}); std.log.debug("{s}", .{path});
const entry = try models.drive.stat(self.db, user_id, path, self.allocator); const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
defer util.deepFree(self.allocator, entry); defer util.deepFree(self.allocator, entry);
@ -834,7 +843,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
} }
pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void { pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void {
if (!Uuid.eql(id, self.user_id orelse return error.NoToken)) return error.AccessDenied; if (!Uuid.eql(id, self.context.userId() orelse return error.NoToken)) return error.AccessDenied;
try models.actors.updateProfile(self.db, id, data, self.allocator); try models.actors.updateProfile(self.db, id, data, self.allocator);
} }
@ -842,7 +851,7 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
const invite = models.invites.getByCode( const invite = models.invites.getByCode(
self.db, self.db,
code, code,
self.community.id, self.context.community.id,
self.allocator, self.allocator,
) catch |err| switch (err) { ) catch |err| switch (err) {
error.NotFound => return error.InvalidInvite, error.NotFound => return error.InvalidInvite,
@ -850,13 +859,13 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
}; };
errdefer util.deepFree(self.allocator, invite); errdefer util.deepFree(self.allocator, invite);
if (!Uuid.eql(invite.community_id, self.community.id)) return error.InvalidInvite; if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.InvalidInvite;
if (!isInviteValid(invite)) return error.InvalidInvite; if (!isInviteValid(invite)) return error.InvalidInvite;
const url = try std.fmt.allocPrint( const url = try std.fmt.allocPrint(
self.allocator, self.allocator,
"{s}://{s}/invite/{s}", "{s}://{s}/invite/{s}",
.{ @tagName(self.community.scheme), self.community.host, invite.code }, .{ @tagName(self.context.community.scheme), self.context.community.host, invite.code },
); );
errdefer util.deepFree(self.allocator, url); errdefer util.deepFree(self.allocator, url);

View File

@ -18,25 +18,25 @@ pub fn methods(comptime models: type) type {
pub fn register(self: anytype, username: []const u8, password: []const u8, opt: RegistrationOptions) !types.Actor { pub fn register(self: anytype, username: []const u8, password: []const u8, opt: RegistrationOptions) !types.Actor {
const tx = try self.db.beginOrSavepoint(); const tx = try self.db.beginOrSavepoint();
const maybe_invite = if (opt.invite_code) |code| const maybe_invite = if (opt.invite_code) |code|
try models.invites.getByCode(tx, code, self.community.id, self.allocator) try models.invites.getByCode(tx, code, self.context.community.id, self.allocator)
else else
null; null;
defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv); defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv);
if (maybe_invite) |invite| { if (maybe_invite) |invite| {
if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity; if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.WrongCommunity;
if (!isInviteValid(invite)) return error.InvalidInvite; if (!isInviteValid(invite)) return error.InvalidInvite;
} }
const invite_kind = if (maybe_invite) |inv| inv.kind else .user; const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
if (self.community.kind == .admin) @panic("Unimplmented"); if (self.context.community.kind == .admin) @panic("Unimplmented");
const user_id = try models.auth.register( const user_id = try models.auth.register(
tx, tx,
username, username,
password, password,
self.community.id, self.context.community.id,
.{ .{
.invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null, .invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null,
.email = opt.email, .email = opt.email,
@ -48,7 +48,7 @@ pub fn methods(comptime models: type) type {
.user => {}, .user => {},
.system => @panic("System user invites unimplemented"), .system => @panic("System user invites unimplemented"),
.community_owner => { .community_owner => {
try models.communities.transferOwnership(tx, self.community.id, user_id); try models.communities.transferOwnership(tx, self.context.community.id, user_id);
}, },
} }

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 { 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"); try self.headers.put("Content-Type", "text/html");
const user = if (srv.user_id) |uid| try srv.getUser(uid) else null; const user = if (srv.context.userId()) |uid| try srv.getUser(uid) else null;
defer util.deepFree(srv.allocator, user); defer util.deepFree(srv.allocator, user);
var stream = try self.open(status_code); var stream = try self.open(status_code);
@ -300,7 +300,7 @@ pub const Response = struct {
.{ .{
.community = srv.community, .community = srv.community,
.user = user, .user = user,
.user_id = srv.user_id, .user_id = srv.context.userId(),
}, },
); );

View File

@ -61,7 +61,7 @@ const index = struct {
pub const method = .GET; pub const method = .GET;
pub fn handler(_: anytype, res: anytype, srv: anytype) !void { pub fn handler(_: anytype, res: anytype, srv: anytype) !void {
if (srv.user_id == null) { if (srv.context.userId() == null) {
try res.headers.put("Location", about.path); try res.headers.put("Location", about.path);
return res.status(.see_other); return res.status(.see_other);
} }