╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │42 + │ │43 + try name_to_user_id.put(hash.hashInt(hash_kind, event.name), .{ .bytes = event_id }); │ │44 } else { │ │45 + // read the user's name so we can drop its name->id index entry │ │46 + if (try event_id_to_user.getCursor(user_key)) |existing_cursor| { │ │47 + const existing_user = try DB.HashMap(.read_only).init(existing_cursor); │ │48 + const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_user); │ │49 + _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_event.name)); │ │50 + } │ │51 + │ │52 if (!try event_id_to_user.remove(user_key)) return error.EventNotFound; │ │53 │ │54 const user_id_to_repos_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repos"));│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 68 .params = bcrypt.Params.owasp, │ │ 69 .encoding = .phc, │ │ 70 }, out, io); │ │ 71 +} │ │ 72 + │ │ 73 +pub const VerifyResult = union(enum) { │ │ 74 + success: [evt.event_id_size]u8, │ │ 75 + unknown_user, │ │ 76 + wrong_password, │ │ 77 +}; │ │ 78 + │ │ 79 +// look up a user by name (via the name->user-id index) and verify the │ │ 80 +// supplied password against the stored bcrypt hash. used by both the TTY │ │ 81 +// login submit and the server's /login route. │ │ 82 +pub fn verifyCredentials( │ │ 83 + comptime DB: type, │ │ 84 + comptime hash_kind: hash.HashKind, │ │ 85 + haxy_moment: DB.HashMap(.read_only), │ │ 86 + arena: *std.heap.ArenaAllocator, │ │ 87 + name: []const u8, │ │ 88 + password: []const u8, │ │ 89 +) !VerifyResult { │ │ 90 + const name_index_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "name->user-id")) orelse return .unknown_user; │ │ 91 + const name_index = try DB.HashMap(.read_only).init(name_index_cursor); │ │ 92 + │ │ 93 + const user_id_cursor = try name_index.getCursor(hash.hashInt(hash_kind, name)) orelse return .unknown_user; │ │ 94 + var user_id: [evt.event_id_size]u8 = undefined; │ │ 95 + _ = try user_id_cursor.readBytes(&user_id); │ │ 96 + │ │ 97 + const user_key = hash.hashInt(hash_kind, &user_id); │ │ 98 + const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->user")) orelse return .unknown_user;│ │ 99 + const event_id_to_user = try DB.HashMap(.read_only).init(event_id_to_user_cursor); │ │100 + │ │101 + const user_cursor = try event_id_to_user.getCursor(user_key) orelse return .unknown_user; │ │102 + const user_map = try DB.HashMap(.read_only).init(user_cursor); │ │103 + const user_event = try evt.read(@This(), DB, hash_kind, arena, user_map); │ │104 + │ │105 + bcrypt.strVerify(user_event.password_hash, password, .{ .silently_truncate_password = false }) catch { │ │106 + return .wrong_password; │ │107 + }; │ │108 + │ │109 + return .{ .success = user_id }; │ │110 } │ │111 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────╮ │ 1 const std = @import("std"); │ │ 2 const ui = @import("./ui.zig"); │ │ 3 const web = @import("./web.zig"); │ │ 4 -const xitui = @import("xit").xitui; │ │ 4 +const xit = @import("xit"); │ │ 5 +const xitui = xit.xitui; │ │ 6 const inp = xitui.input; │ │ 7 +const wgt = xitui.widget; │ │ 8 │ │ 9 const allocator = std.heap.wasm_allocator;│ │10 │ ╰──────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │12 │ │13 fn updateHtml() !void { │ │14 const root_ptr = if (root) |*root_value| root_value else return error.NotStarted;│ │13 - const html = try web.generateHtml(allocator, root_ptr); │ │15 + const html = try web.generateHtml(allocator, root_ptr, &session); │ │16 defer allocator.free(html); │ │17 setHtml(html); │ │18 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │21 // to outlive `root`, because `root` holds slices into the parsed strings.│ │22 var page_arena: ?std.heap.ArenaAllocator = null; │ │23 var page: ?ui.Page = null; │ │24 +var session: ui.Session = .{}; │ │25 │ │26 fn start(json: []const u8, max_width: u32) !void { │ │27 if (page_arena) |*a| a.deinit(); │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │33 .allocate = .alloc_always, │ │34 }); │ │35 │ │33 - var next_root = try ui.initRoot(allocator, &(page orelse unreachable)); │ │36 + // restore session.user_id from the serialized page so the first render │ │37 + // matches what the server rendered (otherwise wasm would render the │ │38 + // page in a logged-out state on top of a logged-in server-rendered HTML). │ │39 + session.user_id = switch (page orelse unreachable) { │ │40 + .home => |h| h.user_id, │ │41 + }; │ │42 + │ │43 + var next_root = try ui.initRoot(allocator, &(page orelse unreachable), &session);│ │44 errdefer next_root.deinit(); │ │45 │ │46 if (root) |*old_root| old_root.deinit(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │63 fn onKeyDown(key_code: u32) !void { │ │64 const root_ptr = if (root) |*root_value| root_value else return error.NotStarted;│ │65 const key: inp.Key = switch (key_code) { │ │66 + 13 => .enter, │ │67 33 => .page_up, │ │68 34 => .page_down, │ │69 35 => .end, │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │135 }; │ │136 } │ │137 │ │138 +// js calls this when the value of an overlay <input> changes; we replace │ │139 +// the matching TextInput's content with the supplied utf-8 bytes. │ │140 +export fn _setTextInputValue(focus_id: u32, value_ptr: [*]const u8, value_len: u32) void { │ │141 + setTextInputValue(focus_id, value_ptr[0..value_len]) catch |err| { │ │142 + var buf: [256]u8 = undefined; │ │143 + const str = std.fmt.bufPrint(&buf, "setTextInputValue: {}", .{err}) catch unreachable;│ │144 + consoleLog(str); │ │145 + }; │ │146 +} │ │147 + │ │148 +fn setTextInputValue(focus_id: u32, bytes: []const u8) !void { │ │149 + const root_ptr = if (root) |*r| r else return error.NotStarted; │ │150 + const root_focus = root_ptr.getFocus(); │ │151 + const child = root_focus.children.get(focus_id) orelse return error.UnknownFocusId; │ │152 + switch (child.focus.kind) { │ │153 + .text_input, .text_input_password => {}, │ │154 + else => return error.NotATextInput, │ │155 + } │ │156 + const ti: *wgt.TextInput(ui.Widget) = @fieldParentPtr("focus", child.focus); │ │157 + try ti.setContent(bytes); │ │158 +} │ │159 + │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │57 const Repo = rp.Repo(.xit, repo_opts); │ │58 var repo = try Repo.open(io, allocator, .{ .path = handler.admin_repo_path }); │ │59 defer repo.deinit(io, allocator); │ │60 - const page: ui.Page = .{ .home = try .init(repo_opts, &page_arena, &repo) }; │ │60 + var ui_session: ui.Session = .{}; │ │61 │ │62 - var root = try ui.initRoot(allocator, &page); │ │62 + const page: ui.Page = .{ .home = try .init(repo_opts, &page_arena, &repo, &ui_session) };│ │63 + │ │64 + var root = try ui.initRoot(allocator, &page, &ui_session); │ │65 defer root.deinit(); │ │66 │ │67 // terminal and its writer adapter live in a nested block so the deinit │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │40 var page_arena = std.heap.ArenaAllocator.init(allocator); │ │41 defer page_arena.deinit(); │ │42 │ │43 - // create the admin repo and build the Page from it │ │44 - const page: ui.Page = blk: { │ │45 - const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server", "admin" });│ │46 - defer allocator.free(work_path); │ │43 + const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server", "admin" }); │ │44 + defer allocator.free(work_path); │ │45 │ │48 - const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; │ │49 - const Repo = rp.Repo(.xit, repo_opts); │ │50 - var repo = try Repo.init(io, allocator, .{ .path = work_path }); │ │51 - defer repo.deinit(io, allocator); │ │46 + const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; │ │47 + const Repo = rp.Repo(.xit, repo_opts); │ │48 + var repo = try Repo.init(io, allocator, .{ .path = work_path }); │ │49 + defer repo.deinit(io, allocator); │ │50 + │ │51 + var session: ui.Session = .{}; │ │52 │ │53 + const page: ui.Page = blk: { │ │54 var arena = std.heap.ArenaAllocator.init(allocator); │ │55 defer arena.deinit(); │ │56 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯