╭ changed ──────╮ │src/command.zig│ ╰───────────────╯ ╭─────────────────────────────────╮ │3 │ │4 pub const CommandKind = enum {│ │5 serve, │ │6 + delete_user, │ │7 }; │ │8 │ │9 const Help = struct { │ ╰─────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │24 \\use "none" for --ssh-listen to disable ssh. git clones over HTTP share the web UI's listener. │ │25 \\--web-url and --ssh-url set the public urls shown to users, like https://example.com and ssh://example.com.│ │26 \\--max-repo-size caps each repo's on-disk size, like 1G, rejecting pushes past it. no limit by default. │ │27 + , │ │28 + }, │ │29 + .delete_user => .{ │ │30 + .name = "delete-user", │ │31 + .descrip = │ │32 + \\delete a user and everything they own, without their password. │ │33 + , │ │34 + .example = │ │35 + \\haxy delete-user alice --data-dir /srv/git │ │36 + \\safe to run while the server is up. │ │37 , │ │38 }, │ │39 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │193 /// if any additional allocation needs to be done, the arena inside the cmd args will be used.│ │194 pub const Command = union(CommandKind) { │ │195 serve: srv.Options, │ │196 + delete_user: srv.DeleteUserOptions, │ │197 │ │198 pub fn initMaybe(cmd_args: *CommandArgs) !?Command { │ │199 const command_kind = cmd_args.command_kind orelse return null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │201 .serve => { │ │202 return .{ .serve = try serveOptions(cmd_args) orelse return null }; │ │203 }, │ │204 + .delete_user => { │ │205 + if (cmd_args.positional_args.len != 1) return null; │ │206 + var options: srv.DeleteUserOptions = .{ .name = cmd_args.positional_args[0] }; │ │207 + if (cmd_args.get("--data-dir")) |value_maybe| options.data_dir = value_maybe orelse return error.DataDirNeedsValue;│ │208 + return .{ .delete_user = options }; │ │209 + }, │ │210 } │ │211 } │ │212 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───╮ │src/main.zig│ ╰────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │157 }, │ │158 .cli => |cli_cmd| switch (cli_cmd) { │ │159 .serve => |options| try srv.run(repo_kind, any_repo_opts, io, allocator, cwd_path, options, run_opts.err, {}),│ │160 + .delete_user => |options| try srv.deleteUser(io, allocator, cwd_path, options, run_opts.out, run_opts.err), │ │161 }, │ │162 } │ │163 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │20 data_dir: []const u8 = ".", │ │21 git_ssh_prefix: []const u8 = "", │ │22 fallback_on_address_in_use: bool = false, │ │23 +}; │ │24 + │ │25 +pub const DeleteUserOptions = struct { │ │26 + name: []const u8, │ │27 + data_dir: []const u8 = ".", │ │28 }; │ │29 │ │30 +// delete the named user as their own delete tab does, minus the password check │ │31 +pub fn deleteUser( │ │32 + io: std.Io, │ │33 + allocator: std.mem.Allocator, │ │34 + cwd_path: []const u8, │ │35 + options: DeleteUserOptions, │ │36 + out: *std.Io.Writer, │ │37 + err: *std.Io.Writer, │ │38 +) !void { │ │39 + const data_dir_path = try std.Io.Dir.path.resolveAlloc(allocator, &.{ cwd_path, options.data_dir }); │ │40 + defer allocator.free(data_dir_path); │ │41 + const users_dir = try std.Io.Dir.path.resolveAlloc(allocator, &.{ data_dir_path, "users" }); │ │42 + defer allocator.free(users_dir); │ │43 + const admin_repo_path = try std.Io.Dir.path.resolveAlloc(allocator, &.{ data_dir_path, "admin" }); │ │44 + defer allocator.free(admin_repo_path); │ │45 + │ │46 + var admin_repo = rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |open_err| switch (open_err) {│ │47 + error.RepoNotFound, error.FileNotFound => { │ │48 + try err.print("no haxy data in {s}\n", .{data_dir_path}); │ │49 + return error.HandledError; │ │50 + }, │ │51 + else => |e| return e, │ │52 + }; │ │53 + defer admin_repo.deinit(io, allocator); │ │54 + │ │55 + var arena = std.heap.ArenaAllocator.init(allocator); │ │56 + defer arena.deinit(); │ │57 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │58 + const user_id, const user = blk: { │ │59 + if (try evt.User.readIdByName(evt.AdminDB, evt.admin_repo_opts.hash, moment, options.name)) |id| { │ │60 + if (try ui.activeUser(moment, &arena, id)) |found| break :blk .{ id, found }; │ │61 + } │ │62 + try err.print("no user named {s}\n", .{options.name}); │ │63 + return error.HandledError; │ │64 + }; │ │65 + │ │66 + try evt.deleteUser(io, allocator, users_dir, &admin_repo, &user_id, .{ .name = user.event.name, .email = user.event.email }); │ │67 + try out.print("deleted user {s}\n", .{user.event.name}); │ │68 +} │ │69 + │ │70 const ListenAddress = struct { │ │71 host: []const u8, │ │72 port: u16, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯