From f8c1fe804dcbf2d358d34266aed18e6baab0c855 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 1 Apr 2026 10:44:34 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BF=AE=E5=A4=8D]=20=E4=BF=AE=E5=A4=8Dgithub?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../auth/components/github-oauth-callback.tsx | 20 +++-- .../tests/unit/github-oauth-callback.test.tsx | 88 +++++++++++++++++++ 3 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 openflare_server/web/tests/unit/github-oauth-callback.test.tsx diff --git a/README.md b/README.md index 64fa95e8..883bc748 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/unin 卸载脚本会先停止并移除 `openflare-agent.service`、删除整个 `/opt/openflare-agent` 目录,然后根据卸载前保存的 `agent.json` 判断 OpenResty 安装方式: * Docker 模式:删除对应 OpenResty 容器,并尝试移除镜像 -* 本机 `openresty_path` 模式:不改动本机 OpenResty +* 本机 `openresty_path` 模式:不改动本机 OpenResty,只提示用户手动卸载 ### 3. 发布第一份配置 diff --git a/openflare_server/web/features/auth/components/github-oauth-callback.tsx b/openflare_server/web/features/auth/components/github-oauth-callback.tsx index 96ba83fd..c502261c 100644 --- a/openflare_server/web/features/auth/components/github-oauth-callback.tsx +++ b/openflare_server/web/features/auth/components/github-oauth-callback.tsx @@ -2,7 +2,7 @@ import { useMutation } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; @@ -13,9 +13,11 @@ import { exchangeGitHubCode } from '@/features/auth/api/auth'; export function GitHubOAuthCallback() { const router = useRouter(); const searchParams = useSearchParams(); - const { setUser } = useAuth(); + const { setUser } = useAuth(); + const handledCodeRef = useRef(null); const [prompt, setPrompt] = useState('正在处理 GitHub 授权结果...'); - const [message, setMessage] = useState<{ tone: 'danger' | 'success'; text: string } | null>(null); + const [message, setMessage] = useState<{ tone: 'danger' | 'success'; text: string } | null>(null); + const code = searchParams?.get('code')?.trim() ?? ''; const mutation = useMutation({ mutationFn: exchangeGitHubCode, @@ -31,15 +33,19 @@ export function GitHubOAuthCallback() { }); useEffect(() => { - const code = searchParams?.get('code'); - if (!code) { + if (!code) { setPrompt('缺少授权 code'); setMessage({ tone: 'danger', text: '未收到 GitHub 授权参数,请返回登录页重试。' }); return; } - mutation.mutate(code); - }, [mutation, searchParams]); + if (handledCodeRef.current === code) { + return; + } + + handledCodeRef.current = code; + mutation.mutate(code); + }, [code, mutation]); return ( diff --git a/openflare_server/web/tests/unit/github-oauth-callback.test.tsx b/openflare_server/web/tests/unit/github-oauth-callback.test.tsx new file mode 100644 index 00000000..cc5a889e --- /dev/null +++ b/openflare_server/web/tests/unit/github-oauth-callback.test.tsx @@ -0,0 +1,88 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, waitFor } from '@testing-library/react'; +import { StrictMode, type ReactNode } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { GitHubOAuthCallback } from '@/features/auth/components/github-oauth-callback'; + +const replaceMock = vi.fn(); +const setUserMock = vi.fn(); +const exchangeGitHubCodeMock = vi.fn(); + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + replace: replaceMock, + }), + useSearchParams: () => new URLSearchParams('code=github-code-123'), +})); + +vi.mock('@/components/providers/auth-provider', () => ({ + useAuth: () => ({ + setUser: setUserMock, + }), +})); + +vi.mock('@/features/auth/api/auth', () => ({ + exchangeGitHubCode: (code: string) => exchangeGitHubCodeMock(code), +})); + +function createQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }); +} + +function renderWithProviders(ui: ReactNode) { + const queryClient = createQueryClient(); + + return render( + + {ui} + , + ); +} + +describe('GitHubOAuthCallback', () => { + beforeEach(() => { + replaceMock.mockReset(); + setUserMock.mockReset(); + exchangeGitHubCodeMock.mockReset(); + exchangeGitHubCodeMock.mockResolvedValue({ + id: 1, + username: 'github-user', + role: 1, + status: 1, + }); + }); + + it('exchanges the same GitHub code only once', async () => { + const view = renderWithProviders(); + + await waitFor(() => { + expect(exchangeGitHubCodeMock).toHaveBeenCalledTimes(1); + }); + + view.rerender( + + + + + , + ); + + await waitFor(() => { + expect(replaceMock).toHaveBeenCalledWith('/'); + }); + + expect(exchangeGitHubCodeMock).toHaveBeenCalledTimes(1); + expect(exchangeGitHubCodeMock).toHaveBeenCalledWith('github-code-123'); + expect(setUserMock).toHaveBeenCalledTimes(1); + }); +});