feat: implement account email mismatch error handling and improve user feedback in authentication flows

This commit is contained in:
Junyan Qin
2026-01-01 17:01:32 +08:00
parent 61f08f3218
commit 02e12cc1e4
10 changed files with 60 additions and 19 deletions
@@ -4,6 +4,7 @@ import asyncio
import traceback import traceback
from .. import group from .. import group
from .....entity.errors import account as account_errors
@group.group_class('user', '/api/v1/user') @group.group_class('user', '/api/v1/user')
@@ -141,6 +142,8 @@ class UserRouterGroup(group.RouterGroup):
'user': user_obj.user, 'user': user_obj.user,
} }
) )
except account_errors.AccountEmailMismatchError as e:
return self.fail(3, str(e))
except ValueError as e: except ValueError as e:
traceback.print_exc() traceback.print_exc()
return self.fail(1, str(e)) return self.fail(1, str(e))
+2 -3
View File
@@ -10,6 +10,7 @@ import asyncio
from ....core import app from ....core import app
from ....entity.persistence import user from ....entity.persistence import user
from ....utils import constants from ....utils import constants
from ....entity.errors import account as account_errors
class UserService: class UserService:
@@ -174,9 +175,7 @@ class UserService:
# Check if system is already initialized # Check if system is already initialized
is_initialized = await self.is_initialized() is_initialized = await self.is_initialized()
if is_initialized: if is_initialized:
raise ValueError( raise account_errors.AccountEmailMismatchError()
'This LangBot instance already has an account. Please use the existing account to login.'
)
# Create new Space user (first time initialization) # Create new Space user (first time initialization)
await self.ap.persistence_mgr.execute_async( await self.ap.persistence_mgr.execute_async(
+6
View File
@@ -0,0 +1,6 @@
from __future__ import annotations
class AccountEmailMismatchError(Exception):
def __str__(self):
return 'Account email mismatch'
+15 -5
View File
@@ -48,9 +48,15 @@ function SpaceOAuthCallbackContent() {
setTimeout(() => { setTimeout(() => {
router.push('/home'); router.push('/home');
}, 1000); }, 1000);
} catch { } catch (err) {
setStatus('error'); 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], [router, t],
@@ -74,9 +80,13 @@ function SpaceOAuthCallbackContent() {
}, 1000); }, 1000);
} catch (err) { } catch (err) {
setStatus('error'); setStatus('error');
setErrorMessage( const errorObj = err as { msg?: string };
err instanceof Error ? err.message : t('account.bindSpaceFailed'), const errMsg = (errorObj?.msg || '').toLowerCase();
); if (errMsg.includes('account email mismatch')) {
setErrorMessage(t('account.spaceEmailMismatch'));
} else {
setErrorMessage(t('account.bindSpaceFailed'));
}
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
+23 -4
View File
@@ -753,7 +753,7 @@ export class BackendClient extends BaseHttpClient {
}); });
} }
public bindSpaceAccount( public async bindSpaceAccount(
code: string, code: string,
state: string, state: string,
): Promise<{ ): Promise<{
@@ -761,7 +761,17 @@ export class BackendClient extends BaseHttpClient {
user: string; user: string;
account_type: 'local' | 'space'; 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) ============ // ============ Space OAuth API (Redirect Flow) ============
@@ -778,10 +788,19 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/user/space/authorize-url', params); return this.get('/api/v1/user/space/authorize-url', params);
} }
public exchangeSpaceOAuthCode(code: string): Promise<{ public async exchangeSpaceOAuthCode(code: string): Promise<{
token: string; token: string;
user: 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;
} }
} }
+5 -7
View File
@@ -93,9 +93,7 @@ export abstract class BaseHttpClient {
// 统一错误处理 // 统一错误处理
if (error.response) { if (error.response) {
const { status, data } = error.response; const { status, data } = error.response;
// Backend uses 'msg' field, also check 'message' for compatibility const errMsg = (data as { msg?: string })?.msg || error.message;
const errMessage =
(data as { msg?: string })?.msg || data?.message || error.message;
switch (status) { switch (status) {
case 401: case 401:
@@ -107,23 +105,23 @@ export abstract class BaseHttpClient {
} }
break; break;
case 403: case 403:
console.error('Permission denied:', errMessage); console.error('Permission denied:', errMsg);
break; break;
case 500: case 500:
console.error('Server error:', errMessage); console.error('Server error:', errMsg);
break; break;
} }
return Promise.reject({ return Promise.reject({
code: data?.code || status, code: data?.code || status,
message: errMessage, msg: errMsg,
data: data?.data || null, data: data?.data || null,
}); });
} }
return Promise.reject({ return Promise.reject({
code: -1, code: -1,
message: error.message || 'Network Error', msg: error.message || 'Network Error',
data: null, data: null,
}); });
}, },
+2
View File
@@ -789,6 +789,8 @@ const enUS = {
bindSpaceInvalidState: bindSpaceInvalidState:
'Invalid bind request. Please try again from account settings.', 'Invalid bind request. Please try again from account settings.',
setPasswordHint: 'Set a password to login with email and password', setPasswordHint: 'Set a password to login with email and password',
spaceEmailMismatch:
'Space login email does not match the local account email',
}, },
}; };
+2
View File
@@ -798,6 +798,8 @@ const jaJP = {
'無効な連携リクエストです。アカウント設定から再度お試しください。', '無効な連携リクエストです。アカウント設定から再度お試しください。',
setPasswordHint: setPasswordHint:
'パスワードを設定するとメールとパスワードでログインできます', 'パスワードを設定するとメールとパスワードでログインできます',
spaceEmailMismatch:
'Spaceログインのメールアドレスがローカルアカウントのメールアドレスと一致しません',
}, },
}; };
+1
View File
@@ -753,6 +753,7 @@ const zhHans = {
bindSpaceFailed: '绑定 Space 账户失败', bindSpaceFailed: '绑定 Space 账户失败',
bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起', bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起',
setPasswordHint: '设置密码后可使用邮箱密码登录', setPasswordHint: '设置密码后可使用邮箱密码登录',
spaceEmailMismatch: 'Space登录账号邮箱与本实例账号邮箱不匹配',
}, },
}; };
+1
View File
@@ -750,6 +750,7 @@ const zhHant = {
bindSpaceFailed: '綁定 Space 帳戶失敗', bindSpaceFailed: '綁定 Space 帳戶失敗',
bindSpaceInvalidState: '無效的綁定請求,請從帳戶設定重新發起', bindSpaceInvalidState: '無效的綁定請求,請從帳戶設定重新發起',
setPasswordHint: '設定密碼後可使用電子郵件密碼登入', setPasswordHint: '設定密碼後可使用電子郵件密碼登入',
spaceEmailMismatch: 'Space登入帳號電子郵件與本實例帳號電子郵件不匹配',
}, },
}; };