╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │222 pub const UserTab = struct { │ │223 view: UserView = .notifications, │ │224 start: usize = 0, │ │225 + // the fingerprint of the ssh key a keys route acts on ("" = none) │ │226 + key: Array(evt.User.fingerprint_len) = .{}, │ │227 + // the confirmation of removing `key` │ │228 + remove_key: bool = false, │ │229 + │ │230 + pub fn eql(a: UserTab, b: UserTab) bool { │ │231 + return a.view == b.view and a.start == b.start and std.mem.eql(u8, a.key.slice(), b.key.slice()) and a.remove_key == b.remove_key;│ │232 + } │ │233 }; │ │234 │ │235 pub const RefOrOid = enum { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │240 │ │241 // the "key:value" segments of a url tail, collected in any order │ │242 const Params = struct { │ │235 - const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from, moment, merge, user }; │ │243 + const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from, moment, merge, user, key };│ │244 │ │245 // a branch:/tag:/object: param; the value stays url-encoded │ │246 const RefParam = struct { kind: RefOrOid, value: []const u8 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │334 const merge_seg = @tagName(Params.ParamKey.merge) ++ ":"; │ │335 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":"; │ │336 const user_seg = @tagName(Params.ParamKey.user) ++ ":"; │ │337 + const key_seg = @tagName(Params.ParamKey.key) ++ ":"; │ │338 + // ends a key removal's confirmation url, so its form posts to the removal│ │339 + const remove_key_word = "removekey"; │ │340 const issue_seg = "issue:"; │ │341 const patch_seg = "patch:"; │ │342 const discuss_seg = "discuss:"; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │443 start: usize = 0, │ │444 // the user a row's form acts on ("" = none) │ │445 user: Array(evt.User.name_max_len) = .{}, │ │446 + // the confirmation of taking `user`'s role away │ │447 + confirm: ?Repo.Roles.Action = null, │ │448 }; │ │449 │ │450 pub fn repoSettingsRoute(identity: []const u8) ?RoutablePage {│ ╰───────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1275 try out.writer.print("{s}/repo/roles", .{try repoUrlPrefix(arena, r.name.slice())}); │ │1276 if (r.user.len != 0) try out.writer.print("/" ++ user_seg ++ "{s}", .{r.user.slice()});│ │1277 if (r.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{r.start}); │ │1278 + if (r.confirm) |action| try out.writer.print("/{s}", .{@tagName(action)}); │ │1279 break :blk out.written(); │ │1280 }, │ │1281 .repo_user => |u| try userTabUrl(arena, try repoUrlPrefix(arena, u.name.slice()), u.tab), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1314 fn userTabUrl(arena: *std.heap.ArenaAllocator, prefix: []const u8, tab: UserTab) ![]const u8 { │ │1315 return switch (tab.view) { │ │1316 .settings => try arena.allocator().print("{s}/user/settings", .{prefix}), │ │1303 - .keys => try arena.allocator().print("{s}/user/keys", .{prefix}), │ │1317 + .keys => blk: { │ │1318 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1319 + try out.writer.print("{s}/user/keys", .{prefix}); │ │1320 + if (tab.key.len != 0) try out.writer.print("/" ++ key_seg ++ "{s}", .{try urlEncodeRef(arena.allocator(), tab.key.slice())});│ │1321 + if (tab.remove_key) try out.writer.writeAll("/" ++ remove_key_word); │ │1322 + break :blk out.written(); │ │1323 + }, │ │1324 .notifications => if (tab.start == 0) │ │1325 try arena.allocator().print("{s}/user/notifications", .{prefix}) │ │1326 else │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1525 } │ │1526 │ │1527 // the tail after a page's "user": none or "notifications" with an optional │ │1508 - // "start:<n>", "settings", "keys", or "new" │ │1528 + // "start:<n>", "settings", "keys" with an optional "key:<fingerprint>" and │ │1529 + // removal confirmation, or "new" │ │1530 fn userTail(segments: *std.mem.SplitIterator(u8, .scalar)) ?union(enum) { new, tab: UserTab } {│ │1531 const word = segments.next() orelse return .{ .tab = .{} }; │ │1532 if (std.mem.eql(u8, word, "new")) return if (segments.next() == null) .new else null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1533 return switch (std.meta.stringToEnum(UserView, word) orelse return null) { │ │1513 - inline .settings, .keys => |view| if (segments.next() == null) .{ .tab = .{ .view = view } } else null, │ │1534 + .settings => if (segments.next() == null) .{ .tab = .{ .view = .settings } } else null, │ │1535 + .keys => .{ .tab = keysTab(segments) orelse return null }, │ │1536 .notifications => .{ .tab = .{ .start = listStart(segments) orelse return null } }, │ │1537 }; │ │1538 + } │ │1539 + │ │1540 + fn keysTab(segments: *std.mem.SplitIterator(u8, .scalar)) ?UserTab { │ │1541 + var params = Params{}; │ │1542 + const word = params.scanTail(segments) catch return null; │ │1543 + if (!params.only(&.{.key})) return null; │ │1544 + var tab: UserTab = .{ .view = .keys }; │ │1545 + if (params.values.get(.key)) |encoded| { │ │1546 + var buf: [3 * evt.User.fingerprint_len]u8 = undefined; │ │1547 + if (encoded.len > buf.len) return null; │ │1548 + @memcpy(buf[0..encoded.len], encoded); │ │1549 + tab.key = Array(evt.User.fingerprint_len).from(std.Uri.percentDecodeInPlace(buf[0..encoded.len])) orelse return null;│ │1550 + } │ │1551 + if (word) |value| { │ │1552 + if (!std.mem.eql(u8, value, remove_key_word) or tab.key.len == 0) return null; │ │1553 + tab.remove_key = true; │ │1554 + } │ │1555 + return tab; │ │1556 } │ │1557 │ │1558 fn listStart(segments: *std.mem.SplitIterator(u8, .scalar)) ?usize { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1589 } │ │1590 if (std.mem.eql(u8, word, "roles")) { │ │1591 _ = segments.next(); │ │1552 - const roles_params = listParams(&segments, &.{ .user, .start }) orelse return null; │ │1553 - var route = repoRolesRoute(pair, roles_params.start() orelse return null) orelse return null; │ │1554 - if (roles_params.values.get(.user)) |user| route.repo_roles.user = Array(evt.User.name_max_len).from(user) orelse return null;│ │1592 + const confirm_word = params.scanTail(&segments) catch return null; │ │1593 + if (!params.only(&.{ .user, .start })) return null; │ │1594 + var route = repoRolesRoute(pair, params.start() orelse return null) orelse return null; │ │1595 + if (params.values.get(.user)) |user| route.repo_roles.user = Array(evt.User.name_max_len).from(user) orelse return null; │ │1596 + if (confirm_word) |value| { │ │1597 + // only taking a role away asks first, and it names whose │ │1598 + const action = std.meta.stringToEnum(Repo.Roles.Action, value) orelse return null; │ │1599 + if ((action != .revoke and action != .demote) or route.repo_roles.user.len == 0) return null; │ │1600 + route.repo_roles.confirm = action; │ │1601 + } │ │1602 return route; │ │1603 } │ │1604 return if (isNewTail(&segments)) .{ .repo_repo_new = Array(repo_route_max_len).from(pair) orelse return null } else null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1855 if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; │ │1856 return switch (a) { │ │1857 .home_users => |a_u| a_u.start == b.home_users.start and std.mem.eql(u8, a_u.search.slice(), b.home_users.search.slice()), │ │1811 - .home_user => |a_t| std.meta.eql(a_t, b.home_user), │ │1858 + .home_user => |a_t| a_t.eql(b.home_user), │ │1859 .user_repos => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_repos.name.slice()) and a_u.start == b.user_repos.start and std.mem.eql(u8, a_u.search.slice(), b.user_repos.search.slice()),│ │1860 .user_forks => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_forks.name.slice()) and a_u.start == b.user_forks.start, │ │1861 .user_repo_new => |a_name| std.mem.eql(u8, a_name.slice(), b.user_repo_new.slice()), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯