Merge remote-tracking branch 'origin/dev' into perf/platform-analytics-query-memory

# Conflicts:
#	apps/backend/src/app/api/latest/internal/platform-analytics/route.tsx
#	apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/platform-analytics/page-client.tsx
This commit is contained in:
Bilal Godil 2026-06-19 11:15:19 -07:00
commit 252c5651a0
3 changed files with 33 additions and 18 deletions

View File

@ -38,7 +38,7 @@ import {
Typography,
useToast
} from "@/components/ui";
import { DeleteUserDialog, ImpersonateUserDialog } from "@/components/user-dialogs";
import { DeleteUserDialog, generateImpersonateSnippet, ImpersonateUserDialog } from "@/components/user-dialogs";
import { ALL_APPS_FRONTEND } from "@/lib/apps-frontend";
import { isAppEnabled } from "@/lib/apps-utils";
import { parseRiskScore } from "@/lib/risk-score-utils";
@ -52,7 +52,7 @@ import { normalizeCountryCode } from "@hexclave/shared/dist/schema-fields";
import { fromNow } from "@hexclave/shared/dist/utils/dates";
import { captureError, HexclaveAssertionError, throwErr } from '@hexclave/shared/dist/utils/errors';
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { deindent } from "@hexclave/shared/dist/utils/strings";
import { usePathname, useSearchParams } from "next/navigation";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
import { createPortal } from "react-dom";
@ -148,12 +148,13 @@ function UserHeader({ user }: UserHeaderProps) {
runAsynchronouslyWithAlert(async () => {
const expiresInMillis = 1000 * 60 * 60 * 2;
const expiresAtDate = new Date(Date.now() + expiresInMillis);
const session = await user.createSession({ expiresInMillis });
const session = await user.createSession({ expiresInMillis, isImpersonation: true });
const tokens = await session.getTokens();
setImpersonateSnippet(deindent`
document.cookie = 'stack-refresh-${hexclaveAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/';
window.location.reload();
`);
setImpersonateSnippet(generateImpersonateSnippet(
hexclaveAdminApp.projectId,
tokens.refreshToken ?? throwErr("Expected refresh token for newly created impersonation session"),
expiresAtDate,
));
});
},
},

View File

@ -31,13 +31,14 @@ import {
type DataGridDataSource,
} from "@hexclave/dashboard-ui-components";
import { fromNow } from "@hexclave/shared/dist/utils/dates";
import { throwErr } from "@hexclave/shared/dist/utils/errors";
import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
import { deindent } from "@hexclave/shared/dist/utils/strings";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useDebounce } from "use-debounce";
import { Link } from "../link";
import { CreateCheckoutDialog } from "../payments/create-checkout-dialog";
import { DeleteUserDialog, ImpersonateUserDialog } from "../user-dialogs";
import { DeleteUserDialog, generateImpersonateSnippet, ImpersonateUserDialog } from "../user-dialogs";
// ─── Types ───────────────────────────────────────────────────────────
@ -394,12 +395,11 @@ function UserActions(props: { user: ExtendedServerUser }) {
const expiresAtDate = new Date(Date.now() + expiresInMillis);
const session = await user.createSession({ expiresInMillis, isImpersonation: true });
const tokens = await session.getTokens();
setImpersonateSnippet(
deindent`
document.cookie = 'stack-refresh-${hexclaveAdminApp.projectId}=${tokens.refreshToken}; expires=${expiresAtDate.toUTCString()}; path=/';
window.location.reload();
`,
);
setImpersonateSnippet(generateImpersonateSnippet(
hexclaveAdminApp.projectId,
tokens.refreshToken ?? throwErr("Expected refresh token for newly created impersonation session"),
expiresAtDate,
));
})
}
>

View File

@ -1,5 +1,6 @@
import { ServerUser } from '@hexclave/next';
import { ActionDialog, CopyField, Typography } from "@/components/ui";
import { deindent } from "@hexclave/shared/dist/utils/strings";
import { useRouter } from './router';
@ -30,14 +31,27 @@ export function DeleteUserDialog(props: {
</ActionDialog>;
}
export function generateImpersonateSnippet(
projectId: string,
refreshToken: string,
expiresAtDate: Date,
): string {
return deindent`
document.cookie = 'hexclave-access=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
document.cookie = 'stack-access=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
document.cookie = 'hexclave-refresh-${encodeURIComponent(projectId)}--default=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
document.cookie = '__Host-hexclave-refresh-${encodeURIComponent(projectId)}--default=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/' + (location.protocol === 'https:' ? '; secure' : '');
document.cookie = (location.protocol === 'https:' ? '__Host-' : '') + 'stack-refresh-${encodeURIComponent(projectId)}--default=' + encodeURIComponent(JSON.stringify({ refresh_token: ${JSON.stringify(refreshToken)}, updated_at_millis: Date.now() })) + '; expires=${expiresAtDate.toUTCString()}; path=/' + (location.protocol === 'https:' ? '; secure' : '');
window.location.reload();
`;
}
export function ImpersonateUserDialog(props: {
user: ServerUser,
impersonateSnippet: string | null,
onClose: () => void,
}) {
return <ActionDialog
open={props.impersonateSnippet !== null}
onOpenChange={(open) => !open && props.onClose()}
@ -45,7 +59,7 @@ export function ImpersonateUserDialog(props: {
okButton
>
<Typography>
Open your website and paste the following code into the browser console:
Open your website and paste the following code into the browser console. This will replace the current session with the impersonated user session.
</Typography>
<CopyField
type="textarea"