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