╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │156 switch (host) { │ │157 .server => |server| { │ │158 // "repo/new" and "user/new" come before "new", which would claim them │ │159 - const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, repo, new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear }; │ │159 + const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, settings, grant, revoke, promote, demote, new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear };│ │160 inline for (@typeInfo(PostRoute).@"enum".field_names) |name| { │ │161 const suffix = "/" ++ name; │ │162 if (std.mem.endsWith(u8, path, suffix)) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │167 .@"repo/new" => handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │168 .@"user/new" => handleUserNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │169 .user => handleUserSettings(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store), │ │170 - .repo => handleRepoSettings(io, request, allocator, path, base, host), │ │170 + .settings => handleRepoSettings(io, request, allocator, path, host), │ │171 + .grant => handleRoles(io, request, allocator, base, host, .grant), │ │172 + .revoke => handleRoles(io, request, allocator, base, host, .revoke), │ │173 + .promote => handleRoles(io, request, allocator, base, host, .promote), │ │174 + .demote => handleRoles(io, request, allocator, base, host, .demote), │ │175 .new => handleNew(io, request, allocator, base, host), │ │176 .edit => handleEdit(io, request, allocator, base, host), │ │177 .remove => handleRemove(io, request, allocator, base, host), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │666 }); │ │667 } │ │668 │ │665 -// change the repo `repo_base` names as its owner, then redirect to its │ │666 -// settings under its new name. a failure goes back to the settings page. │ │669 +// change the repo the settings url `form_location` names as its owner, then│ │670 +// redirect to its settings under its new name. a failure goes back to the │ │671 +// settings page. │ │672 fn handleRepoSettings( │ │673 io: std.Io, │ │674 request: *std.http.Server.Request, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────╮ │675 allocator: std.mem.Allocator, │ │676 form_location: []const u8, │ │672 - repo_base: []const u8, │ │677 host: Host, │ │678 ) !void { │ │679 const server = switch (host) {│ ╰───────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │680 .server => |server| server, │ │681 .local => return respondRepoNotFound(request), │ │682 }; │ │683 + const route = routeFromUrl(host, form_location) orelse return respondRepoNotFound(request); │ │684 + if (route != .repo_settings) return respondRepoNotFound(request); │ │685 var arena = std.heap.ArenaAllocator.init(allocator); │ │686 defer arena.deinit(); │ │687 + const repo_base = try arena.allocator().print("/{s}", .{route.repo_settings.slice()}); │ │688 const actor = (try authorizeWrite(io, allocator, &arena, request, host, repo_base, .owner, null)) orelse return;│ │689 │ │690 const body = try readFormBody(request, allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │704 } │ │705 const discuss_role, const issue_role, const patch_role = roles; │ │706 │ │700 - // authorizeWrite refused any other base │ │701 - const identity = repoBaseIdentity(repo_base) orelse unreachable; │ │702 - const route = ui.Repo.Settings.update(io, allocator, server.users_dir, actor, identity, name, description, access, discuss_role, issue_role, patch_role) catch |err| switch (err) { │ │707 + const updated = ui.Repo.Settings.update(io, allocator, server.users_dir, actor, route.repo_settings.slice(), name, description, access, discuss_role, issue_role, patch_role) catch |err| switch (err) {│ │708 error.NotFound => return respondRepoNotFound(request), │ │709 else => { │ │710 const failure = ui.Session.FormFeedback.RepoFailure.fromError(err) orelse return err; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │721 │ │722 try request.respond("", .{ │ │723 .status = .see_other, │ │719 - .extra_headers = &.{.{ .name = "location", .value = try route.toUrl(&arena) }}, │ │724 + .extra_headers = &.{.{ .name = "location", .value = try updated.toUrl(&arena) }},│ │725 }); │ │726 } │ │727 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │728 +// act on a user's role in the repo the roles url `base` names, as one of its │ │729 +// owners, then redirect back to the list. the add form names the user in its │ │730 +// body and the row forms in the url. │ │731 +fn handleRoles(io: std.Io, request: *std.http.Server.Request, allocator: std.mem.Allocator, base: []const u8, host: Host, action: ui.Repo.Roles.Action) !void {│ │732 + const server = switch (host) { │ │733 + .server => |server| server, │ │734 + .local => return respondRepoNotFound(request), │ │735 + }; │ │736 + const route = routeFromUrl(host, base) orelse return respondRepoNotFound(request); │ │737 + if (route != .repo_roles) return respondRepoNotFound(request); │ │738 + const target = route.repo_roles; │ │739 + var arena = std.heap.ArenaAllocator.init(allocator); │ │740 + defer arena.deinit(); │ │741 + const identity = target.name.slice(); │ │742 + const actor = (try authorizeWrite(io, allocator, &arena, request, host, try arena.allocator().print("/{s}", .{identity}), .owner, null)) orelse return; │ │743 + │ │744 + const name = switch (action) { │ │745 + .grant => blk: { │ │746 + const body = try readFormBody(request, allocator); │ │747 + defer allocator.free(body); │ │748 + break :blk (try parseFormField(arena.allocator(), body, "name")) orelse ""; │ │749 + }, │ │750 + .revoke, .promote, .demote => if (target.user.len != 0) target.user.slice() else return respondRepoNotFound(request), │ │751 + }; │ │752 + │ │753 + const failure = blk: { │ │754 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ │755 + defer admin_repo.deinit(io, allocator); │ │756 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │757 + break :blk ui.Repo.Roles.perform(io, allocator, moment, server.users_dir, actor, identity, action, name) catch |err| switch (err) { │ │758 + error.NotFound, error.EventNotFound => return respondRepoNotFound(request), │ │759 + else => return err, │ │760 + }; │ │761 + }; │ │762 + if (failure) |value| { │ │763 + const location = try (ui.RoutablePage.repoRolesRoute(identity, target.start) orelse return error.RouteTooLong).toUrl(&arena); │ │764 + return respondFormFailure(request, allocator, server.session_store, location, .{ .repo_roles = .{ .failure = value, .name = name } }); │ │765 + } │ │766 + const location = try (ui.Repo.Roles.landingRoute(identity, target.start, actor, action, name) orelse return error.RouteTooLong).toUrl(&arena); │ │767 + try request.respond("", .{ .status = .see_other, .keep_alive = false, .extra_headers = &.{.{ .name = "location", .value = location }} }); │ │768 +} │ │769 + │ │770 // create a user, log them in, and redirect to their page. a failure goes │ │771 // back to the new-user page that posted the form. │ │772 fn handleUserNew( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯