mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
fix(dashboard):use session aware loading
This commit is contained in:
parent
6e4d2eb076
commit
f98d874185
5 changed files with 64 additions and 74 deletions
|
|
@ -69,7 +69,7 @@ import { useMessagesSync } from "@/hooks/use-messages-sync";
|
|||
import { useThreadDetail, useThreadMessages } from "@/hooks/use-thread-queries";
|
||||
import { getAgentFilesystemSelection } from "@/lib/agent-filesystem";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { getDesktopAccessToken } from "@/lib/auth-fetch";
|
||||
import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier";
|
||||
import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors";
|
||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||
|
|
@ -917,29 +917,26 @@ export default function NewChatPage() {
|
|||
// Cancel ongoing request
|
||||
const cancelRun = useCallback(async () => {
|
||||
if (threadId) {
|
||||
const token = getBearerToken();
|
||||
if (token) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
buildBackendUrl(`/api/v1/threads/${threadId}/cancel-active-turn`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
const payload = (await response.json()) as {
|
||||
error_code?: string;
|
||||
};
|
||||
if (payload.error_code === "TURN_CANCELLING") {
|
||||
recentCancelRequestedAtRef.current = Date.now();
|
||||
}
|
||||
const token = await getDesktopAccessToken();
|
||||
try {
|
||||
const response = await fetch(
|
||||
buildBackendUrl(`/api/v1/threads/${threadId}/cancel-active-turn`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
const payload = (await response.json()) as {
|
||||
error_code?: string;
|
||||
};
|
||||
if (payload.error_code === "TURN_CANCELLING") {
|
||||
recentCancelRequestedAtRef.current = Date.now();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[NewChatPage] Failed to signal cancel-active-turn:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[NewChatPage] Failed to signal cancel-active-turn:", error);
|
||||
}
|
||||
}
|
||||
if (abortControllerRef.current) {
|
||||
|
|
@ -964,11 +961,7 @@ export default function NewChatPage() {
|
|||
|
||||
if (!userQuery.trim() && userImages.length === 0) return;
|
||||
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
toast.error("Not authenticated. Please log in again.");
|
||||
return;
|
||||
}
|
||||
const token = await getDesktopAccessToken();
|
||||
|
||||
// Lazy thread creation: create thread on first message if it doesn't exist
|
||||
let currentThreadId = threadId;
|
||||
|
|
@ -1149,8 +1142,9 @@ export default function NewChatPage() {
|
|||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
chat_id: currentThreadId,
|
||||
user_query: userQuery.trim(),
|
||||
|
|
@ -1537,12 +1531,7 @@ export default function NewChatPage() {
|
|||
stagedDecisionsByInterruptIdRef.current.clear();
|
||||
setIsRunning(true);
|
||||
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
toast.error("Not authenticated. Please log in again.");
|
||||
setIsRunning(false);
|
||||
return;
|
||||
}
|
||||
const token = await getDesktopAccessToken();
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
|
@ -1648,8 +1637,9 @@ export default function NewChatPage() {
|
|||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
search_space_id: searchSpaceId,
|
||||
decisions,
|
||||
|
|
@ -1981,11 +1971,7 @@ export default function NewChatPage() {
|
|||
abortControllerRef.current = null;
|
||||
}
|
||||
|
||||
const token = getBearerToken();
|
||||
if (!token) {
|
||||
toast.error("Not authenticated. Please log in again.");
|
||||
return;
|
||||
}
|
||||
const token = await getDesktopAccessToken();
|
||||
|
||||
// Extract the original user query BEFORE removing messages (for reload mode)
|
||||
let userQueryToDisplay: string | undefined;
|
||||
|
|
@ -2104,8 +2090,9 @@ export default function NewChatPage() {
|
|||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { SearchSpace } from "@/contracts/types/search-space.types";
|
||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { authenticatedFetch } from "@/lib/auth-fetch";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { announcements } from "../lib/announcements/announcements-data";
|
|||
import { acquireTestToken } from "./helpers/api/auth";
|
||||
|
||||
/**
|
||||
* One-time authentication setup. Acquires a bearer token for the seeded
|
||||
* e2e user (rate-limit-free /__e2e__/auth/token first, /auth/jwt/login
|
||||
* fallback) and persists it via localStorage so every test in the
|
||||
* One-time authentication setup. Acquires an access token for the seeded
|
||||
* e2e user (rate-limit-free /__e2e__/auth/token first, desktop login
|
||||
* fallback) and persists it as the session cookie so every test in the
|
||||
* chromium project starts already authenticated.
|
||||
*
|
||||
* Also pre-seeds the localStorage flags that gate the two new-user UI
|
||||
|
|
@ -18,7 +18,9 @@ import { acquireTestToken } from "./helpers/api/auth";
|
|||
|
||||
const authFile = path.join(__dirname, "..", "playwright", ".auth", "user.json");
|
||||
|
||||
const STORAGE_KEY = "surfsense_bearer_token";
|
||||
const PORT = process.env.PORT || "3000";
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${PORT}`;
|
||||
const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || "surfsense_session";
|
||||
const ANNOUNCEMENTS_KEY = "surfsense_announcements_state";
|
||||
|
||||
/** Decode the user id (`sub`) from a JWT without verifying the signature. */
|
||||
|
|
@ -45,17 +47,24 @@ setup("authenticate", async ({ page, request }) => {
|
|||
const announcementIds = announcements.map((a) => a.id);
|
||||
const announcementState = { readIds: announcementIds, toastedIds: announcementIds };
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: SESSION_COOKIE_NAME,
|
||||
value: access_token,
|
||||
url: BASE_URL,
|
||||
httpOnly: true,
|
||||
sameSite: "Lax",
|
||||
},
|
||||
]);
|
||||
|
||||
await page.addInitScript(
|
||||
({ key, token, announcementsKey, state, uid }) => {
|
||||
localStorage.setItem(key, token);
|
||||
({ announcementsKey, state, uid }) => {
|
||||
localStorage.setItem(announcementsKey, JSON.stringify(state));
|
||||
if (uid) {
|
||||
localStorage.setItem(`surfsense-tour-${uid}`, "true");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: STORAGE_KEY,
|
||||
token: access_token,
|
||||
announcementsKey: ANNOUNCEMENTS_KEY,
|
||||
state: announcementState,
|
||||
uid: userId,
|
||||
|
|
|
|||
|
|
@ -22,26 +22,21 @@ export type SearchSpaceFixtures = {
|
|||
searchSpace: SearchSpaceRow;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "surfsense_bearer_token";
|
||||
const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || "surfsense_session";
|
||||
|
||||
// Reuse the token written by tests/auth.setup.ts; on cache miss we
|
||||
// Reuse the session cookie written by tests/auth.setup.ts; on cache miss we
|
||||
// mint a fresh one via /__e2e__/auth/token (rate-limit-free).
|
||||
const AUTH_STATE_PATH = path.join(__dirname, "..", "..", "playwright", ".auth", "user.json");
|
||||
|
||||
function loadCachedBearerToken(): string | null {
|
||||
function loadCachedSessionToken(): string | null {
|
||||
try {
|
||||
const raw = fs.readFileSync(AUTH_STATE_PATH, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
origins?: Array<{
|
||||
origin?: string;
|
||||
localStorage?: Array<{ name?: string; value?: string }>;
|
||||
}>;
|
||||
cookies?: Array<{ name?: string; value?: string }>;
|
||||
};
|
||||
for (const origin of parsed.origins ?? []) {
|
||||
for (const entry of origin.localStorage ?? []) {
|
||||
if (entry.name === STORAGE_KEY && entry.value) {
|
||||
return entry.value;
|
||||
}
|
||||
for (const cookie of parsed.cookies ?? []) {
|
||||
if (cookie.name === SESSION_COOKIE_NAME && cookie.value) {
|
||||
return cookie.value;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -53,7 +48,7 @@ function loadCachedBearerToken(): string | null {
|
|||
export const searchSpaceFixtures = base.extend<SearchSpaceFixtures, { apiTokenWorker: string }>({
|
||||
apiTokenWorker: [
|
||||
async ({ playwright }, use) => {
|
||||
const cached = loadCachedBearerToken();
|
||||
const cached = loadCachedSessionToken();
|
||||
if (cached) {
|
||||
await use(cached);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { APIRequestContext } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Direct backend auth helper. Uses the same /auth/jwt/login endpoint the
|
||||
* UI uses; mirrors lib/apis/auth-api.service.ts.
|
||||
* Direct backend auth helper. Uses the desktop login endpoint when the
|
||||
* rate-limit-free e2e mint endpoint is unavailable.
|
||||
*
|
||||
* Returns a bearer token specs can attach to API calls when they don't
|
||||
* want to go through the browser. The browser-side auth (localStorage)
|
||||
* want to go through the browser. The browser-side auth (cookie storage)
|
||||
* is set up separately by tests/auth.setup.ts.
|
||||
*/
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ const E2E_MINT_SECRET = process.env.E2E_MINT_SECRET || "local-e2e-mint-secret-no
|
|||
/**
|
||||
* Mints a JWT for the seeded e2e user via the test-only endpoint mounted
|
||||
* by surfsense_backend/tests/e2e/run_backend.py. Bypasses the production
|
||||
* /auth/jwt/login rate limit (5/min/IP), so it's safe to call from any
|
||||
* desktop login rate limit, so it's safe to call from any
|
||||
* worker / retry. Returns 404 from the backend when the endpoint isn't
|
||||
* mounted (i.e. someone is pointing the suite at a non-e2e backend).
|
||||
*/
|
||||
|
|
@ -46,18 +46,17 @@ export async function mintTestToken(
|
|||
}
|
||||
|
||||
export async function loginAsTestUser(request: APIRequestContext): Promise<string> {
|
||||
const response = await request.post(`${BACKEND_URL}/auth/jwt/login`, {
|
||||
form: {
|
||||
username: TEST_USER_EMAIL,
|
||||
const response = await request.post(`${BACKEND_URL}/auth/desktop/login`, {
|
||||
data: {
|
||||
email: TEST_USER_EMAIL,
|
||||
password: TEST_USER_PASSWORD,
|
||||
grant_type: "password",
|
||||
},
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`Login to ${BACKEND_URL}/auth/jwt/login failed (${response.status()}): ${await response.text()}`
|
||||
`Login to ${BACKEND_URL}/auth/desktop/login failed (${response.status()}): ${await response.text()}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +69,7 @@ export async function loginAsTestUser(request: APIRequestContext): Promise<strin
|
|||
|
||||
/**
|
||||
* Get a bearer token by trying the rate-limit-free mint endpoint first
|
||||
* and falling back to /auth/jwt/login if the e2e endpoint isn't mounted
|
||||
* and falling back to /auth/desktop/login if the e2e endpoint isn't mounted
|
||||
* (e.g. running against a non-e2e backend in local dev).
|
||||
*/
|
||||
export async function acquireTestToken(request: APIRequestContext): Promise<string> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue