A workspace conversation is never remembered, so /admin/bots always reopens the bot's own chat.
This commit is contained in:
@@ -5,14 +5,10 @@ import { setCookies } from "./cookies";
|
||||
import readLastVisitedBot from "./read-last-visited-bot";
|
||||
|
||||
const bot_cookie = AppData["LastVisitedBotCookieName"];
|
||||
const workspace_cookie = AppData["LastVisitedWorkspaceCookieName"];
|
||||
|
||||
function requestWithCookies(): Request {
|
||||
const res = new Response();
|
||||
setCookies(
|
||||
res,
|
||||
lastVisitedBotCookies({ bot_id: 4, workspace_id: 9, secure: false }),
|
||||
);
|
||||
setCookies(res, lastVisitedBotCookies({ bot_id: 4, secure: false }));
|
||||
const cookie_header = res.headers
|
||||
.getSetCookie()
|
||||
.map((header) => header.split(";")[0])
|
||||
@@ -24,39 +20,23 @@ function requestWithCookies(): Request {
|
||||
}
|
||||
|
||||
describe("last visited bot cookies", () => {
|
||||
test("round-trip a workspace conversation", () => {
|
||||
expect(readLastVisitedBot({ req: requestWithCookies() })).toEqual({
|
||||
bot_id: 4,
|
||||
workspace_id: 9,
|
||||
});
|
||||
test("round-trip a bot", () => {
|
||||
expect(readLastVisitedBot({ req: requestWithCookies() })).toEqual({ bot_id: 4 });
|
||||
});
|
||||
|
||||
test("a bot's own chat leaves the workspace cookie empty", () => {
|
||||
test("only the bot cookie is written, never a workspace one", () => {
|
||||
const res = new Response();
|
||||
setCookies(res, lastVisitedBotCookies({ bot_id: 4, secure: false }));
|
||||
const cookie_header = res.headers
|
||||
.getSetCookie()
|
||||
.map((header) => header.split(";")[0])
|
||||
.join("; ");
|
||||
const [header, ...others] = res.headers.getSetCookie();
|
||||
|
||||
const req = new Request("http://localhost", {
|
||||
headers: { Cookie: cookie_header },
|
||||
});
|
||||
|
||||
expect(readLastVisitedBot({ req })).toEqual({ bot_id: 4, workspace_id: null });
|
||||
expect(others).toHaveLength(0);
|
||||
expect(header?.startsWith(`${bot_cookie}=4`)).toBe(true);
|
||||
});
|
||||
|
||||
test("no cookies at all", () => {
|
||||
expect(readLastVisitedBot({ req: new Request("http://localhost") })).toBeNull();
|
||||
});
|
||||
|
||||
test("only the workspace cookie names no conversation", () => {
|
||||
const req = new Request("http://localhost", {
|
||||
headers: { Cookie: `${workspace_cookie}=9` },
|
||||
});
|
||||
expect(readLastVisitedBot({ req })).toBeNull();
|
||||
});
|
||||
|
||||
test("a tampered bot id is ignored", () => {
|
||||
const req = new Request("http://localhost", {
|
||||
headers: { Cookie: `${bot_cookie}=1%20OR%201=1` },
|
||||
|
||||
@@ -4,26 +4,19 @@ import type { CookieOptions } from "./cookies";
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* Null or undefined: the bot's own chat.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* From `isSecureRequest`, so plain HTTP over a VPN still keeps them.
|
||||
* From `isSecureRequest`, so plain HTTP over a VPN still keeps it.
|
||||
*/
|
||||
secure: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The cookies that remember the conversation this browser is in, for
|
||||
* `setCookies`: read back by `readLastVisitedBot` on the bots' landing page.
|
||||
* `HttpOnly`, since only the server reads them, and they hold nothing but
|
||||
* two ids.
|
||||
* The cookie that remembers the bot this browser is in, for `setCookies`:
|
||||
* read back by `readLastVisitedBot` on the bots' landing page. `HttpOnly`,
|
||||
* since only the server reads it, and it holds nothing but an id. A
|
||||
* conversation inside a workspace is never remembered, so the landing page
|
||||
* always reopens the bot's own chat.
|
||||
*/
|
||||
export default function lastVisitedBotCookies({
|
||||
bot_id,
|
||||
workspace_id,
|
||||
secure,
|
||||
}: Params): CookieOptions[] {
|
||||
export default function lastVisitedBotCookies({ bot_id, secure }: Params): CookieOptions[] {
|
||||
return [
|
||||
{
|
||||
name: AppData["LastVisitedBotCookieName"],
|
||||
@@ -31,11 +24,5 @@ export default function lastVisitedBotCookies({
|
||||
secure,
|
||||
maxAge: AppData["LastVisitedCookieMaxAgeSeconds"],
|
||||
},
|
||||
// {
|
||||
// name: AppData["LastVisitedWorkspaceCookieName"],
|
||||
// value: workspace_id ? String(workspace_id) : "",
|
||||
// secure,
|
||||
// maxAge: AppData["LastVisitedCookieMaxAgeSeconds"],
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,27 +2,14 @@ import { describe, expect, test } from "bun:test";
|
||||
import parseLastVisitedBot from "./parse-last-visited-bot";
|
||||
|
||||
describe("parseLastVisitedBot", () => {
|
||||
test("a bot's own chat", () => {
|
||||
expect(parseLastVisitedBot({ bot_id: "3" })).toEqual({ bot_id: 3, workspace_id: null });
|
||||
expect(parseLastVisitedBot({ bot_id: 3, workspace_id: "" })).toEqual({
|
||||
bot_id: 3,
|
||||
workspace_id: null,
|
||||
});
|
||||
expect(parseLastVisitedBot({ bot_id: 3, workspace_id: null })).toEqual({
|
||||
bot_id: 3,
|
||||
workspace_id: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("a workspace conversation", () => {
|
||||
expect(parseLastVisitedBot({ bot_id: "3", workspace_id: "7" })).toEqual({
|
||||
bot_id: 3,
|
||||
workspace_id: 7,
|
||||
});
|
||||
test("a bot id becomes the bot", () => {
|
||||
expect(parseLastVisitedBot({ bot_id: "3" })).toEqual({ bot_id: 3 });
|
||||
expect(parseLastVisitedBot({ bot_id: 3 })).toEqual({ bot_id: 3 });
|
||||
});
|
||||
|
||||
test("rejects ids that aren't positive whole numbers", () => {
|
||||
expect(parseLastVisitedBot({})).toBeNull();
|
||||
expect(parseLastVisitedBot({ bot_id: null })).toBeNull();
|
||||
expect(parseLastVisitedBot({ bot_id: "" })).toBeNull();
|
||||
expect(parseLastVisitedBot({ bot_id: "0" })).toBeNull();
|
||||
expect(parseLastVisitedBot({ bot_id: "-1" })).toBeNull();
|
||||
@@ -32,15 +19,7 @@ describe("parseLastVisitedBot", () => {
|
||||
expect(parseLastVisitedBot({ bot_id: "99999999999999999999" })).toBeNull();
|
||||
});
|
||||
|
||||
test("a bad workspace falls back to nothing, never to its own chat", () => {
|
||||
expect(parseLastVisitedBot({ bot_id: "3", workspace_id: "abc" })).toBeNull();
|
||||
expect(parseLastVisitedBot({ bot_id: "3", workspace_id: "0" })).toBeNull();
|
||||
});
|
||||
|
||||
test("trims what a cookie header may carry", () => {
|
||||
expect(parseLastVisitedBot({ bot_id: " 3 ", workspace_id: " 7 " })).toEqual({
|
||||
bot_id: 3,
|
||||
workspace_id: 7,
|
||||
});
|
||||
expect(parseLastVisitedBot({ bot_id: " 3 " })).toEqual({ bot_id: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
export type LastVisitedBot = {
|
||||
bot_id: number;
|
||||
/**
|
||||
* Null: the bot's own chat.
|
||||
*/
|
||||
workspace_id: number | null;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
bot_id?: string | number | null;
|
||||
workspace_id?: string | number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The conversation the last-visited cookies name, or null when they don't
|
||||
* name one. Ids are checked to be positive whole numbers, so a hand-edited
|
||||
* cookie never reaches a query; whether the user still reaches that
|
||||
* conversation is the caller's check.
|
||||
* The bot the last-visited cookie names, or null when it doesn't name one.
|
||||
* The id is checked to be a positive whole number, so a hand-edited cookie
|
||||
* never reaches a query; whether the user still reaches that bot's own chat
|
||||
* is the caller's check.
|
||||
*/
|
||||
export default function parseLastVisitedBot({ bot_id, workspace_id }: Params): LastVisitedBot | null {
|
||||
export default function parseLastVisitedBot({ bot_id }: Params): LastVisitedBot | null {
|
||||
const bot = parseId(bot_id);
|
||||
|
||||
if (bot == null) return null;
|
||||
|
||||
if (workspace_id === undefined || workspace_id === null || workspace_id === "") {
|
||||
return { bot_id: bot, workspace_id: null };
|
||||
}
|
||||
|
||||
const workspace = parseId(workspace_id);
|
||||
|
||||
if (workspace == null) return null;
|
||||
|
||||
return { bot_id: bot, workspace_id: workspace };
|
||||
return { bot_id: bot };
|
||||
}
|
||||
|
||||
function parseId(value?: string | number | null): number | null {
|
||||
|
||||
@@ -7,14 +7,13 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The conversation this browser was last in, from the last-visited cookies
|
||||
* (see `lastVisitedBotCookies`). Null when it has none, or when they don't
|
||||
* name a conversation. Whether the user still reaches it is the caller's
|
||||
* The bot this browser was last in, from the last-visited cookie (see
|
||||
* `lastVisitedBotCookies`). Null when it has none, or when it doesn't name
|
||||
* a bot. Whether the user still reaches that bot's own chat is the caller's
|
||||
* check: a stale grant must fall back, never redirect.
|
||||
*/
|
||||
export default function readLastVisitedBot({ req }: Params): LastVisitedBot | null {
|
||||
return parseLastVisitedBot({
|
||||
bot_id: getCookie(req, AppData["LastVisitedBotCookieName"]),
|
||||
workspace_id: getCookie(req, AppData["LastVisitedWorkspaceCookieName"]),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user