mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-27 21:01:03 +08:00
fix(dashboard,backend): impersonation without logout + fix OAuth routing (#1617)
## Summary
Two fixes:
**1. Impersonation no longer requires logout** — The generated JS
snippet now clears all auth cookie variants (`hexclave-refresh-{pid}*`,
`stack-refresh-{pid}*`, access tokens) and sets the token in the
structured `hexclave-refresh-{pid}--default` format the SDK reads first.
Previously the snippet only set the legacy cookie, which was ignored
when a structured cookie already existed.
**2. Fix OAuth + other deeply nested API routes returning 404 in dev** —
Moved the API 404 handler from file-based
`api/[...notFoundPath]/route.ts` into the middleware (`proxy.tsx`). The
catch-all at the `api/` level was shadowing dynamic routes 7+ segments
deep (e.g. `auth/oauth/authorize/[provider_id]`) in Turbopack dev mode
(Next.js 16.2.7). The middleware already has `routes` + `SmartRouter`,
so it checks for a match before rewriting and returns the custom 404
directly when nothing matches.
Link to Devin session:
https://app.devin.ai/sessions/d9dcb2d203aa4a6ea36c8cdd2a4a42c2
Requested by: @mantrakp04
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Updated the user impersonation dialog to explicitly state that the
pasted console snippet will replace your current session with the
impersonated user’s session.
* **Refactor**
* Standardized the impersonation console snippet generation to use a
shared token-based approach, including proper expiration handling.
* **Bug Fixes**
* Improved reliability of the impersonation flow by failing when the
required refresh token is unavailable, preventing incomplete snippet
generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mantra <mantra@stack-auth.com>
This commit is contained in:
parent
75e497f3ec
commit
63d0eeefe9
@ -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,
|
||||
));
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@ -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,
|
||||
));
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user