╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │ 9 const Focus = xitui.focus.Focus; │ │10 │ │11 const cookie_name = "haxy_user"; │ │12 +// flash cookie for surfacing the outcome of the most recent /login POST.│ │13 +// set on the failure redirect, read and immediately expired on the next │ │14 +// GET / so refreshing the page doesn't keep showing the error. │ │15 +const login_failure_cookie = "haxy_login_failure"; │ │16 │ │17 const Embed = struct { │ │18 path: []const u8, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │159 // consuming the request, so we don't dispatch through findEmbed here. │ │160 if (std.mem.eql(u8, path, "/") or std.mem.eql(u8, path, "/index.html")) {│ │161 const user_id_hex = getCookieValue(request, cookie_name); │ │162 + const login_failure: ?ui.Home.Auth.Login.Failure = │ │163 + if (getCookieValue(request, login_failure_cookie)) |raw| │ │164 + if (std.mem.eql(u8, raw, "unknown_user")) │ │165 + .unknown_user │ │166 + else if (std.mem.eql(u8, raw, "wrong_password")) │ │167 + .wrong_password │ │168 + else │ │169 + null │ │170 + else │ │171 + null; │ │172 if (http_server.reader.state == .received_head) { │ │173 http_server.reader.state = .ready; │ │174 } │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │161 - const html = try renderIndexHtml(io, allocator, admin_repo_path, user_id_hex); │ │175 + const html = try renderIndexHtml(io, allocator, admin_repo_path, user_id_hex, login_failure); │ │176 defer allocator.free(html); │ │163 - try writeStaticResponse(http_server, 200, "OK", "text/html; charset=utf-8", html, method == .HEAD);│ │177 + // expire the flash cookie on the way out — it's been consumed by │ │178 + // this render and shouldn't survive a refresh. │ │179 + const clear_flash: []const u8 = if (login_failure != null) │ │180 + "Set-Cookie: " ++ login_failure_cookie ++ "=; Path=/; Max-Age=0\r\n" │ │181 + else │ │182 + ""; │ │183 + try http_server.out.print( │ │184 + "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n{s}Content-Length: {d}\r\n\r\n", │ │185 + .{ clear_flash, html.len }, │ │186 + ); │ │187 + if (method != .HEAD) try http_server.out.writeAll(html); │ │188 return; │ │189 } │ │190 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │238 .{hex}, │ │239 ); │ │240 }, │ │217 - .unknown_user, .wrong_password => { │ │241 + .unknown_user => { │ │242 + try http_server.out.print( │ │243 + "HTTP/1.1 303 See Other\r\nLocation: /\r\nSet-Cookie: " ++ login_failure_cookie ++ "=unknown_user; Path=/; HttpOnly; SameSite=Strict\r\nContent-Length: 0\r\n\r\n", │ │244 + .{}, │ │245 + ); │ │246 + }, │ │247 + .wrong_password => { │ │248 try http_server.out.print( │ │219 - "HTTP/1.1 303 See Other\r\nLocation: /\r\nContent-Length: 0\r\n\r\n", │ │249 + "HTTP/1.1 303 See Other\r\nLocation: /\r\nSet-Cookie: " ++ login_failure_cookie ++ "=wrong_password; Path=/; HttpOnly; SameSite=Strict\r\nContent-Length: 0\r\n\r\n",│ │250 .{}, │ │251 ); │ │252 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │270 allocator: std.mem.Allocator, │ │271 admin_repo_path: []const u8, │ │272 user_id_hex_opt: ?[]const u8, │ │273 + login_failure: ?ui.Home.Auth.Login.Failure, │ │274 ) ![]const u8 { │ │275 const template = (findEmbed("/index.html") orelse return error.MissingIndexAsset).body;│ │276 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │285 var session: ui.Session = .{}; │ │286 if (user_id_hex_opt) |hex| { │ │287 if (decodeHexAlloc(page_arena.allocator(), hex)) |bytes| { │ │257 - session.user_id = bytes; │ │288 + session.data.user_id = bytes; │ │289 } else |_| {} │ │290 } │ │291 + session.data.login_failure = login_failure; │ │292 │ │261 - const page: ui.Page = .{ .home = try .init(repo_opts, &page_arena, &repo, &session) };│ │262 - var root = try ui.initRoot(allocator, &page, &session); │ │293 + const snapshot: ui.Snapshot = .{ │ │294 + .page = .{ .home = try .init(repo_opts, &page_arena, &repo, &session) }, │ │295 + .session = session.data, │ │296 + }; │ │297 + var root = try ui.initRoot(allocator, &snapshot.page, &session); │ │298 defer root.deinit(allocator); │ │299 │ │300 const content = try generateHtml(allocator, &root, &session); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │301 defer allocator.free(content); │ │302 │ │268 - // serialize the page so the wasm side can parse it back without making │ │269 - // a second request. base64-encoded so the json can be embedded in html. │ │303 + // serialize the snapshot so the wasm side can parse it back without │ │304 + // making a second request. base64-encoded so the json can be embedded │ │305 + // safely inside the host html. │ │306 var json: std.Io.Writer.Allocating = .init(allocator); │ │307 defer json.deinit(); │ │272 - try std.json.Stringify.value(page, .{}, &json.writer); │ │308 + try std.json.Stringify.value(snapshot, .{}, &json.writer); │ │309 │ │310 const b64 = std.base64.standard.Encoder; │ │311 const json_b64 = try allocator.alloc(u8, b64.calcSize(json.written().len));│ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │388 │ │389 // determines the form route for any submit_button on this page. only one │ │390 // is visible at a time: login when logged out, logout when logged in. │ │355 - const submit_url: []const u8 = if (session.user_id != null) "/logout" else "/login"; │ │391 + const submit_url: []const u8 = if (session.data.user_id != null) "/logout" else "/login";│ │392 │ │393 // collect the text inputs in deterministic top-to-bottom, left-to-right │ │394 // order. root_focus.children is a hash map so iterating it directly would │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯