╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │2320 // port the web UI is served on, for the TUI/SSH footer's "http://localhost:<port>..."│ │2321 // url. null on the web itself (no footer there). │ │2322 web_port: ?u16 = null, │ │2323 + // the authorized_keys line of the key an ssh session connected with │ │2324 + ssh_key: ?[]const u8 = null, │ │2325 // a host operation requested by a widget │ │2326 host_request: ?HostRequest = null, │ │2327 quit_requested: bool = false, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2566 self.data.user_id = null; │ │2567 self.data.user_name = null; │ │2568 self.data.user_email = null; │ │2569 + } │ │2570 + │ │2571 + // log in as the account the session's ssh key last logged into with a │ │2572 + // password, while that account still holds the key │ │2573 + pub fn logInWithSshKey(self: *Self) !void { │ │2574 + const key = self.ssh_key orelse return; │ │2575 + const admin_repo = self.admin_repo orelse return; │ │2576 + const moment = self.haxy_moment orelse return; │ │2577 + const fingerprint = evt.User.fingerprintOfKey(key) orelse return; │ │2578 + const user_id = (try evt.readLoginKeyUser(try admin_repo.core.latestMoment(), &fingerprint)) orelse return; │ │2579 + if (!try evt.User.holdsKey(evt.AdminDB, evt.admin_repo_opts.hash, moment, self.arena, &user_id, &fingerprint)) return; │ │2580 + self.data.user_id = try self.arena.allocator().dupe(u8, &user_id); │ │2581 + try self.loadUser(); │ │2582 } │ │2583 │ │2584 + // add the session's ssh key to the user who just logged in with a │ │2585 + // password, which proves they hold it, and let it log them in from now on │ │2586 + pub fn rememberSshKey(self: *Self) !void { │ │2587 + if (builtin.target.cpu.arch == .wasm32) return; │ │2588 + const key = self.ssh_key orelse return; │ │2589 + const io = self.io orelse return; │ │2590 + const users_dir = self.users_dir orelse return; │ │2591 + const admin_repo = self.admin_repo orelse return; │ │2592 + const user_id = self.userId() orelse return; │ │2593 + const fingerprint = evt.User.fingerprintOfKey(key) orelse return; │ │2594 + const allocator = self.arena.child_allocator; │ │2595 + │ │2596 + const day = (std.time.epoch.EpochSeconds{ .secs = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()) }).getEpochDay().calculateYearDay(); │ │2597 + const month = day.calculateMonthDay(); │ │2598 + const line = try std.fmt.allocPrint(allocator, "{s} added {d:0>4}-{d:0>2}-{d:0>2} via login", .{ key, day.year, month.month.numeric(), month.day_index + 1 });│ │2599 + defer allocator.free(line); │ │2600 + evt.addUserKey(io, allocator, users_dir, admin_repo, &user_id, line) catch |err| switch (err) { │ │2601 + // it's already there, or there's no room for it │ │2602 + error.KeyAdded, error.TooManyKeys => {}, │ │2603 + else => return err, │ │2604 + }; │ │2605 + try evt.setLoginKey(io, admin_repo, &fingerprint, &user_id, .trust); │ │2606 + self.haxy_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │2607 + } │ │2608 + │ │2609 + // stop the session's ssh key logging the user in, on a logout │ │2610 + pub fn forgetSshKey(self: *Self) !void { │ │2611 + if (builtin.target.cpu.arch == .wasm32) return; │ │2612 + const key = self.ssh_key orelse return; │ │2613 + const io = self.io orelse return; │ │2614 + const admin_repo = self.admin_repo orelse return; │ │2615 + const user_id = self.userId() orelse return; │ │2616 + const fingerprint = evt.User.fingerprintOfKey(key) orelse return; │ │2617 + try evt.setLoginKey(io, admin_repo, &fingerprint, &user_id, .forget); │ │2618 + } │ │2619 + │ │2620 pub fn userId(self: *const Self) ?[evt.event_id_size]u8 { │ │2621 const bytes = self.data.user_id orelse return null; │ │2622 if (bytes.len != evt.event_id_size) return null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/ui/NewUser.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │154 │ │155 // log in as the new user, reading them from a moment that has them │ │156 self.session.data.user_id = try self.session.arena.allocator().dupe(u8, &id); │ │157 + try self.session.rememberSshKey(); │ │158 self.session.haxy_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo);│ │159 try self.session.loadUser(); │ │160 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/UserLogin.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │216 // dupe user_id into the arena so the session can hold a stable slice │ │217 const user_id_stable = try self.session.arena.allocator().dupe(u8, &user_id);│ │218 self.session.data.user_id = user_id_stable; │ │219 + try self.session.rememberSshKey(); │ │220 │ │221 // adopt the user's name and persisted prefs │ │222 try self.session.loadUser(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────────╮ │src/ui/UserNotifications.zig│ ╰────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 │ │72 // leave the user tab for the page it belongs to, where the web's logout redirect also lands│ │73 pub fn logOut(session: *ui.Session) !void { │ │74 + try session.forgetSshKey(); │ │75 session.logOut(); │ │76 try session.navigate(session.data.current_page.pageRoot()); │ │77 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯