mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Fix redirect URL on devtool indicator
This commit is contained in:
parent
912eea4f7f
commit
73b8a0c27f
@ -2,7 +2,7 @@
|
||||
|
||||
import type { RequestLogEntry } from "@hexclave/shared/dist/interface/client-interface";
|
||||
import { DEV_TOOL_ROOT_ID } from "@hexclave/shared/dist/utils/dev-tool";
|
||||
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
||||
import { runAsynchronously, runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
|
||||
import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
|
||||
import type { StackClientApp } from "../lib/hexclave-app";
|
||||
import { envVars } from "../generated/env";
|
||||
@ -1933,11 +1933,6 @@ function createComponentsTab(app: StackClientApp<true>): HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
function getCompactUrl(url: string): string {
|
||||
const resolved = new URL(url, window.location.origin);
|
||||
return `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
||||
}
|
||||
|
||||
const sidebar = h('div', { className: 'sdt-pg-sidebar' });
|
||||
const mainArea = h('div', { className: 'sdt-pg-main' });
|
||||
|
||||
@ -1988,7 +1983,6 @@ function createComponentsTab(app: StackClientApp<true>): HTMLElement {
|
||||
const header = h('div', { className: 'sdt-pg-header' });
|
||||
const headerTop = h('div', { className: 'sdt-pg-header-top' });
|
||||
headerTop.appendChild(h('h3', { className: 'sdt-pg-title' }, `${page.label} Page`));
|
||||
headerTop.appendChild(h('a', { href: page.url, target: '_blank', rel: 'noopener noreferrer', className: 'sdt-pg-title-url' }, getCompactUrl(page.url)));
|
||||
if (page.versionStatus === 'outdated') {
|
||||
headerTop.appendChild(h('span', { className: 'sdt-pg-badge sdt-pg-badge-outdated' }, 'Outdated'));
|
||||
}
|
||||
@ -2000,8 +1994,19 @@ function createComponentsTab(app: StackClientApp<true>): HTMLElement {
|
||||
const openBtn = h('button', { className: 'sdt-pg-copy-btn sdt-pg-open-btn' });
|
||||
setHtml(openBtn, 'Open <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17L17 7"/><path d="M7 7h10v10"/></svg>');
|
||||
openBtn.addEventListener('click', () => {
|
||||
const resolved = new URL(page.url, window.location.origin);
|
||||
window.open(resolved.toString(), '_blank', 'noopener,noreferrer');
|
||||
const openedWindow = window.open('about:blank', '_blank');
|
||||
if (openedWindow != null) {
|
||||
openedWindow.opener = null;
|
||||
}
|
||||
runAsynchronouslyWithAlert(async () => {
|
||||
const redirectUrl = await app[hexclaveAppInternalsSymbol].getRedirectToHandlerUrl(page.key);
|
||||
const resolved = new URL(redirectUrl, window.location.origin);
|
||||
if (openedWindow != null) {
|
||||
openedWindow.location.replace(resolved.toString());
|
||||
} else {
|
||||
window.open(resolved.toString(), '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
});
|
||||
});
|
||||
codeRow.appendChild(openBtn);
|
||||
header.appendChild(codeRow);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AccessToken } from "@hexclave/shared/dist/sessions";
|
||||
import { Store } from "@hexclave/shared/dist/utils/stores";
|
||||
import { hexclaveAppInternalsSymbol } from "../../common";
|
||||
import { StackClientApp } from "../interfaces/client-app";
|
||||
|
||||
function createAccessTokenString(refreshTokenId: string): string {
|
||||
@ -49,6 +50,42 @@ function createMockDocument(): Document {
|
||||
}
|
||||
|
||||
describe("StackClientApp cross-domain auth", () => {
|
||||
it("exposes redirect-back-aware handler URLs for devtool previews", async () => {
|
||||
const previousWindow = Reflect.get(globalThis, "window");
|
||||
const hadPreviousWindow = Reflect.has(globalThis, "window");
|
||||
Reflect.set(globalThis, "window", {
|
||||
location: {
|
||||
href: "http://localhost/music?track=1#song",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const clientApp = new StackClientApp({
|
||||
baseUrl: "http://localhost:12345",
|
||||
projectId: "00000000-0000-4000-8000-000000000000",
|
||||
publishableClientKey: "stack-pk-test",
|
||||
tokenStore: "memory",
|
||||
redirectMethod: "none",
|
||||
urls: {
|
||||
signIn: "/handler/sign-in",
|
||||
},
|
||||
noAutomaticPrefetch: true,
|
||||
});
|
||||
|
||||
const redirectUrl = await clientApp[hexclaveAppInternalsSymbol].getRedirectToHandlerUrl("signIn");
|
||||
|
||||
const resolved = new URL(redirectUrl, "http://localhost");
|
||||
expect(resolved.pathname).toBe("/handler/sign-in");
|
||||
expect(resolved.searchParams.get("after_auth_return_to")).toBe("/music?track=1#song");
|
||||
} finally {
|
||||
if (hadPreviousWindow) {
|
||||
Reflect.set(globalThis, "window", previousWindow);
|
||||
} else {
|
||||
Reflect.deleteProperty(globalThis, "window");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the fresh post-auth refresh token when minting a cross-domain handoff", async () => {
|
||||
const freshAccessToken = createAccessTokenString("fresh-refresh-token-id");
|
||||
const clientApp = new StackClientApp({
|
||||
|
||||
@ -3069,6 +3069,20 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
overrideTokenStoreInit?: TokenStoreInit,
|
||||
},
|
||||
) {
|
||||
await this._redirectTo({
|
||||
url: await this._getRedirectToHandlerUrl(handlerName, options, internalOptions),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
protected async _getRedirectToHandlerUrl(
|
||||
handlerName: keyof HandlerUrls,
|
||||
options?: RedirectToOptions,
|
||||
internalOptions?: {
|
||||
awaitPendingAuthResolutions?: boolean,
|
||||
overrideTokenStoreInit?: TokenStoreInit,
|
||||
},
|
||||
): Promise<string> {
|
||||
const rawUrls = getUrls(this._urlOptions, { projectId: this.projectId });
|
||||
const rawHandlerUrl = rawUrls[handlerName];
|
||||
if (!rawHandlerUrl) {
|
||||
@ -3096,8 +3110,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
awaitPendingAuthResolutions: internalOptions?.awaitPendingAuthResolutions,
|
||||
overrideTokenStoreInit: internalOptions?.overrideTokenStoreInit,
|
||||
});
|
||||
await this._redirectTo({ url: crossDomainRedirectUrl, ...options });
|
||||
return;
|
||||
return crossDomainRedirectUrl;
|
||||
}
|
||||
|
||||
const redirectUrl = currentUrl != null && handlerName !== "signOut" && handlerName !== "afterSignOut" && handlerName !== "oauthCallback"
|
||||
@ -3108,7 +3121,10 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
overrideTokenStoreInit: internalOptions?.overrideTokenStoreInit,
|
||||
})
|
||||
: plan.url;
|
||||
await this._redirectIfTrusted(redirectUrl, options);
|
||||
if (!await this._isTrusted(redirectUrl)) {
|
||||
throw new Error(`Redirect URL ${redirectUrl} is not trusted; should be relative.`);
|
||||
}
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
protected _redirectToHandlerDuringRender(handlerName: keyof HandlerUrls, options?: RedirectToOptions): boolean {
|
||||
@ -4190,6 +4206,9 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
||||
redirectToUrl: async (url: string | URL, options?: { replace?: boolean }) => {
|
||||
await this._redirectTo({ url, ...options });
|
||||
},
|
||||
getRedirectToHandlerUrl: async (handlerName: keyof HandlerUrls, options?: RedirectToOptions) => {
|
||||
return await this._getRedirectToHandlerUrl(handlerName, options);
|
||||
},
|
||||
redirectToHandler: async (handlerName: keyof HandlerUrls, options?: RedirectToOptions) => {
|
||||
await this._redirectToHandler(handlerName, options);
|
||||
},
|
||||
|
||||
@ -134,6 +134,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
||||
getUrls(): Readonly<ResolvedHandlerUrls>,
|
||||
getRedirectMethod(): RedirectMethod,
|
||||
redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise<void>,
|
||||
getRedirectToHandlerUrl(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<string>,
|
||||
redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,
|
||||
signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise<void>,
|
||||
awaitPendingAuthResolutions(): Promise<void>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user