╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Login.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
101 self.center.deinit(allocator); 102 } 103 104 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 105 const box = &self.center.child.box; 106 107 const button = &box.children.values()[button_index].widget.text_box; 108 button.options.border_style = if (root_focus.grandchild_id == self.nav_ids[button_index]) 109 .double 110 else 111 .single; 112 113 const failure = self.session.data.login_failure; 114 115 const username_input = &box.children.values()[username_index].widget.text_input; 116 username_input.options.label = if (failure == .unknown_user) 117 " username (invalid) " 118 else 119 " username "; 120 121 const password_input = &box.children.values()[password_index].widget.text_input; 122 password_input.options.label = if (failure == .wrong_password) 123 " password (invalid) " 124 else 125 " password "; 126 127 try self.center.build(allocator, constraint, root_focus); 128 } 129 130 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 131 const box = &self.center.child.box; 132 const child_id = box.focus.child_id orelse return; 133 const current = self.indexOf(child_id) orelse return; 134 135 switch (key) { 136 // Shift+Tab walks backward through the form, matching the 137 // typical UX; arrow_up does the same. 138 .arrow_up, .back_tab => if (current > 0) { 139 try root_focus.setFocus(self.nav_ids[current - 1]); 140 return; 141 }, 142 // Tab moves to the next field, matching the form-style UX users 143 // expect; arrow_down does the same. 144 .arrow_down, .tab => if (current + 1 < self.nav_ids.len) { 145 try root_focus.setFocus(self.nav_ids[current + 1]); 146 return; 147 }, 148 // submit on Enter from any nav field — typical "press Enter in 149 // a form to submit" UX 150 .enter => { 151 try self.submit(allocator, root_focus); 152 return; 153 }, 154 else => {}, 155 } 156 157 if (current == button_index) { 158 switch (key) { 159 .mouse => |mouse| { 160 if (mouse.action == .press and mouse.action.press == .left) { 161 if (root_focus.children.get(child_id)) |entry| { 162 const r = entry.rect; 163 if (mouse.x >= r.x and mouse.y >= r.y and 164 mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) 165 { 166 try self.submit(allocator, root_focus); 167 return; 168 } 169 } 170 } 171 }, 172 else => {}, 173 } 174 } 175 176 const username_input = &box.children.values()[username_index].widget.text_input; 177 const password_input = &box.children.values()[password_index].widget.text_input; 178 const username_len_before = username_input.content.items.len; 179 const password_len_before = password_input.content.items.len; 180 181 if (box.children.getIndex(child_id)) |idx| { 182 try box.children.values()[idx].widget.input(allocator, key, root_focus); 183 } 184 185 if (username_input.content.items.len != username_len_before or 186 password_input.content.items.len != password_len_before) 187 { 188 // any edit clears the failure flag so the "(invalid)" label 189 // doesn't linger after the user has typed a correction. 190 self.session.data.login_failure = null; 191 } 192 } 193 194 pub fn clearGrid(self: *View) void { 195 self.center.clearGrid(); 196 } 197 198 pub fn getGrid(self: View) ?Grid { 199 return self.center.getGrid(); 200 } 201 202 pub fn getFocus(self: *View) *Focus { 203 return self.center.getFocus(); 204 } 205 206 pub fn getSelectedIndex(self: View) ?usize { 207 const child_id = self.center.child.box.focus.child_id orelse return null; 208 return self.indexOf(child_id); 209 } 210 211 fn indexOf(self: View, child_id: usize) ?usize { 212 for (self.nav_ids, 0..) |id, i| { 213 if (id == child_id) return i; 214 } 215 return null; 216 } 217 218 fn submit(self: *View, allocator: std.mem.Allocator, root_focus: *Focus) !void { 219 if (comptime wasm) { 220 // no DB cursor available on the wasm render path 221 self.session.data.login_failure = .unknown_user; 222 return; 223 } 224 225 const box = &self.center.child.box; 226 const username_input = &box.children.values()[username_index].widget.text_input; 227 const password_input = &box.children.values()[password_index].widget.text_input; 228 229 var username_buf: [256]u8 = undefined; 230 const username = joinCodepoints(&username_buf, username_input.content.items) orelse { 231 self.session.data.login_failure = .unknown_user; 232 return; 233 }; 234 235 var password_buf: [256]u8 = undefined; 236 const password = joinCodepoints(&password_buf, password_input.content.items) orelse { 237 self.session.data.login_failure = .wrong_password; 238 return; 239 }; 240 241 const haxy_moment = self.session.haxy_moment orelse { 242 // no DB context (e.g. wasm/web rendering path); treat as unknown. 243 self.session.data.login_failure = .unknown_user; 244 return; 245 }; 246 const arena = self.session.arena orelse { 247 self.session.data.login_failure = .unknown_user; 248 return; 249 }; 250 251 const result = try evt.User.verifyCredentials(DB, repo_opts.hash, haxy_moment, arena, username, password); 252 switch (result) { 253 .unknown_user => { 254 self.session.data.login_failure = .unknown_user; 255 return; 256 }, 257 .wrong_password => { 258 self.session.data.login_failure = .wrong_password; 259 return; 260 }, 261 .success => |user_id| { 262 // dupe user_id into the arena so the session can hold a stable slice 263 const user_id_stable = try arena.allocator().dupe(u8, &user_id); 264 self.session.data.user_id = user_id_stable; 265 self.session.data.login_failure = null; 266 267 // wipe the entered credentials so they don't linger if the 268 // user returns to this page after logging out. 269 username_input.clear(allocator); 270 password_input.clear(allocator); 271 272 // jump focus back to the users tab — the login button we 273 // just pressed is about to be hidden by the tab-label swap. 274 try root_focus.setFocus(self.users_tab_id); 275 }, 276 } 277 } 278 }; 279 280 // joins a list of utf-8 codepoint slices (as TextInput stores them) into a 281 // flat buffer. returns null if the joined bytes don't fit. 282 fn joinCodepoints(buf: []u8, codepoints: []const []const u8) ?[]const u8 { 283 var len: usize = 0; 284 for (codepoints) |cp| { 285 if (len + cp.len > buf.len) return null; 286 @memcpy(buf[len..][0..cp.len], cp); 287 len += cp.len; 288 } 289 return buf[0..len]; 290 } 291