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