╭ 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 pub fn init() Self { 24 return .{}; 25 } 26 27 pub const View = struct { 28 center: ui.Center, 29 data: *const Self, 30 session: *ui.Session, 31 nav_ids: [3]usize, 32 status: Status, 33 // focus id of the header's "users" tab; on a successful submit we jump 34 // focus there so the user isn't stranded on the login button. 35 users_tab_id: usize, 36 37 pub const Status = enum { none, success, wrong_password, unknown_user }; 38 39 const username_index: usize = 0; 40 const password_index: usize = 1; 41 const button_index: usize = 2; 42 43 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session, users_tab_id: usize) !View { 44 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); 45 errdefer box.deinit(allocator); 46 47 var nav_ids: [3]usize = undefined; 48 49 { 50 var username = wgt.TextInput(ui.Widget).init(allocator, .{ .label = " username ", .name = "username" }); 51 errdefer username.deinit(allocator); 52 username.getFocus().focusable = true; 53 nav_ids[username_index] = username.getFocus().id; 54 try box.children.put(allocator, username.getFocus().id, .{ 55 .widget = .{ .text_input = username }, 56 .rect = null, 57 .min_size = null, 58 }); 59 } 60 61 { 62 var password = wgt.TextInput(ui.Widget).init(allocator, .{ .label = " password ", .password = true, .name = "password" }); 63 errdefer password.deinit(allocator); 64 password.getFocus().focusable = true; 65 nav_ids[password_index] = password.getFocus().id; 66 try box.children.put(allocator, password.getFocus().id, .{ 67 .widget = .{ .text_input = password }, 68 .rect = null, 69 .min_size = null, 70 }); 71 } 72 73 { 74 var button = try wgt.TextBox(ui.Widget).init(allocator, "login", .{ .border_style = .single, .wrap_kind = .none }); 75 errdefer button.deinit(allocator); 76 button.getFocus().focusable = true; 77 // the renderer distinguishes plain clickables from buttons that 78 // should POST to a server route by this kind. 79 button.getFocus().kind = .submit_button; 80 nav_ids[button_index] = button.getFocus().id; 81 try box.children.put(allocator, button.getFocus().id, .{ 82 .widget = .{ .text_box = button }, 83 .rect = null, 84 .min_size = null, 85 }); 86 } 87 88 box.getFocus().child_id = nav_ids[username_index]; 89 90 return .{ 91 .center = try ui.Center.init(allocator, .{ .box = box }, .both), 92 .data = data, 93 .session = session, 94 .nav_ids = nav_ids, 95 .status = .none, 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 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 username_input = &box.children.values()[username_index].widget.text_input; 114 username_input.options.label = if (self.status == .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 (self.status == .wrong_password) 121 " password (invalid) " 122 else 123 " password "; 124 125 try self.center.build(allocator, constraint, root_focus); 126 } 127 128 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 129 const box = &self.center.child.box; 130 const child_id = box.focus.child_id orelse return; 131 const current = self.indexOf(child_id) orelse return; 132 133 switch (key) { 134 // Shift+Tab walks backward through the form, matching the 135 // typical UX; arrow_up does the same. 136 .arrow_up, .back_tab => if (current > 0) { 137 try root_focus.setFocus(self.nav_ids[current - 1]); 138 return; 139 }, 140 // Tab moves to the next field, matching the form-style UX users 141 // expect; arrow_down does the same. 142 .arrow_down, .tab => if (current + 1 < self.nav_ids.len) { 143 try root_focus.setFocus(self.nav_ids[current + 1]); 144 return; 145 }, 146 // submit on Enter from any nav field — typical "press Enter in 147 // a form to submit" UX 148 .enter => { 149 try self.submit(allocator, root_focus); 150 return; 151 }, 152 else => {}, 153 } 154 155 if (current == button_index) { 156 switch (key) { 157 .mouse => |mouse| { 158 if (mouse.action == .press and mouse.action.press == .left) { 159 if (root_focus.children.get(child_id)) |entry| { 160 const r = entry.rect; 161 if (mouse.x >= r.x and mouse.y >= r.y and 162 mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) 163 { 164 try self.submit(allocator, root_focus); 165 return; 166 } 167 } 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 self.status = .none; 187 } 188 } 189 190 pub fn clearGrid(self: *View) void { 191 self.center.clearGrid(); 192 } 193 194 pub fn getGrid(self: View) ?Grid { 195 return self.center.getGrid(); 196 } 197 198 pub fn getFocus(self: *View) *Focus { 199 return self.center.getFocus(); 200 } 201 202 pub fn getSelectedIndex(self: View) ?usize { 203 const child_id = self.center.child.box.focus.child_id orelse return null; 204 return self.indexOf(child_id); 205 } 206 207 fn indexOf(self: View, child_id: usize) ?usize { 208 for (self.nav_ids, 0..) |id, i| { 209 if (id == child_id) return i; 210 } 211 return null; 212 } 213 214 fn submit(self: *View, allocator: std.mem.Allocator, root_focus: *Focus) !void { 215 if (comptime wasm) { 216 // no DB cursor available on the wasm render path 217 self.status = .unknown_user; 218 return; 219 } 220 221 const box = &self.center.child.box; 222 const username_input = &box.children.values()[username_index].widget.text_input; 223 const password_input = &box.children.values()[password_index].widget.text_input; 224 225 var username_buf: [256]u8 = undefined; 226 const username = joinCodepoints(&username_buf, username_input.content.items) orelse { 227 self.status = .unknown_user; 228 return; 229 }; 230 231 var password_buf: [256]u8 = undefined; 232 const password = joinCodepoints(&password_buf, password_input.content.items) orelse { 233 self.status = .wrong_password; 234 return; 235 }; 236 237 const haxy_moment = self.session.haxy_moment orelse { 238 // no DB context (e.g. wasm/web rendering path); treat as unknown. 239 self.status = .unknown_user; 240 return; 241 }; 242 const arena = self.session.arena orelse { 243 self.status = .unknown_user; 244 return; 245 }; 246 247 const result = try evt.User.verifyCredentials(DB, repo_opts.hash, haxy_moment, arena, username, password); 248 switch (result) { 249 .unknown_user => { 250 self.status = .unknown_user; 251 return; 252 }, 253 .wrong_password => { 254 self.status = .wrong_password; 255 return; 256 }, 257 .success => |user_id| { 258 // dupe user_id into the arena so the session can hold a stable slice 259 const user_id_stable = try arena.allocator().dupe(u8, &user_id); 260 self.session.user_id = user_id_stable; 261 self.status = .success; 262 263 // wipe the entered credentials so they don't linger if the 264 // user returns to this page after logging out. 265 username_input.clear(allocator); 266 password_input.clear(allocator); 267 268 // jump focus back to the users tab — the login button we 269 // just pressed is about to be hidden by the tab-label swap. 270 try root_focus.setFocus(self.users_tab_id); 271 }, 272 } 273 } 274 }; 275 276 // joins a list of utf-8 codepoint slices (as TextInput stores them) into a 277 // flat buffer. returns null if the joined bytes don't fit. 278 fn joinCodepoints(buf: []u8, codepoints: []const []const u8) ?[]const u8 { 279 var len: usize = 0; 280 for (codepoints) |cp| { 281 if (len + cp.len > buf.len) return null; 282 @memcpy(buf[len..][0..cp.len], cp); 283 len += cp.len; 284 } 285 return buf[0..len]; 286 } 287