╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │25 .{ .path = "script.js", .content_type = "text/javascript; charset=utf-8", .body = @embedFile("embed/script.js") },│ │26 .{ .path = "term.ttf", .content_type = "font/ttf", .body = @embedFile("embed/term.ttf") }, │ │27 .{ .path = "haxy.wasm", .content_type = "application/wasm", .body = @embedFile("haxy.wasm") }, │ │28 +}; │ │29 + │ │30 +// what a web request is served from: the multi-user server (admin repo + │ │31 +// login sessions) or a single local repo. │ │32 +pub const Host = union(enum) { │ │33 + remote: struct { │ │34 + admin_repo_path: []const u8, │ │35 + session_store: SessionStore, │ │36 + }, │ │37 + local: ui.RepoSource, │ │38 }; │ │39 │ │40 pub fn handleConnection( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │41 io: std.Io, │ │42 allocator: std.mem.Allocator, │ │43 stream: std.Io.net.Stream, │ │34 - admin_repo_path: []const u8, │ │35 - session_store: SessionStore, │ │44 + host: Host, │ │45 err: *std.Io.Writer, │ │46 ) !void { │ │47 var send_buffer = [_]u8{0} ** 4096;│ ╰───────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │61 else => |e| return e, │ │62 }; │ │63 │ │55 - handleRequest(io, &request, allocator, admin_repo_path, session_store) catch |request_err| {│ │64 + handleRequest(io, &request, allocator, host) catch |request_err| { │ │65 try err.print("web ui request failed: {s}\n", .{@errorName(request_err)}); │ │66 try err.flush(); │ │67 // best-effort 500. if handleRequest already started writing a │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │79 io: std.Io, │ │80 request: *std.http.Server.Request, │ │81 allocator: std.mem.Allocator, │ │73 - admin_repo_path: []const u8, │ │74 - session_store: SessionStore, │ │82 + host: Host, │ │83 ) !void { │ │84 const method = request.head.method; │ │85 const uri = try std.Uri.parseAfterScheme("", request.head.target);│ ╰──────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 87 │ │ 88 // POST routes can be scoped by any page, so they can redirect using │ │ 89 // the base of the URL. this allows logging in to keep you on the page │ │ 82 - // you were on. │ │ 90 + // you were on. local mode has no accounts, so no POST routes. │ │ 91 if (method == .POST) { │ │ 84 - const PostRoute = enum { login, logout, ansi }; │ │ 85 - inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │ 86 - const suffix = "/" ++ field.name; │ │ 87 - if (std.mem.endsWith(u8, path, suffix)) { │ │ 88 - const base = path[0 .. path.len - suffix.len]; │ │ 89 - return switch (@field(PostRoute, field.name)) { │ │ 90 - .login => handleLogin(io, request, allocator, base, admin_repo_path, session_store), │ │ 91 - .logout => handleLogout(request, base, session_store), │ │ 92 - .ansi => handleAnsi(io, request, allocator, base, admin_repo_path, session_store), │ │ 93 - }; │ │ 94 - } │ │ 92 + switch (host) { │ │ 93 + .remote => |remote| { │ │ 94 + const PostRoute = enum { login, logout, ansi }; │ │ 95 + inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │ 96 + const suffix = "/" ++ field.name; │ │ 97 + if (std.mem.endsWith(u8, path, suffix)) { │ │ 98 + const base = path[0 .. path.len - suffix.len]; │ │ 99 + return switch (@field(PostRoute, field.name)) { │ │100 + .login => handleLogin(io, request, allocator, base, remote.admin_repo_path, remote.session_store),│ │101 + .logout => handleLogout(request, base, remote.session_store), │ │102 + .ansi => handleAnsi(io, request, allocator, base, remote.admin_repo_path, remote.session_store), │ │103 + }; │ │104 + } │ │105 + } │ │106 + }, │ │107 + .local => {}, │ │108 } │ │109 } │ │110 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │125 return; │ │126 } │ │127 │ │115 - if (ui.RoutablePage.fromUrl(path, if (uri.query) |comp| comp.percent_encoded else null)) |current_page| {│ │128 + const query = if (uri.query) |comp| comp.percent_encoded else null; │ │129 + const current_page_maybe = switch (host) { │ │130 + .remote => ui.RoutablePage.fromUrl(path, query), │ │131 + .local => ui.RoutablePage.fromUrlLocal(path, query), │ │132 + }; │ │133 + if (current_page_maybe) |current_page| { │ │134 // resolve the haxy_session cookie's token to a user_id via the │ │135 // store. user_id_buf lives on the stack for the rest of handleRequest, │ │118 - // which is plenty for renderIndexHtml to consume. │ │136 + // which is plenty for renderIndexHtml to consume. local mode has no │ │137 + // accounts, so it is always logged out. │ │138 var user_id_buf: [evt.event_id_size]u8 = undefined; │ │120 - const user_id: ?[]const u8 = blk: { │ │121 - const token = getCookieValue(request, cookie_name) orelse break :blk null; │ │122 - if (!session_store.lookup(token, &user_id_buf)) break :blk null; │ │123 - break :blk user_id_buf[0..evt.event_id_size]; │ │124 - }; │ │125 - const login_failure: ?ui.Home.Auth.Login.Failure = │ │126 - if (getCookieValue(request, login_failure_cookie)) |raw| │ │127 - if (std.mem.eql(u8, raw, "unknown_user")) │ │128 - .unknown_user │ │129 - else if (std.mem.eql(u8, raw, "wrong_password")) │ │130 - .wrong_password │ │139 + var user_id: ?[]const u8 = null; │ │140 + var login_failure: ?ui.Home.Auth.Login.Failure = null; │ │141 + switch (host) { │ │142 + .remote => |remote| { │ │143 + user_id = blk: { │ │144 + const token = getCookieValue(request, cookie_name) orelse break :blk null; │ │145 + if (!remote.session_store.lookup(token, &user_id_buf)) break :blk null; │ │146 + break :blk user_id_buf[0..evt.event_id_size]; │ │147 + }; │ │148 + login_failure = if (getCookieValue(request, login_failure_cookie)) |raw| │ │149 + if (std.mem.eql(u8, raw, "unknown_user")) │ │150 + .unknown_user │ │151 + else if (std.mem.eql(u8, raw, "wrong_password")) │ │152 + .wrong_password │ │153 + else │ │154 + null │ │155 else │ │132 - null │ │133 - else │ │134 - null; │ │156 + null; │ │157 + }, │ │158 + .local => {}, │ │159 + } │ │160 │ │136 - const html = renderIndexHtml(io, allocator, admin_repo_path, .{ │ │161 + const html = renderIndexHtml(io, allocator, host, .{ │ │162 .user_id = user_id, │ │163 .login_failure = login_failure, │ │164 .current_page = current_page, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │165 + .is_local = host == .local, │ │166 }) catch |err| switch (err) { │ │167 error.NotFound => { │ │168 try request.respond("not found", .{│ ╰────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │326 fn renderIndexHtml( │ │327 io: std.Io, │ │328 allocator: std.mem.Allocator, │ │303 - admin_repo_path: []const u8, │ │329 + host: Host, │ │330 session_data: ui.Session.Data, │ │331 ) ![]const u8 { │ │332 const template = (findEmbed("/index.html") orelse return error.MissingIndexAsset).body;│ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │333 │ │308 - // open the admin repo to read live user/repo data. │ │309 - const Repo = rp.Repo(.xit, evt.admin_repo_opts); │ │310 - var repo = try Repo.open(io, allocator, .{ .path = admin_repo_path }); │ │311 - defer repo.deinit(io, allocator); │ │334 var page_arena = std.heap.ArenaAllocator.init(allocator); │ │335 defer page_arena.deinit(); │ │336 + │ │337 + // the remote admin repo must outlive the page build, since the session's │ │338 + // moment reads from it. │ │339 + var repo_maybe: ?rp.Repo(.xit, evt.admin_repo_opts) = null; │ │340 + defer if (repo_maybe) |*repo| repo.deinit(io, allocator); │ │341 │ │315 - var session = try ui.Session.init(&page_arena, &repo, session_data); │ │316 - // give the page builders filesystem access to the on-disk repos (a sibling │ │317 - // "repos" dir next to the admin repo) so the Repo page can read its files. │ │342 + var session = switch (host) { │ │343 + .remote => |remote| blk: { │ │344 + // open the admin repo to read live user/repo data. │ │345 + repo_maybe = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │346 + var session = try ui.Session.init(&page_arena, &repo_maybe.?, session_data); │ │347 + // give the page builders filesystem access to the on-disk repos (a │ │348 + // sibling "repos" dir next to the admin repo) so the Repo page can │ │349 + // read its files. │ │350 + session.repos_dir = try std.fs.path.join(page_arena.allocator(), &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" });│ │351 + break :blk session; │ │352 + }, │ │353 + .local => |local| ui.Session{ │ │354 + .data = session_data, │ │355 + .arena = &page_arena, │ │356 + .page_arena = &page_arena, │ │357 + .local = local, │ │358 + }, │ │359 + }; │ │360 session.io = io; │ │319 - session.repos_dir = try std.fs.path.join(page_arena.allocator(), &.{ std.fs.path.dirname(admin_repo_path) orelse ".", "repos" }); │ │361 │ │362 const snapshot: ui.Snapshot = .{ │ │363 .page = try ui.Page.init(session.page_arena, &session, session.data.current_page), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯