╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Login.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
34 35 const username_index: usize = 0; 36 const password_index: usize = 1; 37 const button_index: usize = 2; 38 39 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { 40 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert }); 41 errdefer box.deinit(allocator); 42 // marks this subtree as an HTML form scope for the web overlay 43 box.getFocus().kind = .{ .custom = "form:login" }; 44 45 var nav_ids: [3]usize = undefined; 46 47 { 48 var username = try wgt.TextInput(ui.Widget).init(allocator, .{ .label = " username ", .name = "username", .rounded_corners = true, .render_content = !wasm }); 49 errdefer username.deinit(allocator); 50 username.getFocus().focusable = true; 51 nav_ids[username_index] = username.getFocus().id; 52 try box.children.put(allocator, username.getFocus().id, .{ 53 .widget = .{ .text_input = username }, 54 .rect = null, 55 .min_size = null, 56 }); 57 } 58 59 { 60 var password = try wgt.TextInput(ui.Widget).init(allocator, .{ .label = " password ", .password = true, .name = "password", .rounded_corners = true, .render_content = !wasm }); 61 errdefer password.deinit(allocator); 62 password.getFocus().focusable = true; 63 nav_ids[password_index] = password.getFocus().id; 64 try box.children.put(allocator, password.getFocus().id, .{ 65 .widget = .{ .text_input = password }, 66 .rect = null, 67 .min_size = null, 68 }); 69 } 70 71 { 72 var button = try wgt.TextBox(ui.Widget).init(allocator, "login", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 73 errdefer button.deinit(allocator); 74 button.getFocus().focusable = true; 75 // the renderer distinguishes plain clickables from buttons that 76 // should POST to a server route by this kind. 77 button.getFocus().kind = .{ .custom = "submit" }; 78 nav_ids[button_index] = button.getFocus().id; 79 try box.children.put(allocator, button.getFocus().id, .{ 80 .widget = .{ .text_box = button }, 81 .rect = null, 82 .min_size = null, 83 }); 84 } 85 86 box.getFocus().child_id = nav_ids[username_index]; 87 88 return .{ 89 .center = try ui.Center.init(allocator, .{ .box = box }), 90 .data = data, 91 .session = session, 92 .nav_ids = nav_ids, 93 }; 94 } 95 96 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 97 self.center.deinit(allocator); 98 } 99 100 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 101 const box = &self.center.child.box; 102 103 const failure = self.session.data.login_failure; 104 105 const username_input = &box.children.values()[username_index].widget.text_input; 106 username_input.options.label = if (failure == .unknown_user) 107 " username (invalid) " 108 else 109 " username "; 110 111 const password_input = &box.children.values()[password_index].widget.text_input; 112 password_input.options.label = if (failure == .wrong_password) 113 " password (invalid) " 114 else 115 " password "; 116 117 // refresh our entries in the session's focus-id -> *TextInput map with 118 // this frame's current input addresses, so the web/wasm form handling 119 // can find them by focus id 120 const inputs_arena = self.session.arena.allocator(); 121 try self.session.text_inputs.put(inputs_arena, username_input.getFocus().id, username_input); 122 try self.session.text_inputs.put(inputs_arena, password_input.getFocus().id, password_input); 123 124 try self.center.build(allocator, constraint, root_focus); 125 } 126 127 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 128 const box = &self.center.child.box; 129 const child_id = box.focus.child_id orelse return; 130 const current = self.indexOf(child_id) orelse return; 131 132 switch (key) { 133 // Shift+Tab walks backward through the form, matching the 134 // typical UX; arrow_up does the same. 135 .arrow_up, .back_tab => if (current > 0) { 136 root_focus.setFocus(self.nav_ids[current - 1]); 137 return; 138 }, 139 // Tab moves to the next field, matching the form-style UX users 140 // expect; arrow_down does the same. 141 .arrow_down, .tab => if (current + 1 < self.nav_ids.len) { 142 root_focus.setFocus(self.nav_ids[current + 1]); 143 return; 144 }, 145 else => {}, 146 } 147 148 if (current == button_index) { 149 switch (key) { 150 .enter => { 151 try self.submit(allocator); 152 return; 153 }, 154 .mouse => |mouse| { 155 if (inp.leftClickOn(root_focus, child_id, mouse)) { 156 try self.submit(allocator); 157 return; 158 } 159 }, 160 else => {}, 161 } 162 } 163 164 const username_input = &box.children.values()[username_index].widget.text_input; 165 const password_input = &box.children.values()[password_index].widget.text_input; 166 const username_len_before = username_input.content.items.len; 167 const password_len_before = password_input.content.items.len; 168 169 if (box.children.getIndex(child_id)) |idx| { 170 try box.children.values()[idx].widget.input(allocator, key, root_focus); 171 } 172 173 if (username_input.content.items.len != username_len_before or 174 password_input.content.items.len != password_len_before) 175 { 176 // any edit clears the failure flag so the "(invalid)" label 177 // doesn't linger after the user has typed a correction. 178 self.session.data.login_failure = null; 179 } 180 } 181 182 pub fn clearGrid(self: *View) void { 183 self.center.clearGrid(); 184 } 185 186 pub fn getGrid(self: View) ?Grid { 187 return self.center.getGrid(); 188 } 189 190 pub fn getFocus(self: *View) *Focus { 191 return self.center.getFocus(); 192 } 193 194 pub fn getSelectedIndex(self: View) ?usize { 195 const child_id = self.center.child.box.focus.child_id orelse return null; 196 return self.indexOf(child_id); 197 } 198 199 fn indexOf(self: View, child_id: usize) ?usize { 200 for (self.nav_ids, 0..) |id, i| { 201 if (id == child_id) return i; 202 } 203 return null; 204 } 205 206 fn submit(self: *View, allocator: std.mem.Allocator) !void { 207 if (comptime wasm) { 208 // no DB cursor available on the wasm render path 209 self.session.data.login_failure = .unknown_user; 210 return; 211 } 212 213 const box = &self.center.child.box; 214 const username_input = &box.children.values()[username_index].widget.text_input; 215 const password_input = &box.children.values()[password_index].widget.text_input; 216 217 const username = try username_input.text(allocator); 218 defer allocator.free(username); 219 220 const password = try password_input.text(allocator); 221 defer allocator.free(password); 222 223 const haxy_moment = self.session.haxy_moment orelse { 224 // no DB context (e.g. wasm/web rendering path); treat as unknown. 225 self.session.data.login_failure = .unknown_user; 226 return; 227 }; 228 229 const result = try evt.User.verifyCredentials(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, self.session.arena, username, password); 230 switch (result) { 231 .unknown_user => { 232 self.session.data.login_failure = .unknown_user; 233 return; 234 }, 235 .wrong_password => { 236 self.session.data.login_failure = .wrong_password; 237 return; 238 }, 239 .success => |user_id| { 240 // dupe user_id into the arena so the session can hold a stable slice 241 const user_id_stable = try self.session.arena.allocator().dupe(u8, &user_id); 242 self.session.data.user_id = user_id_stable; 243 self.session.data.login_failure = null; 244 245 // adopt the user's name and persisted prefs 246 try self.session.loadUser(); 247 248 // wipe the entered credentials so they don't linger if the 249 // user returns to this page after logging out 250 username_input.clear(allocator); 251 password_input.clear(allocator); 252 253 // leave the auth tab for the page it belongs to, where the 254 // web's /login redirect also lands 255 try self.session.navigate(self.session.data.current_page.pageRoot()); 256 }, 257 } 258 } 259 }; 260