╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │632 if (!try request_repo.source.hasBranch(io, allocator, target_branch)) { │ │633 const form_location = try std.fmt.allocPrint(allocator, "{s}/patches/new", .{base}); │ │634 defer allocator.free(form_location); │ │565 - try request.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "location", .value = form_location }} }); │ │566 - return; │ │635 + return respondThreadFormFailure(request, allocator, host, form_location, invalidTargetBranchFeedback(title, tags, description, target_branch));│ │636 } │ │637 const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request); │ │638 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1413 if (!valid) { │ │1414 const form_location = try std.fmt.allocPrint(allocator, "{s}/edit", .{base}); │ │1415 defer allocator.free(form_location); │ │1416 + if (!evt.titleValid(title)) { │ │1417 + return respondThreadFormFailure(request, allocator, host, form_location, requiredTitleFeedback(parts.thread_kind, title, tags, description, target_branch));│ │1418 + } │ │1419 try request.respond("", .{ │ │1420 .status = .see_other, │ │1421 .extra_headers = &.{.{ .name = "location", .value = form_location }}, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1434 if (!try request_repo.source.hasBranch(io, allocator, target_branch)) { │ │1435 const form_location = try std.fmt.allocPrint(allocator, "{s}/edit", .{base}); │ │1436 defer allocator.free(form_location); │ │1365 - try request.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "location", .value = form_location }} }); │ │1366 - return; │ │1437 + return respondThreadFormFailure(request, allocator, host, form_location, invalidTargetBranchFeedback(title, tags, description, target_branch));│ │1438 } │ │1439 const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request); │ │1440 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1630 }.isUnreserved); │ │1631 const cookie = try std.fmt.allocPrint( │ │1632 allocator, │ │1562 - sync_failure_cookie ++ "={s}; Path=/; HttpOnly; SameSite=Strict", │ │1633 + local_flash_cookie ++ "=" ++ local_sync_failure_prefix ++ "{s}; Path=/; HttpOnly; SameSite=Strict",│ │1634 .{encoded.written()}, │ │1635 ); │ │1636 defer allocator.free(cookie); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1648 .status = .see_other, │ │1649 .extra_headers = &.{ │ │1650 .{ .name = "location", .value = "/events" }, │ │1580 - .{ .name = "set-cookie", .value = sync_failure_cookie ++ "=; Path=/; Max-Age=0" },│ │1651 + .{ .name = "set-cookie", .value = local_flash_cookie ++ "=; Path=/; Max-Age=0" }, │ │1652 }, │ │1653 }); │ │1654 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │2227 // (not the user's id) that is stored as a file named by the token hex, whose │ │2228 // contents are the raw user_id bytes. authenticating a request is a lookup of│ │2229 // the cookie's token; logout deletes the file, revoking the session. there is│ │2159 -// deliberately no expiry — a session lives until logout. │ │2230 +// deliberately no expiry — a session lives until logout. form failures use │ │2231 +// files in the flash subdirectory that the redirected GET consumes. the │ │2232 +// directory is cleared whenever the server starts. │ │2233 pub const SessionStore = struct { │ │2234 io: std.Io, │ │2235 dir: std.Io.Dir, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │2236 + flash_dir: std.Io.Dir, │ │2237 auto_login: ?[token_hex_len]u8, │ │2238 │ │2239 pub const token_hex_len = evt.event_id_size * 2;│ ╰──────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2241 pub fn init(io: std.Io, data_dir: std.Io.Dir) !SessionStore { │ │2242 try data_dir.createDirPath(io, "sessions"); │ │2243 const dir = try data_dir.openDir(io, "sessions", .{}); │ │2244 + errdefer dir.close(io); │ │2245 + try dir.deleteTree(io, "flash"); │ │2246 + try dir.createDirPath(io, "flash"); │ │2247 + const flash_dir = try dir.openDir(io, "flash", .{}); │ │2248 + errdefer flash_dir.close(io); │ │2249 const auto_login = blk: { │ │2250 const file = dir.openFile(io, auto_login_name, .{ .mode = .read_only }) catch break :blk null;│ │2251 defer file.close(io); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │2255 reader.interface.readSliceAll(&token) catch break :blk null; │ │2256 break :blk token; │ │2257 }; │ │2179 - return .{ .io = io, .dir = dir, .auto_login = auto_login }; │ │2258 + return .{ .io = io, .dir = dir, .flash_dir = flash_dir, .auto_login = auto_login };│ │2259 } │ │2260 │ │2261 pub fn deinit(self: SessionStore) void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────╮ │2262 + self.flash_dir.close(self.io);│ │2263 self.dir.close(self.io); │ │2264 } │ │2265 │ ╰────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯