╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────╮ │1 const std = @import("std"); │ │2 +const builtin = @import("builtin");│ │3 const xit = @import("xit"); │ │4 const rp = xit.repo; │ │5 const evt = @import("./event.zig");│ ╰──────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │11 const Grid = xitui.grid.Grid; │ │12 │ │13 const cookie_name = "haxy_session"; │ │14 +// the session cookie a login or a claimed auto-login sets; both must scope │ │15 +// it the same way. │ │16 +const session_cookie_fmt = cookie_name ++ "={s}; Path=/; HttpOnly; SameSite=Strict";│ │17 // flash cookie for surfacing the outcome of the most recent /login POST. │ │18 // set on the failure redirect, read and immediately expired on the next │ │19 // GET / so refreshing the page doesn't keep showing the error. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │160 var user_id_buf: [evt.event_id_size]u8 = undefined; │ │161 var user_id: ?[]const u8 = null; │ │162 var login_failure: ?ui.Home.Auth.Login.Failure = null;│ │163 + // backs the cookie a claimed auto-login sets │ │164 + var cookie_buf: [256]u8 = undefined; │ │165 + var session_cookie: ?[]const u8 = null; │ │166 switch (host) { │ │167 .remote => |remote| { │ │168 user_id = blk: { │ ╰───────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 if (!remote.session_store.lookup(token, &user_id_buf)) break :blk null; │ │171 break :blk user_id_buf[0..evt.event_id_size]; │ │172 }; │ │173 + // whoever seeded the store may have left a session to claim │ │174 + if (user_id == null) { │ │175 + var token: [SessionStore.token_hex_len]u8 = undefined; │ │176 + if (remote.session_store.claimAutoLogin(&token) and remote.session_store.lookup(&token, &user_id_buf)) {│ │177 + session_cookie = try std.fmt.bufPrint(&cookie_buf, session_cookie_fmt, .{token}); │ │178 + user_id = user_id_buf[0..evt.event_id_size]; │ │179 + } │ │180 + } │ │181 login_failure = if (getCookieValue(request, login_failure_cookie)) |raw| │ │182 if (std.mem.eql(u8, raw, "unknown_user")) │ │183 .unknown_user │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 }; │ │209 defer allocator.free(html); │ │210 │ │211 + var header_buf: [3]std.http.Header = undefined; │ │212 + var headers: std.ArrayList(std.http.Header) = .initBuffer(&header_buf); │ │213 + headers.appendAssumeCapacity(.{ .name = "content-type", .value = "text/html; charset=utf-8" }); │ │214 // expire the flash cookie on the way out so a refresh doesn't keep │ │197 - // showing the failure label. │ │198 - var headers: [2]std.http.Header = undefined; │ │199 - headers[0] = .{ .name = "content-type", .value = "text/html; charset=utf-8" }; │ │200 - var headers_slice: []const std.http.Header = headers[0..1]; │ │201 - if (login_failure != null) { │ │202 - headers[1] = .{ .name = "set-cookie", .value = login_failure_cookie ++ "=; Path=/; Max-Age=0" }; │ │203 - headers_slice = headers[0..2]; │ │204 - } │ │205 - try request.respond(html, .{ .extra_headers = headers_slice }); │ │215 + // showing the failure label │ │216 + if (login_failure != null) headers.appendAssumeCapacity(.{ .name = "set-cookie", .value = login_failure_cookie ++ "=; Path=/; Max-Age=0" });│ │217 + if (session_cookie) |cookie| headers.appendAssumeCapacity(.{ .name = "set-cookie", .value = cookie }); │ │218 + try request.respond(html, .{ .extra_headers = headers.items }); │ │219 return; │ │220 } │ │221 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │274 .success => |user_id| { │ │275 const token = try session_store.create(&user_id); │ │276 var cookie_buf: [256]u8 = undefined; │ │264 - const cookie = try std.fmt.bufPrint(&cookie_buf, cookie_name ++ "={s}; Path=/; HttpOnly; SameSite=Strict", .{token});│ │277 + const cookie = try std.fmt.bufPrint(&cookie_buf, session_cookie_fmt, .{token}); │ │278 try request.respond("", .{ │ │279 .status = .see_other, │ │280 .extra_headers = &.{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │333 admin_repo_path: []const u8, │ │334 session_store: SessionStore, │ │335 ) !void { │ │336 + // the toggle re-emits the user's own event, so it takes a logged-in user │ │337 var user_id: [evt.event_id_size]u8 = undefined; │ │324 - if (getCookieValue(request, cookie_name)) |token| { │ │325 - if (session_store.lookup(token, &user_id)) { │ │326 - const Repo = rp.Repo(.xit, evt.admin_repo_opts); │ │327 - var repo = try Repo.open(io, allocator, .{ .path = admin_repo_path }); │ │328 - defer repo.deinit(io, allocator); │ │338 + const token = getCookieValue(request, cookie_name) orelse return respondLoginRequired(request);│ │339 + if (!session_store.lookup(token, &user_id)) return respondLoginRequired(request); │ │340 + │ │341 + { │ │342 + const Repo = rp.Repo(.xit, evt.admin_repo_opts); │ │343 + var repo = try Repo.open(io, allocator, .{ .path = admin_repo_path }); │ │344 + defer repo.deinit(io, allocator); │ │345 │ │330 - try evt.User.toggleAnsi(evt.admin_repo_opts, io, allocator, &repo, &user_id); │ │331 - } │ │346 + try evt.User.toggleAnsi(evt.admin_repo_opts, io, allocator, &repo, &user_id); │ │347 } │ │348 │ │349 // return to the settings tab the toggle came from so the change is visible. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │361 }); │ │362 } │ │363 │ │349 -// the logged-in user the request's session cookie names, or null for local │ │350 -// mode / no session / an unknown token │ │351 -fn sessionUser( │ │364 +// the author for an event created by this request, or null when the request │ │365 +// may not create one. local mode has no accounts, so it authors anonymously.│ │366 +fn eventAuthorEmail( │ │367 io: std.Io, │ │368 allocator: std.mem.Allocator, │ │369 arena: *std.heap.ArenaAllocator, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │370 request: *std.http.Server.Request, │ │371 host: Host, │ │357 -) !?evt.User.Record { │ │372 +) !?[]const u8 { │ │373 const remote = switch (host) { │ │374 .remote => |remote| remote, │ │360 - .local => return null, │ │375 + .local => return "user@haxy", // TODO: try the repo's git config user.email first│ │376 }; │ │377 const token = getCookieValue(request, cookie_name) orelse return null; │ │378 var user_id: [evt.event_id_size]u8 = undefined; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │381 var repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │382 defer repo.deinit(io, allocator); │ │383 const moment = try evt.currentMoment(evt.admin_repo_opts, &repo); │ │369 - return (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse unreachable; │ │384 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse return null;│ │385 + return user.event.email; │ │386 +} │ │387 + │ │388 +// refuse a write from a request with no logged-in user. the body goes unread, │ │389 +// so the connection closes. │ │390 +fn respondLoginRequired(request: *std.http.Server.Request) !void { │ │391 + try request.respond("log in to make changes", .{ │ │392 + .status = .forbidden, │ │393 + .keep_alive = false, │ │394 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │395 + }); │ │396 } │ │397 │ │398 // create an issue in the repo the form's page names (base is │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯