╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │159 return; │ │160 } │ │161 │ │162 - if (std.mem.eql(u8, path, "/") or std.mem.eql(u8, path, "/index.html")) { │ │163 - const user_id_hex = getCookieValue(request, cookie_name); │ │162 + if (ui.RoutablePage.fromUrl(path)) |current_page| { │ │163 + // decode the haxy_user cookie into raw bytes on the stack; the │ │164 + // slice lives for the rest of handleRequest, which is plenty for │ │165 + // renderIndexHtml to consume. │ │166 + var user_id_buf: [evt.event_id_size]u8 = undefined; │ │167 + const user_id: ?[]const u8 = blk: { │ │168 + const hex = getCookieValue(request, cookie_name) orelse break :blk null;│ │169 + if (hex.len != evt.event_id_size * 2) break :blk null; │ │170 + _ = std.fmt.hexToBytes(&user_id_buf, hex) catch break :blk null; │ │171 + break :blk user_id_buf[0..evt.event_id_size]; │ │172 + }; │ │173 const login_failure: ?ui.Home.Auth.Login.Failure = │ │174 if (getCookieValue(request, login_failure_cookie)) |raw| │ │175 if (std.mem.eql(u8, raw, "unknown_user")) │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │180 null │ │181 else │ │182 null; │ │174 - const html = try renderIndexHtml(io, allocator, admin_repo_path, user_id_hex, login_failure);│ │183 + const html = try renderIndexHtml(io, allocator, admin_repo_path, .{ │ │184 + .user_id = user_id, │ │185 + .login_failure = login_failure, │ │186 + .current_page = current_page, │ │187 + }); │ │188 defer allocator.free(html); │ │189 // expire the flash cookie on the way out so a refresh doesn't keep │ │190 // showing the failure label. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │247 try request.respond("", .{ │ │248 .status = .see_other, │ │249 .extra_headers = &.{ │ │237 - .{ .name = "location", .value = "/" }, │ │250 + .{ .name = "location", .value = ui.RoutablePage.url(.home_users) },│ │251 .{ .name = "set-cookie", .value = cookie }, │ │252 }, │ │253 }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │256 try request.respond("", .{ │ │257 .status = .see_other, │ │258 .extra_headers = &.{ │ │246 - .{ .name = "location", .value = "/" }, │ │259 + .{ .name = "location", .value = ui.RoutablePage.url(.home_auth) }, │ │260 .{ .name = "set-cookie", .value = login_failure_cookie ++ "=unknown_user; Path=/; HttpOnly; SameSite=Strict" },│ │261 }, │ │262 }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │265 try request.respond("", .{ │ │266 .status = .see_other, │ │267 .extra_headers = &.{ │ │255 - .{ .name = "location", .value = "/" }, │ │268 + .{ .name = "location", .value = ui.RoutablePage.url(.home_auth) }, │ │269 .{ .name = "set-cookie", .value = login_failure_cookie ++ "=wrong_password; Path=/; HttpOnly; SameSite=Strict" },│ │270 }, │ │271 }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │280 try request.respond("", .{ │ │281 .status = .see_other, │ │282 .extra_headers = &.{ │ │270 - .{ .name = "location", .value = "/" }, │ │283 + .{ .name = "location", .value = ui.RoutablePage.url(.home_auth) }, │ │284 .{ .name = "set-cookie", .value = cookie_name ++ "=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0" },│ │285 }, │ │286 }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │290 io: std.Io, │ │291 allocator: std.mem.Allocator, │ │292 admin_repo_path: []const u8, │ │280 - user_id_hex_opt: ?[]const u8, │ │281 - login_failure: ?ui.Home.Auth.Login.Failure, │ │293 + session_data: ui.SessionData, │ │294 ) ![]const u8 { │ │295 const template = (findEmbed("/index.html") orelse return error.MissingIndexAsset).body;│ │296 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │302 var page_arena = std.heap.ArenaAllocator.init(allocator); │ │303 defer page_arena.deinit(); │ │304 │ │293 - var session: ui.Session = .{}; │ │294 - if (user_id_hex_opt) |hex| { │ │295 - if (decodeHexAlloc(page_arena.allocator(), hex)) |bytes| { │ │296 - session.data.user_id = bytes; │ │297 - } else |_| {} │ │298 - } │ │299 - session.data.login_failure = login_failure; │ │305 + var session: ui.Session = .{ .data = session_data }; │ │306 │ │307 const snapshot: ui.Snapshot = .{ │ │308 .page = .{ .home = try .init(repo_opts, &page_arena, &repo, &session) },│ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │463 } │ │464 }.lt); │ │465 │ │460 - const action_url: []const u8 = if (session.data.user_id != null) "/logout" else "/login";│ │466 + // only the auth page hosts a form right now; on any other page we │ │467 + // emit nothing. on auth, the URL depends on whether there's already │ │468 + // an active session. │ │469 + const action_url: []const u8 = switch (session.data.current_page) { │ │470 + .home_auth => if (session.data.user_id != null) "/logout" else "/login", │ │471 + .home_users, .home_repos => return try out.toOwnedSlice(allocator), │ │472 + }; │ │473 │ │474 try out.appendSlice(allocator, "<form action=\""); │ │475 try out.appendSlice(allocator, action_url); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │582 return null; │ │583 } │ │584 │ │573 -fn decodeHexAlloc(allocator: std.mem.Allocator, hex: []const u8) ![]u8 { │ │574 - if (hex.len % 2 != 0) return error.InvalidHex; │ │575 - const bytes = try allocator.alloc(u8, hex.len / 2); │ │576 - errdefer allocator.free(bytes); │ │577 - _ = std.fmt.hexToBytes(bytes, hex) catch return error.InvalidHex; │ │578 - return bytes; │ │579 -} │ │580 - │ │585 fn parseFormField(allocator: std.mem.Allocator, body: []const u8, key: []const u8) !?[]u8 {│ │586 var iter = std.mem.splitScalar(u8, body, '&'); │ │587 while (iter.next()) |pair| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯