╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │142 const uri = try std.Uri.parseAfterScheme("", request.head.target); │ │143 const path = uri.path.percent_encoded; │ │144 │ │145 - if (method == .POST and std.mem.eql(u8, path, "/login")) { │ │146 - return handleLogin(io, request, allocator, admin_repo_path, session_store); │ │145 + // login/logout are page-scoped. the base (the path minus the trailing verb) │ │146 + // is where we redirect afterward, so logging in keeps you on the page you │ │147 + // were on instead of jumping to the home view. │ │148 + if (method == .POST and std.mem.endsWith(u8, path, "/login")) { │ │149 + return handleLogin(io, request, allocator, path[0 .. path.len - "/login".len], admin_repo_path, session_store);│ │150 } │ │148 - if (method == .POST and std.mem.eql(u8, path, "/logout")) { │ │149 - return handleLogout(request, session_store); │ │151 + if (method == .POST and std.mem.endsWith(u8, path, "/logout")) { │ │152 + return handleLogout(request, path[0 .. path.len - "/logout".len], session_store); │ │153 } │ │151 - if (method == .POST and std.mem.eql(u8, path, "/ansi")) { │ │152 - return handleAnsi(io, request, allocator, admin_repo_path, session_store); │ │154 + if (method == .POST and std.mem.endsWith(u8, path, "/ansi")) { │ │155 + return handleAnsi(io, request, allocator, path[0 .. path.len - "/ansi".len], admin_repo_path, session_store); │ │156 } │ │157 │ │158 const get_or_head = method == .GET or method == .HEAD; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │239 io: std.Io, │ │240 request: *std.http.Server.Request,│ │241 allocator: std.mem.Allocator, │ │242 + base: []const u8, │ │243 admin_repo_path: []const u8, │ │244 session_store: SessionStore, │ │245 ) !void { │ ╰───────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │262 │ │263 const haxy_moment = try evt.currentMoment(evt.admin_repo_opts, &repo); │ │264 const result = try evt.User.verifyCredentials(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, &arena, username, password);│ │265 + │ │266 + // on success, return to the page the login came from (base, or "/" at the │ │267 + // root); on failure, stay on its auth tab to surface the error. │ │268 + const success_location: []const u8 = if (base.len == 0) "/" else base; │ │269 + const failure_location = try std.fmt.allocPrint(arena.allocator(), "{s}/auth", .{base}); │ │270 │ │271 switch (result) { │ │272 .success => |user_id| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │276 try request.respond("", .{ │ │277 .status = .see_other, │ │278 .extra_headers = &.{ │ │270 - .{ .name = "location", .value = ui.RoutablePage.url(.default) },│ │279 + .{ .name = "location", .value = success_location }, │ │280 .{ .name = "set-cookie", .value = cookie }, │ │281 }, │ │282 }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │285 try request.respond("", .{ │ │286 .status = .see_other, │ │287 .extra_headers = &.{ │ │279 - .{ .name = "location", .value = ui.RoutablePage.url(.auth) }, │ │288 + .{ .name = "location", .value = failure_location }, │ │289 .{ .name = "set-cookie", .value = login_failure_cookie ++ "=unknown_user; Path=/; HttpOnly; SameSite=Strict" },│ │290 }, │ │291 }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │294 try request.respond("", .{ │ │295 .status = .see_other, │ │296 .extra_headers = &.{ │ │288 - .{ .name = "location", .value = ui.RoutablePage.url(.auth) }, │ │297 + .{ .name = "location", .value = failure_location }, │ │298 .{ .name = "set-cookie", .value = login_failure_cookie ++ "=wrong_password; Path=/; HttpOnly; SameSite=Strict" },│ │299 }, │ │300 }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │302 } │ │303 } │ │304 │ │296 -fn handleLogout(request: *std.http.Server.Request, session_store: SessionStore) !void { │ │305 +fn handleLogout(request: *std.http.Server.Request, base: []const u8, session_store: SessionStore) !void {│ │306 // revoke the session server-side so the cookie is dead for anyone holding │ │307 // a copy, not just this browser. │ │308 if (getCookieValue(request, cookie_name)) |token| session_store.remove(token); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │309 + // return to the page the logout came from (base, or "/" at the root). │ │310 + const location: []const u8 = if (base.len == 0) "/" else base; │ │311 // close the connection instead of keeping it alive: we don't read the │ │312 // request body, and respond()'s keep-alive path would otherwise try to│ │313 // discard it — which asserts on a bodyless POST that carries no │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │317 .status = .see_other, │ │318 .keep_alive = false, │ │319 .extra_headers = &.{ │ │309 - .{ .name = "location", .value = ui.RoutablePage.url(.auth) }, │ │320 + .{ .name = "location", .value = location }, │ │321 .{ .name = "set-cookie", .value = cookie_name ++ "=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0" },│ │322 }, │ │323 }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │327 io: std.Io, │ │328 request: *std.http.Server.Request,│ │329 allocator: std.mem.Allocator, │ │330 + base: []const u8, │ │331 admin_repo_path: []const u8, │ │332 session_store: SessionStore, │ │333 ) !void { │ ╰───────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯