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