(#21) Add /users/:id/profile endpoint
This commit is contained in:
parent
cece277eec
commit
795f7983f8
4 changed files with 100 additions and 11 deletions
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue