╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │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. │ │20 const login_failure_cookie = "haxy_login_failure"; │ │21 +const sync_failure_cookie = "haxy_sync_failure"; │ │22 │ │23 const Embed = struct { │ │24 path: []const u8, │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │116 } │ │117 } │ │118 }, │ │118 - .local => { │ │119 - const PostRoute = enum { new, edit, open, close, resolve }; │ │119 + .local => |local| { │ │120 + const PostRoute = enum { new, edit, open, close, resolve, sync };│ │121 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │122 const suffix = "/" ++ field.name; │ │123 if (std.mem.endsWith(u8, path, suffix)) { │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │128 .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │129 .close => handleIssueStatus(io, request, allocator, base, host, .closed),│ │130 .resolve => handleIssueResolve(io, request, allocator, base, host), │ │131 + .sync => handleSync(io, request, allocator, base, local), │ │132 }; │ │133 } │ │134 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │165 var user_id_buf: [evt.event_id_size]u8 = undefined; │ │166 var user_id: ?[]const u8 = null; │ │167 var login_failure: ?ui.Home.Auth.Login.Failure = null; │ │168 + var sync_failure: ?[]const u8 = null; │ │169 + var sync_failure_allocated: ?[]u8 = null; │ │170 + defer if (sync_failure_allocated) |value| allocator.free(value);│ │171 // backs the cookie a claimed auto-login sets │ │172 var cookie_buf: [256]u8 = undefined; │ │173 var session_cookie: ?[]const u8 = null; │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │196 else │ │197 null; │ │198 }, │ │194 - .local => {}, │ │199 + .local => if (getCookieValue(request, sync_failure_cookie)) |raw| { │ │200 + const value = try allocator.dupe(u8, raw); │ │201 + sync_failure_allocated = value; │ │202 + sync_failure = std.Uri.percentDecodeInPlace(value); │ │203 + }, │ │204 } │ │205 │ │206 const clone_http_port: ?u16, const clone_ssh_port: ?u16 = switch (host) {│ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │210 const html = renderIndexHtml(io, allocator, host, .{│ │211 .user_id = user_id, │ │212 .login_failure = login_failure, │ │213 + .sync_failure = sync_failure, │ │214 .current_page = current_page, │ │215 .is_local = host == .local, │ │216 .clone_http_port = clone_http_port, │ ╰─────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │227 }; │ │228 defer allocator.free(html); │ │229 │ │220 - var header_buf: [3]std.http.Header = undefined; │ │230 + var header_buf: [4]std.http.Header = undefined; │ │231 var headers: std.ArrayList(std.http.Header) = .initBuffer(&header_buf); │ │232 headers.appendAssumeCapacity(.{ .name = "content-type", .value = "text/html; charset=utf-8" });│ │233 // expire the flash cookie on the way out so a refresh doesn't keep │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │234 // showing the failure label │ │235 if (login_failure != null) headers.appendAssumeCapacity(.{ .name = "set-cookie", .value = login_failure_cookie ++ "=; Path=/; Max-Age=0" });│ │236 + if (sync_failure != null) headers.appendAssumeCapacity(.{ .name = "set-cookie", .value = sync_failure_cookie ++ "=; Path=/; Max-Age=0" }); │ │237 if (session_cookie) |cookie| headers.appendAssumeCapacity(.{ .name = "set-cookie", .value = cookie }); │ │238 try request.respond(html, .{ .extra_headers = headers.items }); │ │239 return; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1035 }); │ │1036 } │ │1037 │ │1038 +fn handleSync( │ │1039 + io: std.Io, │ │1040 + request: *std.http.Server.Request, │ │1041 + allocator: std.mem.Allocator, │ │1042 + base: []const u8, │ │1043 + source: ui.RepoSource, │ │1044 +) !void { │ │1045 + if (base.len != 0) { │ │1046 + try request.respond("not found", .{ │ │1047 + .status = .not_found, │ │1048 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │1049 + }); │ │1050 + return; │ │1051 + } │ │1052 + │ │1053 + var owned_failure: ?[]u8 = null; │ │1054 + defer if (owned_failure) |message| allocator.free(message); │ │1055 + const failure: ?[]const u8 = blk: { │ │1056 + owned_failure = ui.Repo.Events.sync(io, allocator, source) catch |err| break :blk @errorName(err);│ │1057 + break :blk owned_failure; │ │1058 + }; │ │1059 + if (failure) |message| { │ │1060 + var encoded: std.Io.Writer.Allocating = .init(allocator); │ │1061 + defer encoded.deinit(); │ │1062 + try std.Uri.Component.percentEncode(&encoded.writer, message, struct { │ │1063 + fn isUnreserved(c: u8) bool { │ │1064 + return std.ascii.isAlphanumeric(c) or c == '-' or c == '.' or c == '_' or c == '~'; │ │1065 + } │ │1066 + }.isUnreserved); │ │1067 + const cookie = try std.fmt.allocPrint( │ │1068 + allocator, │ │1069 + sync_failure_cookie ++ "={s}; Path=/; HttpOnly; SameSite=Strict", │ │1070 + .{encoded.written()}, │ │1071 + ); │ │1072 + defer allocator.free(cookie); │ │1073 + try request.respond("", .{ │ │1074 + .status = .see_other, │ │1075 + .extra_headers = &.{ │ │1076 + .{ .name = "location", .value = "/events" }, │ │1077 + .{ .name = "set-cookie", .value = cookie }, │ │1078 + }, │ │1079 + }); │ │1080 + return; │ │1081 + } │ │1082 + │ │1083 + try request.respond("", .{ │ │1084 + .status = .see_other, │ │1085 + .extra_headers = &.{ │ │1086 + .{ .name = "location", .value = "/events" }, │ │1087 + .{ .name = "set-cookie", .value = sync_failure_cookie ++ "=; Path=/; Max-Age=0" }, │ │1088 + }, │ │1089 + }); │ │1090 +} │ │1091 + │ │1092 fn renderIndexHtml( │ │1093 io: std.Io, │ │1094 allocator: std.mem.Allocator, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯