╭ Login.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 const evt = @import("../../../event.zig"); 4 const ui = @import("../../../ui.zig"); 5 const xit = @import("xit"); 6 const rp = xit.repo; 7 const hash = xit.hash; 8 const xitui = xit.xitui; 9 const wgt = xitui.widget; 10 const layout = xitui.layout; 11 const inp = xitui.input; 12 const Grid = xitui.grid.Grid; 13 const Focus = xitui.focus.Focus; 14 const bcrypt = std.crypto.pwhash.bcrypt; 15 16 const wasm = builtin.target.cpu.arch == .wasm32; 17 18 const repo_opts = ui.canonical_repo_opts; 19 const DB = ui.DB; 20 21 const Self = @This(); 22 23 // classifies a failed /login attempt; null in session.data.login_failure means 24 // "no failure to surface". 25 pub const Failure = enum { wrong_password, unknown_user }; 26 27 pub fn init() Self { 28 return .{}; 29 } 30 31 pub const View = struct { 32 center: ui.Center, 33 data: *const Self, 34 session: *ui.Session, 35 nav_ids: [3]usize, 36 // focus id of the header's "users" tab; on a successful submit we jump 37 // focus there so the user isn't stranded on the login button. 38 users_tab_id: usize, 39 40 const username_index: usize = 0; 41 const password_index: usize = 1; 42 const button_index: usize = 2; 43 44 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session, users_tab_id: usize) !View { 45 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert }); 46 errdefer box.deinit(allocator); 47 48 var nav_ids: [3]usize = undefined; 49 50 { 51 var username = wgt.TextInput(ui.Widget).init(allocator, .{ .label = " username ", .name = "username", .rounded_corners = true }); 52 errdefer username.deinit(allocator); 53 username.getFocus().focusable = true; 54 nav_ids[username_index] = username.getFocus().id; 55 try box.children.put(allocator, username.getFocus().id, .{ 56 .widget = .{ .text_input = username }, 57 .rect = null, 58 .min_size = null, 59 }); 60 } 61 62 { 63 var password = wgt.TextInput(ui.Widget).init(allocator, .{ .label = " password ", .password = true, .name = "password", .rounded_corners = true }); 64 errdefer password.deinit(allocator); 65 password.getFocus().focusable = true; 66 nav_ids[password_index] = password.getFocus().id; 67 try box.children.put(allocator, password.getFocus().id, .{ 68 .widget = .{ .text_input = password }, 69 .rect = null, 70 .min_size = null, 71 }); 72 } 73 74 { 75 var button = try wgt.TextBox(ui.Widget).init(allocator, "login", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 76 errdefer button.deinit(allocator); 77 button.getFocus().focusable = true; 78 // the renderer distinguishes plain clickables from buttons that 79 // should POST to a server route by this kind. 80 button.getFocus().kind = .{ .custom = "submit" }; 81 nav_ids[button_index] = button.getFocus().id; 82 try box.children.put(allocator, button.getFocus().id, .{ 83 .widget = .{ .text_box = button }, 84 .rect = null, 85 .min_size = null, 86 }); 87 } 88 89 box.getFocus().child_id = nav_ids[username_index]; 90 91 return .{ 92 .center = try ui.Center.init(allocator, .{ .box = box }, .both), 93 .data = data, 94 .session = session, 95 .nav_ids = nav_ids, 96 .users_tab_id = users_tab_id, 97 }; 98 } 99 100 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 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 failure = self.session.data.login_failure; 108 109 const username_input = &box.children.values()[username_index].widget.text_input; 110 username_input.options.label = if (failure == .unknown_user) 111 " username (invalid) " 112 else 113 " username "; 114 115 const password_input = &box.children.values()[password_index].widget.text_input; 116 password_input.options.label = if (failure == .wrong_password) 117 " password (invalid) " 118 else 119 " password "; 120 121 try self.center.build(allocator, constraint, root_focus); 122 } 123 124 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 125 const box = &self.center.child.box; 126 const child_id = box.focus.child_id orelse return; 127 const current = self.indexOf(child_id) orelse return; 128 129 switch (key) { 130 // Shift+Tab walks backward through the form, matching the 131 // typical UX; arrow_up does the same. 132 .arrow_up, .back_tab => if (current > 0) { 133 try root_focus.setFocus(self.nav_ids[current - 1]); 134 return; 135 }, 136 // Tab moves to the next field, matching the form-style UX users 137 // expect; arrow_down does the same. 138 .arrow_down, .tab => if (current + 1 < self.nav_ids.len) { 139 try root_focus.setFocus(self.nav_ids[current + 1]); 140 return; 141 }, 142 // submit on Enter from any nav field — typical "press Enter in 143 // a form to submit" UX 144 .enter => { 145 try self.submit(allocator, root_focus); 146 return; 147 }, 148 else => {}, 149 } 150 151 if (current == button_index) { 152 switch (key) { 153 .mouse => |mouse| { 154 if (mouse.action == .press and mouse.action.press == .left) { 155 if (root_focus.children.get(child_id)) |entry| { 156 const r = entry.rect; 157 if (mouse.x >= r.x and mouse.y >= r.y and 158 mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) 159 { 160 try self.submit(allocator, root_focus); 161 return; 162 } 163 } 164 } 165 }, 166 else => {}, 167 } 168 } 169 170 const username_input = &box.children.values()[username_index].widget.text_input; 171 const password_input = &box.children.values()[password_index].widget.text_input; 172 const username_len_before = username_input.content.items.len; 173 const password_len_before = password_input.content.items.len; 174 175 if (box.children.getIndex(child_id)) |idx| { 176 try box.children.values()[idx].widget.input(allocator, key, root_focus); 177 } 178 179 if (username_input.content.items.len != username_len_before or 180 password_input.content.items.len != password_len_before) 181 { 182 // any edit clears the failure flag so the "(invalid)" label 183 // doesn't linger after the user has typed a correction. 184 self.session.data.login_failure = null; 185 } 186 } 187 188 pub fn clearGrid(self: *View) void { 189 self.center.clearGrid(); 190 } 191 192 pub fn getGrid(self: View) ?Grid { 193 return self.center.getGrid(); 194 } 195 196 pub fn getFocus(self: *View) *Focus { 197 return self.center.getFocus(); 198 } 199 200 pub fn getSelectedIndex(self: View) ?usize { 201 const child_id = self.center.child.box.focus.child_id orelse return null; 202 return self.indexOf(child_id); 203 } 204 205 fn indexOf(self: View, child_id: usize) ?usize { 206 for (self.nav_ids, 0..) |id, i| { 207 if (id == child_id) return i; 208 } 209 return null; 210 } 211 212 fn submit(self: *View, allocator: std.mem.Allocator, root_focus: *Focus) !void { 213 if (comptime wasm) { 214 // no DB cursor available on the wasm render path 215 self.session.data.login_failure = .unknown_user; 216 return; 217 } 218 219 const box = &self.center.child.box; 220 const username_input = &box.children.values()[username_index].widget.text_input; 221 const password_input = &box.children.values()[password_index].widget.text_input; 222 223 const username = try username_input.text(allocator); 224 defer allocator.free(username); 225 226 const password = try password_input.text(allocator); 227 defer allocator.free(password); 228 229 const haxy_moment = self.session.haxy_moment orelse { 230 // no DB context (e.g. wasm/web rendering path); treat as unknown. 231 self.session.data.login_failure = .unknown_user; 232 return; 233 }; 234 235 const result = try evt.User.verifyCredentials(DB, repo_opts.hash, haxy_moment, self.session.arena, username, password); 236 switch (result) { 237 .unknown_user => { 238 self.session.data.login_failure = .unknown_user; 239 return; 240 }, 241 .wrong_password => { 242 self.session.data.login_failure = .wrong_password; 243 return; 244 }, 245 .success => |user_id| { 246 // dupe user_id into the arena so the session can hold a stable slice 247 const user_id_stable = try self.session.arena.allocator().dupe(u8, &user_id); 248 self.session.data.user_id = user_id_stable; 249 self.session.data.login_failure = null; 250 251 // wipe the entered credentials so they don't linger if the 252 // user returns to this page after logging out. 253 username_input.clear(allocator); 254 password_input.clear(allocator); 255 256 // jump focus back to the users tab — the login button we 257 // just pressed is about to be hidden by the tab-label swap. 258 try root_focus.setFocus(self.users_tab_id); 259 }, 260 } 261 } 262 }; 263