(#21) Add /users/:id/profile endpoint

This commit is contained in:
jaina heartles 2022-12-07 01:59:46 -08:00
parent cece277eec
commit 795f7983f8
4 changed files with 100 additions and 11 deletions

View file

@ -61,6 +61,7 @@ pub const UserResponse = struct {
created_at: DateTime,
updated_at: DateTime,
};
pub const PartialUserProfile = services.actors.PartialProfile;
pub const NoteResponse = struct {
id: Uuid,
@ -741,5 +742,10 @@ fn ApiConn(comptime DbConn: type) type {
.data = try services.files.deref(self.allocator, id),
};
}
pub fn updateUserProfile(self: *Self, id: Uuid, data: PartialUserProfile) !void {
if (!Uuid.eql(id, self.user_id orelse return error.NoToken)) return error.AccessDenied;
try services.actors.updateProfile(self.db, id, data, self.allocator);
}
};
}

View file

@ -1,7 +1,11 @@
const std = @import("std");
const util = @import("util");
const sql = @import("sql");
const auth = @import("./auth.zig");
const common = @import("./common.zig");
const files = @import("./files.zig");
const Partial = common.Partial;
const Uuid = util.Uuid;
const DateTime = util.DateTime;
@ -26,16 +30,6 @@ pub const ActorDetailed = struct {
updated_at: DateTime,
};
pub const Profile = struct {
display_name: ?[]const u8,
bio: []const u8,
avatar_file_id: ?Uuid,
header_file_id: ?Uuid,
profile_fields: ProfileField,
};
pub const LookupError = error{
DatabaseFailure,
};
@ -164,5 +158,71 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor {
};
}
pub const PartialProfile = Partial(Profile);
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 max_fields = 32;
//pub fn update(db: anytype, id: Uuid, new: Partial(Profile), alloc: std.mem.Allocator) !Actor {}
pub const max_display_name_len = 128;
pub const max_bio = 1 << 16;
pub fn updateProfile(db: anytype, id: Uuid, new: PartialProfile, alloc: std.mem.Allocator) !void {
var builder = sql.QueryBuilder.init(alloc);
defer builder.deinit();
try builder.appendSlice("UPDATE actor");
if (new.display_name) |_| try builder.set("display_name", "$2");
if (new.bio) |_| try builder.set("bio", "$3");
if (new.avatar_file_id) |_| try builder.set("avatar_file_id", "$4");
if (new.header_file_id) |_| try builder.set("header_file_id", "$5");
if (new.profile_fields) |_| try builder.set("profile_fields", "$6");
if (builder.set_statements_appended == 0) return error.NoChange;
try builder.set("updated_at", "$7");
try builder.andWhere("id = $1");
const profile_fields = if (new.profile_fields) |pf| try std.json.stringifyAlloc(alloc, pf, .{}) else null;
defer if (profile_fields) |pf| alloc.free(pf);
const tx = try db.begin();
errdefer tx.rollback();
if (new.display_name) |maybe_dn| if (maybe_dn) |dn| {
if (dn.len > max_display_name_len) return error.DisplayNameTooLong;
};
if (new.bio) |b| if (b.len > max_bio) return error.BioTooLong;
if (new.avatar_file_id) |maybe_file_id| if (maybe_file_id) |file_id| {
const info = try files.get(tx, file_id, alloc);
defer util.deepFree(alloc, info);
if (!Uuid.eql(id, info.owner_id)) return error.FileAccessDenied;
if (info.status != .uploaded) return error.FileNotReady;
};
if (new.header_file_id) |maybe_file_id| if (maybe_file_id) |file_id| {
const info = try files.get(tx, file_id, alloc);
defer util.deepFree(alloc, info);
if (!Uuid.eql(id, info.owner_id)) return error.FileAccessDenied;
if (info.status != .uploaded) return error.FileNotReady;
};
if (new.profile_fields) |f| if (f.len > max_fields) return error.TooManyFields;
try tx.execWithOptions(try builder.terminate(), .{
id,
new.display_name orelse null,
new.bio orelse null,
new.avatar_file_id orelse null,
new.header_file_id orelse null,
profile_fields,
DateTime.now(),
}, .{ .allocator = alloc, .ignore_unused_arguments = true });
try tx.commit();
}

View file

@ -18,6 +18,7 @@ pub const routes = .{
controllers.apiEndpoint(invites.create),
controllers.apiEndpoint(users.create),
controllers.apiEndpoint(users.get),
controllers.apiEndpoint(users.update_profile),
controllers.apiEndpoint(notes.create),
controllers.apiEndpoint(notes.get),
//controllers.apiEndpoint(streaming.streaming),

View file

@ -1,4 +1,5 @@
const util = @import("util");
const api = @import("api");
pub const create = struct {
pub const method = .POST;
@ -40,3 +41,24 @@ pub const get = struct {
try res.json(.ok, result);
}
};
pub const update_profile = struct {
pub const method = .PUT;
pub const path = "/users/:id";
pub const Args = struct {
id: util.Uuid,
};
pub const Body = api.PartialUserProfile;
// 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.getUser(req.args.id);
defer util.deepFree(srv.allocator, result);
try res.json(.ok, result);
}
};