From 02e12cc1e4bb8681526d121a26d425fd6f03ed2d Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Thu, 1 Jan 2026 17:01:32 +0800 Subject: [PATCH] feat: implement account email mismatch error handling and improve user feedback in authentication flows --- .../pkg/api/http/controller/groups/user.py | 3 +++ src/langbot/pkg/api/http/service/user.py | 5 ++-- src/langbot/pkg/entity/errors/account.py | 6 +++++ web/src/app/auth/space/callback/page.tsx | 20 ++++++++++---- web/src/app/infra/http/BackendClient.ts | 27 ++++++++++++++++--- web/src/app/infra/http/BaseHttpClient.ts | 12 ++++----- web/src/i18n/locales/en-US.ts | 2 ++ web/src/i18n/locales/ja-JP.ts | 2 ++ web/src/i18n/locales/zh-Hans.ts | 1 + web/src/i18n/locales/zh-Hant.ts | 1 + 10 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 src/langbot/pkg/entity/errors/account.py diff --git a/src/langbot/pkg/api/http/controller/groups/user.py b/src/langbot/pkg/api/http/controller/groups/user.py index b23a6c32..ed5548f0 100644 --- a/src/langbot/pkg/api/http/controller/groups/user.py +++ b/src/langbot/pkg/api/http/controller/groups/user.py @@ -4,6 +4,7 @@ import asyncio import traceback from .. import group +from .....entity.errors import account as account_errors @group.group_class('user', '/api/v1/user') @@ -141,6 +142,8 @@ class UserRouterGroup(group.RouterGroup): 'user': user_obj.user, } ) + except account_errors.AccountEmailMismatchError as e: + return self.fail(3, str(e)) except ValueError as e: traceback.print_exc() return self.fail(1, str(e)) diff --git a/src/langbot/pkg/api/http/service/user.py b/src/langbot/pkg/api/http/service/user.py index d83ee0ff..9ac73fbe 100644 --- a/src/langbot/pkg/api/http/service/user.py +++ b/src/langbot/pkg/api/http/service/user.py @@ -10,6 +10,7 @@ import asyncio from ....core import app from ....entity.persistence import user from ....utils import constants +from ....entity.errors import account as account_errors class UserService: @@ -174,9 +175,7 @@ class UserService: # Check if system is already initialized is_initialized = await self.is_initialized() if is_initialized: - raise ValueError( - 'This LangBot instance already has an account. Please use the existing account to login.' - ) + raise account_errors.AccountEmailMismatchError() # Create new Space user (first time initialization) await self.ap.persistence_mgr.execute_async( diff --git a/src/langbot/pkg/entity/errors/account.py b/src/langbot/pkg/entity/errors/account.py new file mode 100644 index 00000000..edd5b41f --- /dev/null +++ b/src/langbot/pkg/entity/errors/account.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +class AccountEmailMismatchError(Exception): + def __str__(self): + return 'Account email mismatch' diff --git a/web/src/app/auth/space/callback/page.tsx b/web/src/app/auth/space/callback/page.tsx index bce384cf..f237f37e 100644 --- a/web/src/app/auth/space/callback/page.tsx +++ b/web/src/app/auth/space/callback/page.tsx @@ -48,9 +48,15 @@ function SpaceOAuthCallbackContent() { setTimeout(() => { router.push('/home'); }, 1000); - } catch { + } catch (err) { setStatus('error'); - setErrorMessage(t('common.spaceLoginFailed')); + const errorObj = err as { msg?: string }; + const errMsg = (errorObj?.msg || '').toLowerCase(); + if (errMsg.includes('account email mismatch')) { + setErrorMessage(t('account.spaceEmailMismatch')); + } else { + setErrorMessage(t('common.spaceLoginFailed')); + } } }, [router, t], @@ -74,9 +80,13 @@ function SpaceOAuthCallbackContent() { }, 1000); } catch (err) { setStatus('error'); - setErrorMessage( - err instanceof Error ? err.message : t('account.bindSpaceFailed'), - ); + const errorObj = err as { msg?: string }; + const errMsg = (errorObj?.msg || '').toLowerCase(); + if (errMsg.includes('account email mismatch')) { + setErrorMessage(t('account.spaceEmailMismatch')); + } else { + setErrorMessage(t('account.bindSpaceFailed')); + } } finally { setIsProcessing(false); } diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 3cfeb6cd..dd4b1feb 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -753,7 +753,7 @@ export class BackendClient extends BaseHttpClient { }); } - public bindSpaceAccount( + public async bindSpaceAccount( code: string, state: string, ): Promise<{ @@ -761,7 +761,17 @@ export class BackendClient extends BaseHttpClient { user: string; account_type: 'local' | 'space'; }> { - return this.post('/api/v1/user/bind-space', { code, state }); + const response = await this.instance.post('/api/v1/user/bind-space', { + code, + state, + }); + if (response.data.code !== 0) { + throw { + code: response.data.code, + msg: response.data.msg || 'Unknown error', + }; + } + return response.data.data; } // ============ Space OAuth API (Redirect Flow) ============ @@ -778,10 +788,19 @@ export class BackendClient extends BaseHttpClient { return this.get('/api/v1/user/space/authorize-url', params); } - public exchangeSpaceOAuthCode(code: string): Promise<{ + public async exchangeSpaceOAuthCode(code: string): Promise<{ token: string; user: string; }> { - return this.post('/api/v1/user/space/callback', { code }); + const response = await this.instance.post('/api/v1/user/space/callback', { + code, + }); + if (response.data.code !== 0) { + throw { + code: response.data.code, + msg: response.data.msg || 'Unknown error', + }; + } + return response.data.data; } } diff --git a/web/src/app/infra/http/BaseHttpClient.ts b/web/src/app/infra/http/BaseHttpClient.ts index fa4100d0..1198c647 100644 --- a/web/src/app/infra/http/BaseHttpClient.ts +++ b/web/src/app/infra/http/BaseHttpClient.ts @@ -93,9 +93,7 @@ export abstract class BaseHttpClient { // 统一错误处理 if (error.response) { const { status, data } = error.response; - // Backend uses 'msg' field, also check 'message' for compatibility - const errMessage = - (data as { msg?: string })?.msg || data?.message || error.message; + const errMsg = (data as { msg?: string })?.msg || error.message; switch (status) { case 401: @@ -107,23 +105,23 @@ export abstract class BaseHttpClient { } break; case 403: - console.error('Permission denied:', errMessage); + console.error('Permission denied:', errMsg); break; case 500: - console.error('Server error:', errMessage); + console.error('Server error:', errMsg); break; } return Promise.reject({ code: data?.code || status, - message: errMessage, + msg: errMsg, data: data?.data || null, }); } return Promise.reject({ code: -1, - message: error.message || 'Network Error', + msg: error.message || 'Network Error', data: null, }); }, diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 0fa3f01f..cd78389a 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -789,6 +789,8 @@ const enUS = { bindSpaceInvalidState: 'Invalid bind request. Please try again from account settings.', setPasswordHint: 'Set a password to login with email and password', + spaceEmailMismatch: + 'Space login email does not match the local account email', }, }; diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 4ed1e0fa..3b40ba48 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -798,6 +798,8 @@ const jaJP = { '無効な連携リクエストです。アカウント設定から再度お試しください。', setPasswordHint: 'パスワードを設定するとメールとパスワードでログインできます', + spaceEmailMismatch: + 'Spaceログインのメールアドレスがローカルアカウントのメールアドレスと一致しません', }, }; diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 46460a87..d6a6f029 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -753,6 +753,7 @@ const zhHans = { bindSpaceFailed: '绑定 Space 账户失败', bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起', setPasswordHint: '设置密码后可使用邮箱密码登录', + spaceEmailMismatch: 'Space登录账号邮箱与本实例账号邮箱不匹配', }, }; diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index b425e721..3b99f99f 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -750,6 +750,7 @@ const zhHant = { bindSpaceFailed: '綁定 Space 帳戶失敗', bindSpaceInvalidState: '無效的綁定請求,請從帳戶設定重新發起', setPasswordHint: '設定密碼後可使用電子郵件密碼登入', + spaceEmailMismatch: 'Space登入帳號電子郵件與本實例帳號電子郵件不匹配', }, };