mirror of
https://github.com/ilyhalight/voice-over-translation.git
synced 2026-10-19 07:43:33 +00:00
fix: extra run auth callback in feeds (like tiktok)
This commit is contained in:
parent
868de1e152
commit
0fb23f98b0
2 changed files with 147 additions and 13 deletions
|
|
@ -167,19 +167,7 @@ export async function updateAccountInfo() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function updateAccountByCallbackData(
|
||||
data: AuthMessageData,
|
||||
): Promise<void> {
|
||||
const { state, code } = data;
|
||||
const storedState = sessionStorage.getItem("votYandexState");
|
||||
if (!state || state !== storedState) {
|
||||
throw new Error("[VOT] Invalid state value");
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error("[VOT] Invalid auth code response");
|
||||
}
|
||||
|
||||
async function exchangeCodeForAccount(code: string): Promise<void> {
|
||||
let token: TokenInfo;
|
||||
try {
|
||||
token = await getOAuthTokenByCode(code);
|
||||
|
|
@ -206,6 +194,41 @@ export async function updateAccountByCallbackData(
|
|||
await updateAccountInfo();
|
||||
}
|
||||
|
||||
// Every VideoHandler listens for the same callback message, and Yandex auth
|
||||
// codes are single-use, so all of them must share one exchange. The record is
|
||||
// kept after settling because a repeated code can never succeed again.
|
||||
let pendingCodeExchange: {
|
||||
code: string;
|
||||
state: string;
|
||||
promise: Promise<void>;
|
||||
} | null = null;
|
||||
|
||||
export async function updateAccountByCallbackData(
|
||||
data: AuthMessageData,
|
||||
): Promise<void> {
|
||||
const { state, code } = data;
|
||||
if (
|
||||
code &&
|
||||
pendingCodeExchange?.code === code &&
|
||||
pendingCodeExchange.state === state
|
||||
) {
|
||||
return pendingCodeExchange.promise;
|
||||
}
|
||||
|
||||
const storedState = sessionStorage.getItem("votYandexState");
|
||||
if (!state || state !== storedState) {
|
||||
throw new Error("[VOT] Invalid state value");
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error("[VOT] Invalid auth code response");
|
||||
}
|
||||
|
||||
const promise = exchangeCodeForAccount(code);
|
||||
pendingCodeExchange = { code, state, promise };
|
||||
return promise;
|
||||
}
|
||||
|
||||
export const createAuthLink = async (): Promise<string> => {
|
||||
const state = getUUID();
|
||||
const codeVerifier = createCodeVerifier();
|
||||
|
|
|
|||
111
tests/yandex-auth-callback.test.ts
Normal file
111
tests/yandex-auth-callback.test.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
|
||||
(globalThis as unknown as { DEBUG_MODE: boolean }).DEBUG_MODE = false;
|
||||
|
||||
const memoryStorage = () => {
|
||||
const items = new Map<string, string>();
|
||||
return {
|
||||
getItem: (key: string) => items.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void items.set(key, value),
|
||||
removeItem: (key: string) => void items.delete(key),
|
||||
};
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
configurable: true,
|
||||
value: memoryStorage(),
|
||||
});
|
||||
Object.defineProperty(globalThis, "sessionStorage", {
|
||||
configurable: true,
|
||||
value: memoryStorage(),
|
||||
});
|
||||
|
||||
const { updateAccountByCallbackData } = await import(
|
||||
"../src/modules/auth/yandex"
|
||||
);
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let tokenRequests: string[] = [];
|
||||
|
||||
const jsonResponse = (body: unknown) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
function stubFetch(handleToken: (code: string) => Response) {
|
||||
globalThis.fetch = (async (_url: unknown, init?: RequestInit) => {
|
||||
if (init?.method === "POST") {
|
||||
const code = new URLSearchParams(String(init.body)).get("code") ?? "";
|
||||
tokenRequests.push(code);
|
||||
return handleToken(code);
|
||||
}
|
||||
return jsonResponse({ login: "user", default_avatar_id: "avatar" });
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
const grantToken = () =>
|
||||
jsonResponse({ access_token: "token", expires_in: 3600 });
|
||||
|
||||
const rejectGrant = () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: "invalid_grant",
|
||||
error_description: "Code has expired",
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
|
||||
describe("updateAccountByCallbackData", () => {
|
||||
beforeEach(() => {
|
||||
tokenRequests = [];
|
||||
sessionStorage.setItem("votYandexState", "state");
|
||||
sessionStorage.setItem("votYandexCodeVerifier", "verifier");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test("shares one token exchange between concurrent and later calls with the same code", async () => {
|
||||
stubFetch(grantToken);
|
||||
const data = { state: "state", code: "code-1" };
|
||||
|
||||
await Promise.all([
|
||||
updateAccountByCallbackData(data),
|
||||
updateAccountByCallbackData(data),
|
||||
updateAccountByCallbackData(data),
|
||||
]);
|
||||
await updateAccountByCallbackData(data);
|
||||
|
||||
expect(tokenRequests).toEqual(["code-1"]);
|
||||
});
|
||||
|
||||
test("shares a rejection but still exchanges a different code afterwards", async () => {
|
||||
stubFetch((code) => (code === "code-2" ? rejectGrant() : grantToken()));
|
||||
sessionStorage.setItem("votYandexState", "state-2");
|
||||
const failing = { state: "state-2", code: "code-2" };
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
updateAccountByCallbackData(failing),
|
||||
updateAccountByCallbackData(failing),
|
||||
]);
|
||||
expect(results.map((result) => result.status)).toEqual([
|
||||
"rejected",
|
||||
"rejected",
|
||||
]);
|
||||
expect(tokenRequests).toEqual(["code-2"]);
|
||||
|
||||
sessionStorage.setItem("votYandexState", "state-3");
|
||||
await updateAccountByCallbackData({ state: "state-3", code: "code-3" });
|
||||
expect(tokenRequests).toEqual(["code-2", "code-3"]);
|
||||
});
|
||||
|
||||
test("still rejects an invalid state without sending a request", async () => {
|
||||
stubFetch(grantToken);
|
||||
|
||||
await expect(
|
||||
updateAccountByCallbackData({ state: "wrong", code: "code-4" }),
|
||||
).rejects.toThrow("[VOT] Invalid state value");
|
||||
expect(tokenRequests).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue