From e65adaabc7d62935431fb134f52988853694ecae Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:48:41 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix(auth):=20=E5=86=85=E6=B5=8B=E6=9C=9F?= =?UTF-8?q?=E9=97=B4=E5=85=B3=E9=97=AD=E5=85=AC=E5=BC=80=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=EF=BC=8C=E4=BB=85=E4=BF=9D=E7=95=99=E5=B7=B2=E6=9C=89=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公开注册、注册发码和验证码自动建号都会在内测阶段误开账号。登录入口保留,测试账号由运维预置。 Fixes #326 Co-authored-by: Cursor --- .../src/windup_app/server/user/interface.py | 4 +- .../app/src/windup_app/server/user/service.py | 23 ++-- .../app/src/windup_app/web/api/auth.py | 24 ++-- .../tests/test_auth_registration_closed.py | 30 +++++ backend/tests/test_user_service.py | 45 +++---- frontend/src/app/layout/app-header.test.tsx | 6 +- frontend/src/app/layout/app-header.tsx | 4 +- .../src/features/account-panel/index.test.tsx | 118 +++--------------- frontend/src/features/account-panel/index.tsx | 21 +--- frontend/src/pages/landing/index.test.tsx | 4 +- .../src/pages/landing/marketing-header.tsx | 13 +- 11 files changed, 94 insertions(+), 198 deletions(-) create mode 100644 backend/tests/test_auth_registration_closed.py diff --git a/backend/packages/app/src/windup_app/server/user/interface.py b/backend/packages/app/src/windup_app/server/user/interface.py index 84c22755..ec361a9a 100644 --- a/backend/packages/app/src/windup_app/server/user/interface.py +++ b/backend/packages/app/src/windup_app/server/user/interface.py @@ -46,9 +46,9 @@ def send_verification_code(self, email: str, purpose: str) -> None: @abstractmethod def login_by_code(self, input: LoginByCodeInput) -> LoginResult: - """邮箱+验证码登录,无账号时自动注册。 + """邮箱+验证码登录。内测期间不自动建号。 - :raises windup_common.exceptions.BizException: 验证码错误 / 已过期 / 账号已封禁。 + :raises windup_common.exceptions.BizException: 验证码错误 / 已过期 / 账号不存在 / 账号已封禁。 """ # -- 登出 ------------------------------------------------------------ diff --git a/backend/packages/app/src/windup_app/server/user/service.py b/backend/packages/app/src/windup_app/server/user/service.py index c80dbc27..87dd8551 100644 --- a/backend/packages/app/src/windup_app/server/user/service.py +++ b/backend/packages/app/src/windup_app/server/user/service.py @@ -280,6 +280,9 @@ def login_by_password_with_session( def send_verification_code(self, email: str, purpose: str) -> None: """发送邮箱验证码。""" + if purpose == "register": + raise BizException("内测期间暂不开放注册", code=BizCode.BAD_REQUEST) + # 频率限制 cooldown_key = VERIFY_COOLDOWN_KEY.format(email=email) if self.redis.get(cooldown_key): @@ -315,25 +318,17 @@ def login_by_code(self, input: LoginByCodeInput) -> LoginResult: def login_by_code_with_session( self, session: Session, input: LoginByCodeInput ) -> LoginResult: - """邮箱+验证码登录,无账号自动注册(带 session)。""" + """邮箱+验证码登录(带 session)。内测期间不自动建号。""" # 校验验证码 self._verify_code(input.email, input.code, "login") - # 查找或创建用户 user = session.scalar(select(User).where(User.email == input.email)) if user is None: - user = User(email=input.email, email_verified_at=datetime.now(timezone.utc)) - session.add(user) - session.flush() - # 自动注册送积分 - self._create_credit_account(session, user.id) - logger.info("[WINDUP] 验证码自动注册 | user_id=%s email=%s", user.id, user.email) - else: - if user.status == UserStatus.BANNED: - raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) - # 标记邮箱已验证 - if user.email_verified_at is None: - user.email_verified_at = datetime.now(timezone.utc) + raise BizException("账号不存在", code=BizCode.NOT_FOUND) + if user.status == UserStatus.BANNED: + raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) + if user.email_verified_at is None: + user.email_verified_at = datetime.now(timezone.utc) user.last_login_at = datetime.now(timezone.utc) session.flush() diff --git a/backend/packages/app/src/windup_app/web/api/auth.py b/backend/packages/app/src/windup_app/web/api/auth.py index b196a024..8570ac0c 100644 --- a/backend/packages/app/src/windup_app/web/api/auth.py +++ b/backend/packages/app/src/windup_app/web/api/auth.py @@ -111,19 +111,15 @@ class UserOut(BaseModel): @router.post("/register", response_model=Response[TokenResponse]) def register(body: RegisterRequest, session: Session = Depends(get_session)): - """邮箱+验证码+密码注册,注册即登录。""" - result = service.register_by_email_with_session( - session, - type("RegisterInput", (), {"email": body.email, "password": body.password, "code": body.code, "nickname": body.nickname})(), - ) - return Response.success( - TokenResponse( - access_token=result.access_token, - refresh_token=result.refresh_token, - user=result.user, - ), - message="注册成功", - ) + """邮箱+验证码+密码注册。 + + 内测期间关闭公开注册,路由与请求模型保留以便以后重新开放。 + """ + from windup_common.enums.biz_code import BizCode + from windup_common.exceptions import BizException + + del body, session + raise BizException("内测期间暂不开放注册", code=BizCode.BAD_REQUEST) @router.post("/login", response_model=Response[TokenResponse]) @@ -152,7 +148,7 @@ def send_code(body: SendCodeRequest): @router.post("/login-by-code", response_model=Response[TokenResponse]) def login_by_code(body: LoginByCodeRequest, session: Session = Depends(get_session)): - """验证码登录,无账号自动注册。""" + """验证码登录。内测期间不自动注册。""" result = service.login_by_code_with_session( session, type("LoginByCodeInput", (), {"email": body.email, "code": body.code})(), diff --git a/backend/tests/test_auth_registration_closed.py b/backend/tests/test_auth_registration_closed.py new file mode 100644 index 00000000..aa77c335 --- /dev/null +++ b/backend/tests/test_auth_registration_closed.py @@ -0,0 +1,30 @@ +"""内测关闭公开注册:公开建号路径必须被拒绝。""" + +from windup_common.enums.biz_code import BizCode + + +def test_register_endpoint_rejects_public_signup(client): + resp = client.post( + "/auth/register", + json={ + "email": "new@example.com", + "password": "password123", + "code": "123456", + }, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == BizCode.BAD_REQUEST + assert body["message"] == "内测期间暂不开放注册" + assert body["data"] is None + + +def test_send_code_rejects_register_purpose(client): + resp = client.post( + "/auth/send-code", + json={"email": "new@example.com", "purpose": "register"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == BizCode.BAD_REQUEST + assert body["message"] == "内测期间暂不开放注册" diff --git a/backend/tests/test_user_service.py b/backend/tests/test_user_service.py index e8771584..9ca6f414 100644 --- a/backend/tests/test_user_service.py +++ b/backend/tests/test_user_service.py @@ -282,46 +282,29 @@ def test_login_banned_user(db_session, service, mock_email): # -- 验证码登录测试 ------------------------------------------------------ -def test_login_by_code_new_user(db_session, service, mock_email): - service._redis.get.return_value = "123456" +def test_login_by_code_unknown_email_does_not_create_user(db_session, service, mock_email): + """内测关闭公开注册后,验证码登录不得自动建号。""" + from sqlalchemy import select + service._redis.get.return_value = "123456" input_data = LoginByCodeInput(email="code@example.com", code="123456") - result = service.login_by_code_with_session(db_session, input_data) - assert result.user.email == "code@example.com" - assert result.user.email_verified_at is not None + with pytest.raises(BizException, match="账号不存在") as exc: + service.login_by_code_with_session(db_session, input_data) + from windup_common.enums.biz_code import BizCode -def test_login_by_code_new_user_creates_credit_account(db_session, service, mock_email): - """验证码自动注册时应创建积分账户并赠送初始积分。""" - from sqlalchemy import select - from windup_app.server.quota.model import CreditAccount, CreditTransaction - from windup_common.enums.quota import CreditReason - from windup_framework.config.quota import settings as quota_settings + assert exc.value.code == BizCode.NOT_FOUND + assert db_session.scalar(select(User).where(User.email == "code@example.com")) is None - service._redis.get.return_value = "123456" - input_data = LoginByCodeInput(email="auto@example.com", code="123456") - result = service.login_by_code_with_session(db_session, input_data) - user_id = result.user.id +def test_send_verification_code_rejects_register_purpose(service, mock_email): + service._redis.get.return_value = None - # 验证积分账户已创建 - account = db_session.scalar( - select(CreditAccount).where(CreditAccount.user_id == user_id) - ) - assert account is not None - assert account.balance == quota_settings.register_gift_amount - assert account.total_earned == quota_settings.register_gift_amount + with pytest.raises(BizException, match="内测期间暂不开放注册"): + service.send_verification_code("new@example.com", "register") - # 验证赠送流水已记录 - txn = db_session.scalar( - select(CreditTransaction).where( - CreditTransaction.user_id == user_id, - CreditTransaction.reason == CreditReason.REGISTER_GIFT, - ) - ) - assert txn is not None - assert txn.delta == quota_settings.register_gift_amount + mock_email.send_verification_code.assert_not_called() def test_login_by_code_existing_user(db_session, service, mock_email): diff --git a/frontend/src/app/layout/app-header.test.tsx b/frontend/src/app/layout/app-header.test.tsx index e99819cf..f50c40ba 100644 --- a/frontend/src/app/layout/app-header.test.tsx +++ b/frontend/src/app/layout/app-header.test.tsx @@ -176,7 +176,7 @@ describe('AppHeader', () => { it('为访客提供可发现的登录入口并保留完整站内回跳地址', async () => { renderHeader('/quick-start?mode=fast#brief') - const entry = await screen.findByRole('link', { name: '登录 / 注册' }) + const entry = await screen.findByRole('link', { name: '登录' }) expect(entry.getAttribute('href')).toBe( '/?account=login&returnTo=%2Fquick-start%3Fmode%3Dfast%23brief', ) @@ -192,7 +192,7 @@ describe('AppHeader', () => { fireEvent.click(screen.getByRole('button', { name: '退出登录' })) await waitFor(() => expect(screen.getByTestId('location').textContent).toBe('/')) - expect(await screen.findByRole('link', { name: '登录 / 注册' })).toBeTruthy() + expect(await screen.findByRole('link', { name: '登录' })).toBeTruthy() expect(apis.logout).toHaveBeenCalledWith('rotated-refresh-token') }) @@ -206,7 +206,7 @@ describe('AppHeader', () => { fireEvent.click(screen.getByRole('button', { name: '退出登录' })) await waitFor(() => expect(screen.getByTestId('location').textContent).toBe('/')) - expect(await screen.findByRole('link', { name: '登录 / 注册' })).toBeTruthy() + expect(await screen.findByRole('link', { name: '登录' })).toBeTruthy() }) it('没有昵称时使用邮箱展示账号身份', async () => { diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index 9660b4cd..6130c3d4 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -190,10 +190,10 @@ export function AppHeader() { ) : session.state.status === 'guest' ? ( - 登录 / 注册 + 登录 登录 ) : ( diff --git a/frontend/src/features/account-panel/index.test.tsx b/frontend/src/features/account-panel/index.test.tsx index bf83965e..7e46035c 100644 --- a/frontend/src/features/account-panel/index.test.tsx +++ b/frontend/src/features/account-panel/index.test.tsx @@ -94,29 +94,21 @@ describe('AccountPanel', () => { expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() expect(screen.getByRole('heading', { name: '欢迎回来。' })).toBeTruthy() - expect(screen.getByText('未注册的邮箱将在验证后自动创建账号。')).toBeTruthy() + expect(screen.queryByText('未注册的邮箱将在验证后自动创建账号。')).toBeNull() expect(screen.queryByRole('tab', { name: '注册' })).toBeNull() - expect(screen.getByRole('button', { name: '创建账号' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) - it('opens registration as a centered, progressive form', async () => { - renderPanel('/?account=register&returnTo=%2Fworkspace') - - const dialog = screen.getByRole('dialog', { name: '创建 Windup 账号' }) - expect(screen.getByRole('heading', { name: '欢迎来到 Windup' })).toBeTruthy() - expect(dialog.className).toContain('max-w-[34rem]') - expect(screen.getByText('从一个角色开始,')).toBeTruthy() - expect(screen.queryByText('继续搭建,')).toBeNull() - expect(screen.getByTestId('register-fields').className).toContain('auth-register-fields') - expect(screen.queryByRole('tablist', { name: '账号操作' })).toBeNull() - expect(screen.getByLabelText('邮箱')).toBeTruthy() - expect(screen.queryByLabelText('密码')).toBeNull() - expect(screen.queryByLabelText('昵称(选填)')).toBeNull() - expect(screen.queryByLabelText('验证码')).toBeNull() - expect(screen.getByTestId('register-fields').querySelector('[aria-live]')).toBeNull() - expect(screen.getByRole('button', { name: '继续' })).toBeTruthy() + it('opens a closed-registration URL as login and never starts signup', async () => { + const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') + + expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '欢迎回来。' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() + expect(screen.queryByRole('button', { name: '继续' })).toBeNull() expect(screen.getByRole('button', { name: '登录' })).toBeTruthy() + expect(apis.register).not.toHaveBeenCalled() await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) @@ -141,15 +133,15 @@ describe('AccountPanel', () => { const dialog = screen.getByRole('dialog', { name: '登录 Windup' }) const closeButton = screen.getByRole('button', { name: '关闭账号面板' }) - const entrySwitch = screen.getByRole('button', { name: '创建账号' }) + const submitButton = screen.getByRole('button', { name: '登录' }) - entrySwitch.focus() - fireEvent.keyDown(entrySwitch, { key: 'Tab' }) + submitButton.focus() + fireEvent.keyDown(submitButton, { key: 'Tab' }) expect(document.activeElement).toBe(closeButton) closeButton.focus() fireEvent.keyDown(closeButton, { key: 'Tab', shiftKey: true }) - expect(document.activeElement).toBe(entrySwitch) + expect(document.activeElement).toBe(submitButton) expect(dialog.contains(document.activeElement)).toBe(true) }) @@ -266,9 +258,7 @@ describe('AccountPanel', () => { email: 'reader@example.com', code: '123456', }) - expect( - screen.getByText('登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。').textContent, - ).toContain('登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。') + expect(screen.getByText('登录成功,正在继续。').textContent).toContain('登录成功,正在继续。') await act(async () => vi.advanceTimersByTimeAsync(900)) expect(screen.getByTestId('location').textContent).toBe('/projects?view=recent') @@ -348,85 +338,15 @@ describe('AccountPanel', () => { expect(apis.sendCode).not.toHaveBeenCalled() }) - it('preserves registration input when showing a password and returning a step', async () => { - renderPanel('/?account=register') - fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - - const password = await screen.findByLabelText('密码') - expect(password.getAttribute('type')).toBe('password') - fireEvent.click(screen.getByRole('button', { name: '显示密码' })) - expect(password.getAttribute('type')).toBe('text') - - fireEvent.click(screen.getByRole('button', { name: '返回上一步' })) - expect(((await screen.findByLabelText('邮箱')) as HTMLInputElement).value).toBe( - 'new@example.com', - ) - expect(screen.getByTestId('auth-motion-stage').dataset.motionDirection).toBe('backward') - }) - - it('switches account entry only after the current panel exits', async () => { + it('does not expose a signup switch from the login panel', async () => { vi.useFakeTimers() renderPanel('/?account=login&returnTo=%2Fworkspace') - fireEvent.click(screen.getByRole('button', { name: '创建账号' })) - expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') - await act(async () => vi.advanceTimersByTimeAsync(520)) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByTestId('location').textContent).toBe( - '/?account=register&returnTo=%2Fworkspace', - ) - }) - - it('validates each registration step and reuses the existing register API contract', async () => { - const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') - fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) - - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '为账号加一道保护' })).toBeTruthy() - expect(await screen.findByLabelText('密码')).toBeTruthy() - fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'short' } }) - - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect((await screen.findByRole('alert')).textContent).toContain('密码需为 8–128 位') - expect(apis.register).not.toHaveBeenCalled() - - fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password-123' } }) - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '留下你的称呼' })).toBeTruthy() - expect(await screen.findByLabelText('昵称(选填)')).toBeTruthy() - fireEvent.change(screen.getByLabelText('昵称(选填)'), { - target: { value: 'N'.repeat(51) }, - }) - - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect((await screen.findByRole('alert')).textContent).toContain('昵称不能超过 50 个字符') - - fireEvent.change(screen.getByLabelText('昵称(选填)'), { target: { value: '新用户' } }) - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - await waitFor(() => - expect(apis.sendCode).toHaveBeenCalledWith({ - email: 'new@example.com', - purpose: 'register', - }), - ) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '确认你的邮箱' })).toBeTruthy() - expect(await screen.findByLabelText('验证码')).toBeTruthy() - fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' } }) - fireEvent.submit(screen.getByRole('button', { name: '创建账号' }).closest('form')!) - await waitFor(() => - expect(apis.register).toHaveBeenCalledWith({ - email: 'new@example.com', - password: 'password-123', - code: '123456', - nickname: '新用户', - }), - ) + expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') }) }) diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 212b7803..16b4d067 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -230,7 +230,7 @@ export function AccountPanel() { const entry = searchParams.get('account') if (entry !== 'login' && entry !== 'register') return null - return + return } /** 只有面板真正打开时才读取会话,关闭状态不把认证 Context 强加给应用外壳。 */ @@ -374,14 +374,6 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { window.requestAnimationFrame(() => emailInputRef.current?.focus()) } - function switchEntry(nextEntry: AccountEntry) { - leaveWithAnimation(() => { - const next = new URLSearchParams(searchParams) - next.set('account', nextEntry) - setSearchParams(next, { replace: true }) - }) - } - async function sendCode(): Promise { if (isSendingCode || cooldownSeconds > 0) return false if (!EMAIL_PATTERN.test(normalizedEmail)) { @@ -488,7 +480,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { successMessage = '账号已创建,正在继续。' } else if (mode === 'code') { await session.loginByCode({ email: normalizedEmail, code }) - successMessage = '登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。' + successMessage = '登录成功,正在继续。' } else { await session.login({ email: normalizedEmail, password }) successMessage = '登录成功,正在继续。' @@ -780,7 +772,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { {!isRegister && mode === 'code' && (

- 未注册的邮箱将在验证后自动创建账号。 + 内测期间仅支持已有账号登录。

)} @@ -805,13 +797,6 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { - -

- {isRegister ? '已有账号?' : '还没有账号?'}{' '} - -

diff --git a/frontend/src/pages/landing/index.test.tsx b/frontend/src/pages/landing/index.test.tsx index 3190cb52..bd2864f5 100644 --- a/frontend/src/pages/landing/index.test.tsx +++ b/frontend/src/pages/landing/index.test.tsx @@ -56,9 +56,7 @@ describe('LandingPage', () => { expect(screen.getByRole('link', { name: '登录' }).getAttribute('href')).toBe( '/?account=login&returnTo=%2Fworkspace', ) - expect(screen.getByRole('link', { name: '注册' }).getAttribute('href')).toBe( - '/?account=register&returnTo=%2Fworkspace', - ) + expect(screen.queryByRole('link', { name: '注册' })).toBeNull() // Header 只处理账号入口,Hero 与收尾负责把用户带进创作。 const creationLinks = screen.getAllByRole('link', { name: '开始创作' }) expect(creationLinks).toHaveLength(2) diff --git a/frontend/src/pages/landing/marketing-header.tsx b/frontend/src/pages/landing/marketing-header.tsx index 755aae9e..7f3c8946 100644 --- a/frontend/src/pages/landing/marketing-header.tsx +++ b/frontend/src/pages/landing/marketing-header.tsx @@ -7,11 +7,6 @@ const loginEntry = `/?${new URLSearchParams({ returnTo: '/workspace', })}` -const registerEntry = `/?${new URLSearchParams({ - account: 'register', - returnTo: '/workspace', -})}` - const sections = [ ['#capabilities', '产品能力'], ['#workflow', '制作流程'], @@ -73,15 +68,9 @@ export function MarketingHeader() { <> - 登录 - - - 注册 + 登录 )} From 7601f2a80329c70d990991c1559f8bff7be61de8 Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:58:43 +0800 Subject: [PATCH 2/6] style(auth): format closed-registration helper copy oxfmt requires the login helper line to stay on one line. This unblocks Frontend CI format:check on #327. Co-authored-by: Cursor --- frontend/src/features/account-panel/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 16b4d067..0acaef56 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -771,9 +771,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { )} {!isRegister && mode === 'code' && ( -

- 内测期间仅支持已有账号登录。 -

+

内测期间仅支持已有账号登录。

)} From 503cce7e8d9be5cc7cc5c13b45014e0d81fb3f6a Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:11:58 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(auth):=20=E6=92=A4=E5=9B=9E=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=94=B9=E5=8A=A8=E5=B9=B6=E8=A1=A5=E4=B8=8A=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E7=99=BB=E5=BD=95=E5=88=86=E6=94=AF=E8=A6=86?= =?UTF-8?q?=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公开注册只在后端关闭。前端注册入口保持原样,调用时由接口拒绝。补测封禁账号与未验证邮箱的验证码登录,补齐 codecov/patch 缺的两行。 Co-authored-by: Cursor --- backend/tests/test_user_service.py | 35 ++++++ frontend/src/app/layout/app-header.test.tsx | 6 +- frontend/src/app/layout/app-header.tsx | 4 +- .../src/features/account-panel/index.test.tsx | 118 +++++++++++++++--- frontend/src/features/account-panel/index.tsx | 23 +++- frontend/src/pages/landing/index.test.tsx | 4 +- .../src/pages/landing/marketing-header.tsx | 13 +- 7 files changed, 174 insertions(+), 29 deletions(-) diff --git a/backend/tests/test_user_service.py b/backend/tests/test_user_service.py index 9ca6f414..184bd356 100644 --- a/backend/tests/test_user_service.py +++ b/backend/tests/test_user_service.py @@ -307,6 +307,41 @@ def test_send_verification_code_rejects_register_purpose(service, mock_email): mock_email.send_verification_code.assert_not_called() +def test_login_by_code_banned_user(db_session, service, mock_email): + from sqlalchemy import select + + service._redis.get.return_value = "123456" + service.register_by_email_with_session( + db_session, + RegisterInput(email="banned-code@example.com", password="pass123", code="123456"), + ) + user = db_session.scalar(select(User).where(User.email == "banned-code@example.com")) + user.status = UserStatus.BANNED + db_session.flush() + + service._redis.get.return_value = "654321" + with pytest.raises(BizException, match="账号已被封禁"): + service.login_by_code_with_session( + db_session, + LoginByCodeInput(email="banned-code@example.com", code="654321"), + ) + + +def test_login_by_code_marks_unverified_email(db_session, service): + user = User(email="unverified@example.com", password_hash=_hash_password("pass123")) + db_session.add(user) + db_session.flush() + + service._redis.get.return_value = "123456" + result = service.login_by_code_with_session( + db_session, + LoginByCodeInput(email="unverified@example.com", code="123456"), + ) + + assert result.user.email == "unverified@example.com" + assert result.user.email_verified_at is not None + + def test_login_by_code_existing_user(db_session, service, mock_email): # 先注册 service._redis.get.return_value = "123456" diff --git a/frontend/src/app/layout/app-header.test.tsx b/frontend/src/app/layout/app-header.test.tsx index f50c40ba..e99819cf 100644 --- a/frontend/src/app/layout/app-header.test.tsx +++ b/frontend/src/app/layout/app-header.test.tsx @@ -176,7 +176,7 @@ describe('AppHeader', () => { it('为访客提供可发现的登录入口并保留完整站内回跳地址', async () => { renderHeader('/quick-start?mode=fast#brief') - const entry = await screen.findByRole('link', { name: '登录' }) + const entry = await screen.findByRole('link', { name: '登录 / 注册' }) expect(entry.getAttribute('href')).toBe( '/?account=login&returnTo=%2Fquick-start%3Fmode%3Dfast%23brief', ) @@ -192,7 +192,7 @@ describe('AppHeader', () => { fireEvent.click(screen.getByRole('button', { name: '退出登录' })) await waitFor(() => expect(screen.getByTestId('location').textContent).toBe('/')) - expect(await screen.findByRole('link', { name: '登录' })).toBeTruthy() + expect(await screen.findByRole('link', { name: '登录 / 注册' })).toBeTruthy() expect(apis.logout).toHaveBeenCalledWith('rotated-refresh-token') }) @@ -206,7 +206,7 @@ describe('AppHeader', () => { fireEvent.click(screen.getByRole('button', { name: '退出登录' })) await waitFor(() => expect(screen.getByTestId('location').textContent).toBe('/')) - expect(await screen.findByRole('link', { name: '登录' })).toBeTruthy() + expect(await screen.findByRole('link', { name: '登录 / 注册' })).toBeTruthy() }) it('没有昵称时使用邮箱展示账号身份', async () => { diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index 6130c3d4..9660b4cd 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -190,10 +190,10 @@ export function AppHeader() { ) : session.state.status === 'guest' ? ( - 登录 + 登录 / 注册 登录 ) : ( diff --git a/frontend/src/features/account-panel/index.test.tsx b/frontend/src/features/account-panel/index.test.tsx index 7e46035c..bf83965e 100644 --- a/frontend/src/features/account-panel/index.test.tsx +++ b/frontend/src/features/account-panel/index.test.tsx @@ -94,21 +94,29 @@ describe('AccountPanel', () => { expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() expect(screen.getByRole('heading', { name: '欢迎回来。' })).toBeTruthy() - expect(screen.queryByText('未注册的邮箱将在验证后自动创建账号。')).toBeNull() + expect(screen.getByText('未注册的邮箱将在验证后自动创建账号。')).toBeTruthy() expect(screen.queryByRole('tab', { name: '注册' })).toBeNull() - expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() + expect(screen.getByRole('button', { name: '创建账号' })).toBeTruthy() await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) - it('opens a closed-registration URL as login and never starts signup', async () => { - const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') - - expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '欢迎回来。' })).toBeTruthy() - expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() - expect(screen.queryByRole('button', { name: '继续' })).toBeNull() + it('opens registration as a centered, progressive form', async () => { + renderPanel('/?account=register&returnTo=%2Fworkspace') + + const dialog = screen.getByRole('dialog', { name: '创建 Windup 账号' }) + expect(screen.getByRole('heading', { name: '欢迎来到 Windup' })).toBeTruthy() + expect(dialog.className).toContain('max-w-[34rem]') + expect(screen.getByText('从一个角色开始,')).toBeTruthy() + expect(screen.queryByText('继续搭建,')).toBeNull() + expect(screen.getByTestId('register-fields').className).toContain('auth-register-fields') + expect(screen.queryByRole('tablist', { name: '账号操作' })).toBeNull() + expect(screen.getByLabelText('邮箱')).toBeTruthy() + expect(screen.queryByLabelText('密码')).toBeNull() + expect(screen.queryByLabelText('昵称(选填)')).toBeNull() + expect(screen.queryByLabelText('验证码')).toBeNull() + expect(screen.getByTestId('register-fields').querySelector('[aria-live]')).toBeNull() + expect(screen.getByRole('button', { name: '继续' })).toBeTruthy() expect(screen.getByRole('button', { name: '登录' })).toBeTruthy() - expect(apis.register).not.toHaveBeenCalled() await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) @@ -133,15 +141,15 @@ describe('AccountPanel', () => { const dialog = screen.getByRole('dialog', { name: '登录 Windup' }) const closeButton = screen.getByRole('button', { name: '关闭账号面板' }) - const submitButton = screen.getByRole('button', { name: '登录' }) + const entrySwitch = screen.getByRole('button', { name: '创建账号' }) - submitButton.focus() - fireEvent.keyDown(submitButton, { key: 'Tab' }) + entrySwitch.focus() + fireEvent.keyDown(entrySwitch, { key: 'Tab' }) expect(document.activeElement).toBe(closeButton) closeButton.focus() fireEvent.keyDown(closeButton, { key: 'Tab', shiftKey: true }) - expect(document.activeElement).toBe(submitButton) + expect(document.activeElement).toBe(entrySwitch) expect(dialog.contains(document.activeElement)).toBe(true) }) @@ -258,7 +266,9 @@ describe('AccountPanel', () => { email: 'reader@example.com', code: '123456', }) - expect(screen.getByText('登录成功,正在继续。').textContent).toContain('登录成功,正在继续。') + expect( + screen.getByText('登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。').textContent, + ).toContain('登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。') await act(async () => vi.advanceTimersByTimeAsync(900)) expect(screen.getByTestId('location').textContent).toBe('/projects?view=recent') @@ -338,15 +348,85 @@ describe('AccountPanel', () => { expect(apis.sendCode).not.toHaveBeenCalled() }) - it('does not expose a signup switch from the login panel', async () => { + it('preserves registration input when showing a password and returning a step', async () => { + renderPanel('/?account=register') + fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + + const password = await screen.findByLabelText('密码') + expect(password.getAttribute('type')).toBe('password') + fireEvent.click(screen.getByRole('button', { name: '显示密码' })) + expect(password.getAttribute('type')).toBe('text') + + fireEvent.click(screen.getByRole('button', { name: '返回上一步' })) + expect(((await screen.findByLabelText('邮箱')) as HTMLInputElement).value).toBe( + 'new@example.com', + ) + expect(screen.getByTestId('auth-motion-stage').dataset.motionDirection).toBe('backward') + }) + + it('switches account entry only after the current panel exits', async () => { vi.useFakeTimers() renderPanel('/?account=login&returnTo=%2Fworkspace') - expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() - expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') - await act(async () => vi.advanceTimersByTimeAsync(520)) + fireEvent.click(screen.getByRole('button', { name: '创建账号' })) expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') + + await act(async () => vi.advanceTimersByTimeAsync(520)) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByTestId('location').textContent).toBe( + '/?account=register&returnTo=%2Fworkspace', + ) + }) + + it('validates each registration step and reuses the existing register API contract', async () => { + const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') + fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) + + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '为账号加一道保护' })).toBeTruthy() + expect(await screen.findByLabelText('密码')).toBeTruthy() + fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'short' } }) + + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect((await screen.findByRole('alert')).textContent).toContain('密码需为 8–128 位') + expect(apis.register).not.toHaveBeenCalled() + + fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password-123' } }) + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '留下你的称呼' })).toBeTruthy() + expect(await screen.findByLabelText('昵称(选填)')).toBeTruthy() + fireEvent.change(screen.getByLabelText('昵称(选填)'), { + target: { value: 'N'.repeat(51) }, + }) + + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect((await screen.findByRole('alert')).textContent).toContain('昵称不能超过 50 个字符') + + fireEvent.change(screen.getByLabelText('昵称(选填)'), { target: { value: '新用户' } }) + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + await waitFor(() => + expect(apis.sendCode).toHaveBeenCalledWith({ + email: 'new@example.com', + purpose: 'register', + }), + ) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '确认你的邮箱' })).toBeTruthy() + expect(await screen.findByLabelText('验证码')).toBeTruthy() + fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' } }) + fireEvent.submit(screen.getByRole('button', { name: '创建账号' }).closest('form')!) + await waitFor(() => + expect(apis.register).toHaveBeenCalledWith({ + email: 'new@example.com', + password: 'password-123', + code: '123456', + nickname: '新用户', + }), + ) }) }) diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 0acaef56..212b7803 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -230,7 +230,7 @@ export function AccountPanel() { const entry = searchParams.get('account') if (entry !== 'login' && entry !== 'register') return null - return + return } /** 只有面板真正打开时才读取会话,关闭状态不把认证 Context 强加给应用外壳。 */ @@ -374,6 +374,14 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { window.requestAnimationFrame(() => emailInputRef.current?.focus()) } + function switchEntry(nextEntry: AccountEntry) { + leaveWithAnimation(() => { + const next = new URLSearchParams(searchParams) + next.set('account', nextEntry) + setSearchParams(next, { replace: true }) + }) + } + async function sendCode(): Promise { if (isSendingCode || cooldownSeconds > 0) return false if (!EMAIL_PATTERN.test(normalizedEmail)) { @@ -480,7 +488,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { successMessage = '账号已创建,正在继续。' } else if (mode === 'code') { await session.loginByCode({ email: normalizedEmail, code }) - successMessage = '登录成功,正在继续。' + successMessage = '登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。' } else { await session.login({ email: normalizedEmail, password }) successMessage = '登录成功,正在继续。' @@ -771,7 +779,9 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { )} {!isRegister && mode === 'code' && ( -

内测期间仅支持已有账号登录。

+

+ 未注册的邮箱将在验证后自动创建账号。 +

)} @@ -795,6 +805,13 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { + +

+ {isRegister ? '已有账号?' : '还没有账号?'}{' '} + +

diff --git a/frontend/src/pages/landing/index.test.tsx b/frontend/src/pages/landing/index.test.tsx index bd2864f5..3190cb52 100644 --- a/frontend/src/pages/landing/index.test.tsx +++ b/frontend/src/pages/landing/index.test.tsx @@ -56,7 +56,9 @@ describe('LandingPage', () => { expect(screen.getByRole('link', { name: '登录' }).getAttribute('href')).toBe( '/?account=login&returnTo=%2Fworkspace', ) - expect(screen.queryByRole('link', { name: '注册' })).toBeNull() + expect(screen.getByRole('link', { name: '注册' }).getAttribute('href')).toBe( + '/?account=register&returnTo=%2Fworkspace', + ) // Header 只处理账号入口,Hero 与收尾负责把用户带进创作。 const creationLinks = screen.getAllByRole('link', { name: '开始创作' }) expect(creationLinks).toHaveLength(2) diff --git a/frontend/src/pages/landing/marketing-header.tsx b/frontend/src/pages/landing/marketing-header.tsx index 7f3c8946..755aae9e 100644 --- a/frontend/src/pages/landing/marketing-header.tsx +++ b/frontend/src/pages/landing/marketing-header.tsx @@ -7,6 +7,11 @@ const loginEntry = `/?${new URLSearchParams({ returnTo: '/workspace', })}` +const registerEntry = `/?${new URLSearchParams({ + account: 'register', + returnTo: '/workspace', +})}` + const sections = [ ['#capabilities', '产品能力'], ['#workflow', '制作流程'], @@ -68,10 +73,16 @@ export function MarketingHeader() { <> 登录 + + 注册 + )} From 5aa8d36982af11be0f823f9bdfaf6e75dee7fb6f Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:48:22 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(auth):=20=E5=89=8D=E7=AB=AF=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E6=B3=A8=E5=86=8C=E5=85=A5=E5=8F=A3=E5=B9=B6=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E9=80=9A=E8=BF=87=20Issues=20=E7=94=B3=E8=AF=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 访客仍能看到注册按钮,但不可提交。登录面板引导通过 GitHub Issues 联系团队开通。 --- frontend/src/app/layout/app-header.test.tsx | 6 +- frontend/src/app/layout/app-header.tsx | 4 +- .../features/account-panel/account-panel.css | 10 +- .../src/features/account-panel/index.test.tsx | 122 ++++-------------- frontend/src/features/account-panel/index.tsx | 28 ++-- frontend/src/pages/landing/index.test.tsx | 8 +- .../src/pages/landing/marketing-header.tsx | 29 +++-- 7 files changed, 68 insertions(+), 139 deletions(-) diff --git a/frontend/src/app/layout/app-header.test.tsx b/frontend/src/app/layout/app-header.test.tsx index e99819cf..f50c40ba 100644 --- a/frontend/src/app/layout/app-header.test.tsx +++ b/frontend/src/app/layout/app-header.test.tsx @@ -176,7 +176,7 @@ describe('AppHeader', () => { it('为访客提供可发现的登录入口并保留完整站内回跳地址', async () => { renderHeader('/quick-start?mode=fast#brief') - const entry = await screen.findByRole('link', { name: '登录 / 注册' }) + const entry = await screen.findByRole('link', { name: '登录' }) expect(entry.getAttribute('href')).toBe( '/?account=login&returnTo=%2Fquick-start%3Fmode%3Dfast%23brief', ) @@ -192,7 +192,7 @@ describe('AppHeader', () => { fireEvent.click(screen.getByRole('button', { name: '退出登录' })) await waitFor(() => expect(screen.getByTestId('location').textContent).toBe('/')) - expect(await screen.findByRole('link', { name: '登录 / 注册' })).toBeTruthy() + expect(await screen.findByRole('link', { name: '登录' })).toBeTruthy() expect(apis.logout).toHaveBeenCalledWith('rotated-refresh-token') }) @@ -206,7 +206,7 @@ describe('AppHeader', () => { fireEvent.click(screen.getByRole('button', { name: '退出登录' })) await waitFor(() => expect(screen.getByTestId('location').textContent).toBe('/')) - expect(await screen.findByRole('link', { name: '登录 / 注册' })).toBeTruthy() + expect(await screen.findByRole('link', { name: '登录' })).toBeTruthy() }) it('没有昵称时使用邮箱展示账号身份', async () => { diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index 9660b4cd..6130c3d4 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -190,10 +190,10 @@ export function AppHeader() { ) : session.state.status === 'guest' ? ( - 登录 / 注册 + 登录 登录 ) : ( diff --git a/frontend/src/features/account-panel/account-panel.css b/frontend/src/features/account-panel/account-panel.css index d4a5c4bd..bd019552 100644 --- a/frontend/src/features/account-panel/account-panel.css +++ b/frontend/src/features/account-panel/account-panel.css @@ -99,7 +99,7 @@ .auth-screen-tab:focus-visible, .auth-screen-code-action:focus-visible, .auth-screen-submit:focus-visible, -.auth-screen-entry-switch button:focus-visible, +.auth-screen-entry-switch a:focus-visible, .auth-password-field button:focus-visible, .auth-register-back:focus-visible { outline: 2px solid #1b1b19; @@ -514,7 +514,7 @@ transform: rotate(-18deg); } -.auth-screen-entry-switch button { +.auth-screen-entry-switch a { flex-shrink: 0; color: #34332e; font-weight: 500; @@ -573,7 +573,7 @@ font-size: 0.875rem; } -.auth-screen-entry-switch button:hover { +.auth-screen-entry-switch a:hover { color: #000; text-underline-offset: 0.3em; transform: translateY(-1px); @@ -965,7 +965,7 @@ .auth-screen-code-action:active:not(:disabled), .auth-screen-submit:hover:not(:disabled), .auth-screen-submit:active:not(:disabled), - .auth-screen-entry-switch button:hover { + .auth-screen-entry-switch a:hover { animation: none; transform: none; } @@ -982,7 +982,7 @@ .auth-register-field-shell, .auth-register-field-shell > svg, .auth-password-field button, - .auth-screen-entry-switch button { + .auth-screen-entry-switch a { transition: none; } } diff --git a/frontend/src/features/account-panel/index.test.tsx b/frontend/src/features/account-panel/index.test.tsx index bf83965e..a8bb2ef4 100644 --- a/frontend/src/features/account-panel/index.test.tsx +++ b/frontend/src/features/account-panel/index.test.tsx @@ -94,29 +94,26 @@ describe('AccountPanel', () => { expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() expect(screen.getByRole('heading', { name: '欢迎回来。' })).toBeTruthy() - expect(screen.getByText('未注册的邮箱将在验证后自动创建账号。')).toBeTruthy() + expect(screen.queryByText('未注册的邮箱将在验证后自动创建账号。')).toBeNull() + expect(screen.getByText('内测期间仅支持已有账号登录。')).toBeTruthy() expect(screen.queryByRole('tab', { name: '注册' })).toBeNull() - expect(screen.getByRole('button', { name: '创建账号' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() + expect(screen.getByRole('link', { name: 'GitHub Issues' }).getAttribute('href')).toBe( + 'https://github.com/1024XEngineer/Windup/issues', + ) await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) - it('opens registration as a centered, progressive form', async () => { - renderPanel('/?account=register&returnTo=%2Fworkspace') - - const dialog = screen.getByRole('dialog', { name: '创建 Windup 账号' }) - expect(screen.getByRole('heading', { name: '欢迎来到 Windup' })).toBeTruthy() - expect(dialog.className).toContain('max-w-[34rem]') - expect(screen.getByText('从一个角色开始,')).toBeTruthy() - expect(screen.queryByText('继续搭建,')).toBeNull() - expect(screen.getByTestId('register-fields').className).toContain('auth-register-fields') - expect(screen.queryByRole('tablist', { name: '账号操作' })).toBeNull() - expect(screen.getByLabelText('邮箱')).toBeTruthy() - expect(screen.queryByLabelText('密码')).toBeNull() - expect(screen.queryByLabelText('昵称(选填)')).toBeNull() - expect(screen.queryByLabelText('验证码')).toBeNull() - expect(screen.getByTestId('register-fields').querySelector('[aria-live]')).toBeNull() - expect(screen.getByRole('button', { name: '继续' })).toBeTruthy() + it('opens a closed-registration URL as login and never starts signup', async () => { + const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') + + expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '欢迎回来。' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() + expect(screen.queryByRole('button', { name: '继续' })).toBeNull() expect(screen.getByRole('button', { name: '登录' })).toBeTruthy() + expect(screen.getByText(/内测期间暂不开放注册/)).toBeTruthy() + expect(apis.register).not.toHaveBeenCalled() await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) @@ -141,15 +138,15 @@ describe('AccountPanel', () => { const dialog = screen.getByRole('dialog', { name: '登录 Windup' }) const closeButton = screen.getByRole('button', { name: '关闭账号面板' }) - const entrySwitch = screen.getByRole('button', { name: '创建账号' }) + const requestAccess = screen.getByRole('link', { name: 'GitHub Issues' }) - entrySwitch.focus() - fireEvent.keyDown(entrySwitch, { key: 'Tab' }) + requestAccess.focus() + fireEvent.keyDown(requestAccess, { key: 'Tab' }) expect(document.activeElement).toBe(closeButton) closeButton.focus() fireEvent.keyDown(closeButton, { key: 'Tab', shiftKey: true }) - expect(document.activeElement).toBe(entrySwitch) + expect(document.activeElement).toBe(requestAccess) expect(dialog.contains(document.activeElement)).toBe(true) }) @@ -266,9 +263,7 @@ describe('AccountPanel', () => { email: 'reader@example.com', code: '123456', }) - expect( - screen.getByText('登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。').textContent, - ).toContain('登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。') + expect(screen.getByText('登录成功,正在继续。').textContent).toContain('登录成功,正在继续。') await act(async () => vi.advanceTimersByTimeAsync(900)) expect(screen.getByTestId('location').textContent).toBe('/projects?view=recent') @@ -348,85 +343,16 @@ describe('AccountPanel', () => { expect(apis.sendCode).not.toHaveBeenCalled() }) - it('preserves registration input when showing a password and returning a step', async () => { - renderPanel('/?account=register') - fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - - const password = await screen.findByLabelText('密码') - expect(password.getAttribute('type')).toBe('password') - fireEvent.click(screen.getByRole('button', { name: '显示密码' })) - expect(password.getAttribute('type')).toBe('text') - - fireEvent.click(screen.getByRole('button', { name: '返回上一步' })) - expect(((await screen.findByLabelText('邮箱')) as HTMLInputElement).value).toBe( - 'new@example.com', - ) - expect(screen.getByTestId('auth-motion-stage').dataset.motionDirection).toBe('backward') - }) - - it('switches account entry only after the current panel exits', async () => { + it('does not expose a signup switch from the login panel', async () => { vi.useFakeTimers() renderPanel('/?account=login&returnTo=%2Fworkspace') - fireEvent.click(screen.getByRole('button', { name: '创建账号' })) - expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '创建账号' })).toBeNull() expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') await act(async () => vi.advanceTimersByTimeAsync(520)) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByTestId('location').textContent).toBe( - '/?account=register&returnTo=%2Fworkspace', - ) - }) - - it('validates each registration step and reuses the existing register API contract', async () => { - const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') - fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) - - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '为账号加一道保护' })).toBeTruthy() - expect(await screen.findByLabelText('密码')).toBeTruthy() - fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'short' } }) - - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect((await screen.findByRole('alert')).textContent).toContain('密码需为 8–128 位') - expect(apis.register).not.toHaveBeenCalled() - - fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password-123' } }) - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '留下你的称呼' })).toBeTruthy() - expect(await screen.findByLabelText('昵称(选填)')).toBeTruthy() - fireEvent.change(screen.getByLabelText('昵称(选填)'), { - target: { value: 'N'.repeat(51) }, - }) - - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - expect((await screen.findByRole('alert')).textContent).toContain('昵称不能超过 50 个字符') - - fireEvent.change(screen.getByLabelText('昵称(选填)'), { target: { value: '新用户' } }) - fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) - await waitFor(() => - expect(apis.sendCode).toHaveBeenCalledWith({ - email: 'new@example.com', - purpose: 'register', - }), - ) - expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() - expect(screen.getByRole('heading', { name: '确认你的邮箱' })).toBeTruthy() - expect(await screen.findByLabelText('验证码')).toBeTruthy() - fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' } }) - fireEvent.submit(screen.getByRole('button', { name: '创建账号' }).closest('form')!) - await waitFor(() => - expect(apis.register).toHaveBeenCalledWith({ - email: 'new@example.com', - password: 'password-123', - code: '123456', - nickname: '新用户', - }), - ) + expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') }) }) diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 212b7803..1084a8f9 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -91,6 +91,7 @@ const loginMotionCopy = [ const REGISTER_STEP_COUNT = 4 const AUTH_ICON_PROPS = { weight: 'light' as const } const AUTH_FIELD_CLASS = 'auth-screen-field w-full outline-none disabled:cursor-not-allowed' +const ACCESS_REQUEST_URL = 'https://github.com/1024XEngineer/Windup/issues' function errorMessage(error: unknown): string { return error instanceof Error && error.message ? error.message : '操作失败,请稍后重试' @@ -230,7 +231,7 @@ export function AccountPanel() { const entry = searchParams.get('account') if (entry !== 'login' && entry !== 'register') return null - return + return } /** 只有面板真正打开时才读取会话,关闭状态不把认证 Context 强加给应用外壳。 */ @@ -374,14 +375,6 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { window.requestAnimationFrame(() => emailInputRef.current?.focus()) } - function switchEntry(nextEntry: AccountEntry) { - leaveWithAnimation(() => { - const next = new URLSearchParams(searchParams) - next.set('account', nextEntry) - setSearchParams(next, { replace: true }) - }) - } - async function sendCode(): Promise { if (isSendingCode || cooldownSeconds > 0) return false if (!EMAIL_PATTERN.test(normalizedEmail)) { @@ -488,7 +481,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { successMessage = '账号已创建,正在继续。' } else if (mode === 'code') { await session.loginByCode({ email: normalizedEmail, code }) - successMessage = '登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。' + successMessage = '登录成功,正在继续。' } else { await session.login({ email: normalizedEmail, password }) successMessage = '登录成功,正在继续。' @@ -518,7 +511,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { function trapFocus(event: ReactKeyboardEvent) { if (event.key !== 'Tab') return const focusable = dialogRef.current?.querySelectorAll( - 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])', + 'button:not([disabled]), input:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])', ) if (!focusable?.length) return const first = focusable[0] @@ -779,9 +772,7 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { )} {!isRegister && mode === 'code' && ( -

- 未注册的邮箱将在验证后自动创建账号。 -

+

内测期间仅支持已有账号登录。

)} @@ -807,10 +798,11 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) {

- {isRegister ? '已有账号?' : '还没有账号?'}{' '} - + 内测期间暂不开放注册。如需开通,请通过{' '} + + GitHub Issues + {' '} + 联系团队申请。

diff --git a/frontend/src/pages/landing/index.test.tsx b/frontend/src/pages/landing/index.test.tsx index 3190cb52..81337cde 100644 --- a/frontend/src/pages/landing/index.test.tsx +++ b/frontend/src/pages/landing/index.test.tsx @@ -56,9 +56,11 @@ describe('LandingPage', () => { expect(screen.getByRole('link', { name: '登录' }).getAttribute('href')).toBe( '/?account=login&returnTo=%2Fworkspace', ) - expect(screen.getByRole('link', { name: '注册' }).getAttribute('href')).toBe( - '/?account=register&returnTo=%2Fworkspace', - ) + expect((screen.getByRole('button', { name: '注册' }) as HTMLButtonElement).disabled).toBe(true) + expect(screen.queryByRole('link', { name: '注册' })).toBeNull() + expect( + screen.getByRole('link', { name: '内测暂不开放,联系团队申请' }).getAttribute('href'), + ).toBe('https://github.com/1024XEngineer/Windup/issues') // Header 只处理账号入口,Hero 与收尾负责把用户带进创作。 const creationLinks = screen.getAllByRole('link', { name: '开始创作' }) expect(creationLinks).toHaveLength(2) diff --git a/frontend/src/pages/landing/marketing-header.tsx b/frontend/src/pages/landing/marketing-header.tsx index 755aae9e..1c34ed5f 100644 --- a/frontend/src/pages/landing/marketing-header.tsx +++ b/frontend/src/pages/landing/marketing-header.tsx @@ -7,10 +7,7 @@ const loginEntry = `/?${new URLSearchParams({ returnTo: '/workspace', })}` -const registerEntry = `/?${new URLSearchParams({ - account: 'register', - returnTo: '/workspace', -})}` +const ACCESS_REQUEST_URL = 'https://github.com/1024XEngineer/Windup/issues' const sections = [ ['#capabilities', '产品能力'], @@ -77,12 +74,24 @@ export function MarketingHeader() { > 登录 - - 注册 - + + + + 内测暂不开放,联系团队申请 + + )} From 3b37fdde1577f393fca11a43c8318674808f8661 Mon Sep 17 00:00:00 2001 From: xiaocheny214 <187097481+xiaocheny214@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:51:45 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(auth):=20=E4=BF=9D=E7=95=99=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E5=AE=9E=E7=8E=B0=EF=BC=8C=E5=8F=AA=E6=B3=A8=E8=A7=A3?= =?UTF-8?q?=E5=AF=B9=E5=A4=96=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 内测仍关闭公开注册,但注册表单、切换逻辑和测试留在仓库里,方便以后重新开放。 --- frontend/src/app/layout/app-header.tsx | 1 + .../features/account-panel/account-panel.css | 13 ++- .../src/features/account-panel/index.test.tsx | 102 ++++++++++++++++++ frontend/src/features/account-panel/index.tsx | 27 ++++- frontend/src/pages/landing/index.test.tsx | 4 + .../src/pages/landing/marketing-header.tsx | 15 +++ 6 files changed, 157 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index 6130c3d4..d18a1967 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -193,6 +193,7 @@ export function AppHeader() { aria-label="登录" className="inline-flex min-h-10 items-center rounded-lg border border-app-ink/14 bg-app-surface-raised/45 px-3 text-[13px] font-medium whitespace-nowrap text-app-ink-soft transition-colors hover:bg-app-surface-raised/75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-app-accent" > + {/* 内测关闭公开注册。重新开放时改回「登录 / 注册」。 */} 登录 登录 diff --git a/frontend/src/features/account-panel/account-panel.css b/frontend/src/features/account-panel/account-panel.css index bd019552..bdc337eb 100644 --- a/frontend/src/features/account-panel/account-panel.css +++ b/frontend/src/features/account-panel/account-panel.css @@ -100,6 +100,7 @@ .auth-screen-code-action:focus-visible, .auth-screen-submit:focus-visible, .auth-screen-entry-switch a:focus-visible, +.auth-screen-entry-switch button:focus-visible, .auth-password-field button:focus-visible, .auth-register-back:focus-visible { outline: 2px solid #1b1b19; @@ -514,7 +515,8 @@ transform: rotate(-18deg); } -.auth-screen-entry-switch a { +.auth-screen-entry-switch a, +.auth-screen-entry-switch button { flex-shrink: 0; color: #34332e; font-weight: 500; @@ -573,7 +575,8 @@ font-size: 0.875rem; } -.auth-screen-entry-switch a:hover { +.auth-screen-entry-switch a:hover, +.auth-screen-entry-switch button:hover { color: #000; text-underline-offset: 0.3em; transform: translateY(-1px); @@ -965,7 +968,8 @@ .auth-screen-code-action:active:not(:disabled), .auth-screen-submit:hover:not(:disabled), .auth-screen-submit:active:not(:disabled), - .auth-screen-entry-switch a:hover { + .auth-screen-entry-switch a:hover, + .auth-screen-entry-switch button:hover { animation: none; transform: none; } @@ -982,7 +986,8 @@ .auth-register-field-shell, .auth-register-field-shell > svg, .auth-password-field button, - .auth-screen-entry-switch a { + .auth-screen-entry-switch a, + .auth-screen-entry-switch button { transition: none; } } diff --git a/frontend/src/features/account-panel/index.test.tsx b/frontend/src/features/account-panel/index.test.tsx index a8bb2ef4..ddbe36ad 100644 --- a/frontend/src/features/account-panel/index.test.tsx +++ b/frontend/src/features/account-panel/index.test.tsx @@ -117,6 +117,27 @@ describe('AccountPanel', () => { await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) }) + // 内测关闭公开注册。重新开放时取消 skip,并恢复 AccountPanel 的 register 入口。 + it.skip('opens registration as a centered, progressive form', async () => { + renderPanel('/?account=register&returnTo=%2Fworkspace') + + const dialog = screen.getByRole('dialog', { name: '创建 Windup 账号' }) + expect(screen.getByRole('heading', { name: '欢迎来到 Windup' })).toBeTruthy() + expect(dialog.className).toContain('max-w-[34rem]') + expect(screen.getByText('从一个角色开始,')).toBeTruthy() + expect(screen.queryByText('继续搭建,')).toBeNull() + expect(screen.getByTestId('register-fields').className).toContain('auth-register-fields') + expect(screen.queryByRole('tablist', { name: '账号操作' })).toBeNull() + expect(screen.getByLabelText('邮箱')).toBeTruthy() + expect(screen.queryByLabelText('密码')).toBeNull() + expect(screen.queryByLabelText('昵称(选填)')).toBeNull() + expect(screen.queryByLabelText('验证码')).toBeNull() + expect(screen.getByTestId('register-fields').querySelector('[aria-live]')).toBeNull() + expect(screen.getByRole('button', { name: '继续' })).toBeTruthy() + expect(screen.getByRole('button', { name: '登录' })).toBeTruthy() + await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('邮箱'))) + }) + it('keeps the close control inside the dialog focus boundary', () => { renderPanel('/?account=login') @@ -354,6 +375,87 @@ describe('AccountPanel', () => { expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') }) + + it.skip('preserves registration input when showing a password and returning a step', async () => { + renderPanel('/?account=register') + fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + + const password = await screen.findByLabelText('密码') + expect(password.getAttribute('type')).toBe('password') + fireEvent.click(screen.getByRole('button', { name: '显示密码' })) + expect(password.getAttribute('type')).toBe('text') + + fireEvent.click(screen.getByRole('button', { name: '返回上一步' })) + expect(((await screen.findByLabelText('邮箱')) as HTMLInputElement).value).toBe( + 'new@example.com', + ) + expect(screen.getByTestId('auth-motion-stage').dataset.motionDirection).toBe('backward') + }) + + it.skip('switches account entry only after the current panel exits', async () => { + vi.useFakeTimers() + renderPanel('/?account=login&returnTo=%2Fworkspace') + + fireEvent.click(screen.getByRole('button', { name: '创建账号' })) + expect(screen.getByRole('dialog', { name: '登录 Windup' })).toBeTruthy() + expect(screen.getByTestId('location').textContent).toBe('/?account=login&returnTo=%2Fworkspace') + + await act(async () => vi.advanceTimersByTimeAsync(520)) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByTestId('location').textContent).toBe( + '/?account=register&returnTo=%2Fworkspace', + ) + }) + + it.skip('validates each registration step and reuses the existing register API contract', async () => { + const { apis } = renderPanel('/?account=register&returnTo=%2Fworkspace') + fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'new@example.com' } }) + + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '为账号加一道保护' })).toBeTruthy() + expect(await screen.findByLabelText('密码')).toBeTruthy() + fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'short' } }) + + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect((await screen.findByRole('alert')).textContent).toContain('密码需为 8–128 位') + expect(apis.register).not.toHaveBeenCalled() + + fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password-123' } }) + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '留下你的称呼' })).toBeTruthy() + expect(await screen.findByLabelText('昵称(选填)')).toBeTruthy() + fireEvent.change(screen.getByLabelText('昵称(选填)'), { + target: { value: 'N'.repeat(51) }, + }) + + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + expect((await screen.findByRole('alert')).textContent).toContain('昵称不能超过 50 个字符') + + fireEvent.change(screen.getByLabelText('昵称(选填)'), { target: { value: '新用户' } }) + fireEvent.submit(screen.getByRole('button', { name: '继续' }).closest('form')!) + await waitFor(() => + expect(apis.sendCode).toHaveBeenCalledWith({ + email: 'new@example.com', + purpose: 'register', + }), + ) + expect(screen.getByRole('dialog', { name: '创建 Windup 账号' })).toBeTruthy() + expect(screen.getByRole('heading', { name: '确认你的邮箱' })).toBeTruthy() + expect(await screen.findByLabelText('验证码')).toBeTruthy() + fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' } }) + fireEvent.submit(screen.getByRole('button', { name: '创建账号' }).closest('form')!) + await waitFor(() => + expect(apis.register).toHaveBeenCalledWith({ + email: 'new@example.com', + password: 'password-123', + code: '123456', + nickname: '新用户', + }), + ) + }) }) describe('AppShell account panel host', () => { diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 1084a8f9..752d1d3c 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -231,6 +231,8 @@ export function AccountPanel() { const entry = searchParams.get('account') if (entry !== 'login' && entry !== 'register') return null + // 内测期间关闭公开注册。重新开放时改回: + // return return } @@ -375,6 +377,16 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { window.requestAnimationFrame(() => emailInputRef.current?.focus()) } + /* + function switchEntry(nextEntry: AccountEntry) { + leaveWithAnimation(() => { + const next = new URLSearchParams(searchParams) + next.set('account', nextEntry) + setSearchParams(next, { replace: true }) + }) + } + */ + async function sendCode(): Promise { if (isSendingCode || cooldownSeconds > 0) return false if (!EMAIL_PATTERN.test(normalizedEmail)) { @@ -481,6 +493,8 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { successMessage = '账号已创建,正在继续。' } else if (mode === 'code') { await session.loginByCode({ email: normalizedEmail, code }) + // 内测不自动建号。重新开放注册时改回: + // successMessage = '登录成功。如果这是你首次使用该邮箱,我们已为你创建账号。' successMessage = '登录成功,正在继续。' } else { await session.login({ email: normalizedEmail, password }) @@ -772,7 +786,10 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { )} {!isRegister && mode === 'code' && ( -

内测期间仅支持已有账号登录。

+

+ {/* 未注册的邮箱将在验证后自动创建账号。 */} + 内测期间仅支持已有账号登录。 +

)} @@ -797,6 +814,14 @@ function AccountPanelDialog({ entry }: { entry: AccountEntry }) { + {/* +

+ {isRegister ? '已有账号?' : '还没有账号?'}{' '} + +

+ */}

内测期间暂不开放注册。如需开通,请通过{' '} diff --git a/frontend/src/pages/landing/index.test.tsx b/frontend/src/pages/landing/index.test.tsx index 81337cde..7ec45855 100644 --- a/frontend/src/pages/landing/index.test.tsx +++ b/frontend/src/pages/landing/index.test.tsx @@ -61,6 +61,10 @@ describe('LandingPage', () => { expect( screen.getByRole('link', { name: '内测暂不开放,联系团队申请' }).getAttribute('href'), ).toBe('https://github.com/1024XEngineer/Windup/issues') + // 重新开放注册时恢复: + // expect(screen.getByRole('link', { name: '注册' }).getAttribute('href')).toBe( + // '/?account=register&returnTo=%2Fworkspace', + // ) // Header 只处理账号入口,Hero 与收尾负责把用户带进创作。 const creationLinks = screen.getAllByRole('link', { name: '开始创作' }) expect(creationLinks).toHaveLength(2) diff --git a/frontend/src/pages/landing/marketing-header.tsx b/frontend/src/pages/landing/marketing-header.tsx index 1c34ed5f..b14c0196 100644 --- a/frontend/src/pages/landing/marketing-header.tsx +++ b/frontend/src/pages/landing/marketing-header.tsx @@ -7,7 +7,14 @@ const loginEntry = `/?${new URLSearchParams({ returnTo: '/workspace', })}` +// 内测关闭公开注册。重新开放时恢复 registerEntry 与下方注册链接。 const ACCESS_REQUEST_URL = 'https://github.com/1024XEngineer/Windup/issues' +/* +const registerEntry = `/?${new URLSearchParams({ + account: 'register', + returnTo: '/workspace', +})}` +*/ const sections = [ ['#capabilities', '产品能力'], @@ -74,6 +81,14 @@ export function MarketingHeader() { > 登录 + {/* + + 注册 + + */}