╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ UserLogin.zig ────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
9 const xitui = xit.xitui; 10 const wgt = xitui.widget; 11 const layout = xitui.layout; 12 const Key = xitui.input.Key; 13 const Grid = xitui.grid.Grid; 14 const Focus = xitui.focus.Focus; 15 const bcrypt = std.crypto.pwhash.bcrypt; 16 17 const wasm = builtin.target.cpu.arch == .wasm32; 18 19 pub const View = struct { 20 center: ui.widget.Center, 21 session: *ui.Session, 22 nav_ids: [3]usize, 23 24 const username_index: usize = 0; 25 const password_index: usize = 1; 26 const button_index: usize = 2; 27 28 // the form posts under `route`, the page's user tab route 29 pub fn init(allocator: std.mem.Allocator, session: *ui.Session, route: ui.RoutablePage) !View { 30 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .round_corners = true, .direction = .vert }); 31 errdefer box.deinit(allocator); 32 // marks this subtree as an HTML form scope for the web renderer 33 box.getFocus().kind = .{ .custom = try session.page_arena.allocator().print("form:{s}/login", .{try route.toUrl(session.page_arena)}) }; 34 35 var nav_ids: [3]usize = undefined; 36 37 { 38 var username = try wgt.TextInput.init(allocator, .{ .top_label = .{ .text = " username " }, .name = "username", .round_corners = true, .render_content = session.is_terminal }); 39 errdefer username.deinit(allocator); 40 username.getFocus().mode = .all; 41 if (session.formFeedback(.login)) |feedback| try username.setContent(allocator, feedback.username); 42 nav_ids[username_index] = username.getFocus().id; 43 try box.children.put(allocator, username.getFocus().id, .{ 44 .widget = .{ .text_input = username }, 45 .rect = null, 46 .min_size = null, 47 }); 48 } 49 50 { 51 var password = try wgt.TextInput.init(allocator, .{ .top_label = .{ .text = " password " }, .password = true, .name = "password", .round_corners = true, .render_content = session.is_terminal }); 52 errdefer password.deinit(allocator); 53 password.getFocus().mode = .all; 54 nav_ids[password_index] = password.getFocus().id; 55 try box.children.put(allocator, password.getFocus().id, .{ 56 .widget = .{ .text_input = password }, 57 .rect = null, 58 .min_size = null, 59 }); 60 } 61 62 { 63 var button = try wgt.TextBox.init(allocator, "login", .{ .border = .single, .round_corners = true, .wrap_kind = .none }); 64 errdefer button.deinit(allocator); 65 button.getFocus().mode = .all; 66 // the renderer distinguishes plain clickables from buttons that 67 // should POST to a server route by this kind. 68 button.getFocus().kind = .{ .custom = "submit" }; 69 nav_ids[button_index] = button.getFocus().id; 70 try box.children.put(allocator, button.getFocus().id, .{ 71 .widget = .{ .text_box = button }, 72 .rect = null, 73 .min_size = null, 74 }); 75 } 76 77 box.getFocus().child_id = nav_ids[username_index]; 78 79 return .{ 80 .center = try ui.widget.Center.init(allocator, .{ .box = box }), 81 .session = session, 82 .nav_ids = nav_ids, 83 }; 84 } 85 86 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 87 self.center.deinit(allocator); 88 } 89 90 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 91 const box = &self.center.child.box; 92 93 const failure = if (self.session.formFeedback(.login)) |feedback| feedback.failure else null; 94 95 const username_input = &box.children.values()[username_index].widget.text_input; 96 username_input.options.top_label.text = if (failure == .unknown_user) 97 " username (invalid) " 98 else 99 " username "; 100 101 const password_input = &box.children.values()[password_index].widget.text_input; 102 password_input.options.top_label.text = if (failure == .wrong_password) 103 " password (invalid) " 104 else 105 " password "; 106 107 // refresh our entries in the session's focus-id -> *TextInput map with 108 // this frame's current input addresses, so the web/wasm form handling 109 // can find them by focus id 110 const inputs_arena = self.session.arena.allocator(); 111 try self.session.text_inputs.put(inputs_arena, username_input.getFocus().id, username_input); 112 try self.session.text_inputs.put(inputs_arena, password_input.getFocus().id, password_input); 113 114 try self.center.build(allocator, constraint, root_focus); 115 } 116 117 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 118 const box = &self.center.child.box; 119 const child_id = box.focus.child_id orelse return; 120 const current = self.indexOf(child_id) orelse return; 121 122 switch (key) { 123 // Shift+Tab walks backward through the form, matching the 124 // typical UX; arrow_up does the same. 125 .arrow_up, .back_tab => if (current > 0) { 126 root_focus.setFocus(self.nav_ids[current - 1]); 127 return; 128 }, 129 // Tab moves to the next field, matching the form-style UX users 130 // expect; arrow_down does the same. 131 .arrow_down, .tab => if (current + 1 < self.nav_ids.len) { 132 root_focus.setFocus(self.nav_ids[current + 1]); 133 return; 134 }, 135 else => {}, 136 } 137 138 if (current == button_index) { 139 switch (key) { 140 .enter => { 141 try self.submit(allocator); 142 return; 143 }, 144 .mouse => |mouse| { 145 if (inp.leftClickOn(root_focus, child_id, mouse)) { 146 try self.submit(allocator); 147 return; 148 } 149 }, 150 else => {}, 151 } 152 } 153 154 if (box.children.getIndex(child_id)) |idx| { 155 try box.children.values()[idx].widget.input(allocator, key, root_focus); 156 } 157 } 158 159 pub fn clearGrid(self: *View) void { 160 self.center.clearGrid(); 161 } 162 163 pub fn getGrid(self: View) ?Grid { 164 return self.center.getGrid(); 165 } 166 167 pub fn getFocus(self: *View) *Focus { 168 return self.center.getFocus(); 169 } 170 171 pub fn atTop(self: View) bool { 172 return self.center.child.box.focus.child_id == self.nav_ids[username_index]; 173 } 174 175 fn indexOf(self: View, child_id: usize) ?usize { 176 for (self.nav_ids, 0..) |id, i| { 177 if (id == child_id) return i; 178 } 179 return null; 180 } 181 182 fn submit(self: *View, allocator: std.mem.Allocator) !void { 183 const box = &self.center.child.box; 184 const username_input = &box.children.values()[username_index].widget.text_input; 185 const password_input = &box.children.values()[password_index].widget.text_input; 186 187 const username = try username_input.text(allocator); 188 defer allocator.free(username); 189 190 const password = try password_input.text(allocator); 191 defer allocator.free(password); 192 193 if (comptime wasm) { 194 // no DB cursor available on the wasm render path 195 try self.rememberFailure(.unknown_user, username); 196 return; 197 } 198 199 const haxy_moment = self.session.haxy_moment orelse { 200 // no DB context (e.g. wasm/web rendering path); treat as unknown. 201 try self.rememberFailure(.unknown_user, username); 202 return; 203 }; 204 205 const result = try evt.User.verifyCredentials(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, self.session.arena, username, password); 206 switch (result) { 207 .unknown_user => { 208 try self.rememberFailure(.unknown_user, username); 209 return; 210 }, 211 .wrong_password => { 212 try self.rememberFailure(.wrong_password, username); 213 return; 214 }, 215 .success => |user_id| { 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(); 223 224 // wipe the entered credentials so they don't linger if the 225 // user returns to this page after logging out 226 username_input.clear(allocator); 227 password_input.clear(allocator); 228 229 // leave the user tab for the page it belongs to, where the 230 // web's /login redirect also lands 231 try self.session.navigate(self.session.data.current_page.pageRoot()); 232 }, 233 } 234 } 235 236 fn rememberFailure(self: *View, failure: ui.Session.FormFeedback.LoginFailure, username: []const u8) !void { 237 self.session.data.form_feedback = .{ .login = .{ 238 .failure = failure, 239 .username = try self.session.arena.allocator().dupe(u8, username), 240 } }; 241 } 242 }; 243