Commit cba10ce2 by YeYuheng Committed by GitHub

Fix login language synchronization (#7101)

* fix: pass language through login flows

* fix: keep client language independent from user info

---------

Co-authored-by: archer <545436317@qq.com>
parent b4a5a76d
...@@ -109,7 +109,8 @@ export type OauthLoginBodyType = z.infer<typeof OauthLoginBodySchema>; ...@@ -109,7 +109,8 @@ export type OauthLoginBodyType = z.infer<typeof OauthLoginBodySchema>;
// ===== Fast Login ===== // ===== Fast Login =====
export const FastLoginBodySchema = z.object({ export const FastLoginBodySchema = z.object({
token: z.string().meta({ description: 'Token' }), token: z.string().meta({ description: 'Token' }),
code: z.string().meta({ description: 'Code' }) code: z.string().meta({ description: 'Code' }),
language: LanguageSchema.optional().meta({ description: '语言' })
}); });
export type FastLoginBodyType = z.infer<typeof FastLoginBodySchema>; export type FastLoginBodyType = z.infer<typeof FastLoginBodySchema>;
...@@ -120,7 +121,8 @@ export const WxLoginBodySchema = z.object({ ...@@ -120,7 +121,8 @@ export const WxLoginBodySchema = z.object({
bd_vid: z.string().optional(), bd_vid: z.string().optional(),
msclkid: z.string().optional(), msclkid: z.string().optional(),
fastgpt_sem: z.string().optional(), fastgpt_sem: z.string().optional(),
sourceDomain: z.string().optional() sourceDomain: z.string().optional(),
language: LanguageSchema.optional().meta({ description: '语言' })
}); });
export type WxLoginBodyType = z.infer<typeof WxLoginBodySchema>; export type WxLoginBodyType = z.infer<typeof WxLoginBodySchema>;
export const GetWXLoginQRResponseSchema = z.object({ export const GetWXLoginQRResponseSchema = z.object({
......
import { z } from 'zod'; import { z } from 'zod';
import { LanguageSchema } from '../../../../../common/i18n/type';
// ===== Update password by old password ===== // ===== Update password by old password =====
export const UpdatePasswordByOldBodySchema = z export const UpdatePasswordByOldBodySchema = z
...@@ -56,7 +57,8 @@ export const UpdatePasswordByCodeBodySchema = z.object({ ...@@ -56,7 +57,8 @@ export const UpdatePasswordByCodeBodySchema = z.object({
username: z.string().trim().min(1).meta({ description: '用户名' }), username: z.string().trim().min(1).meta({ description: '用户名' }),
code: z.string().meta({ description: '验证码' }), code: z.string().meta({ description: '验证码' }),
password: z.string().trim().min(1).meta({ description: '新密码' }), password: z.string().trim().min(1).meta({ description: '新密码' }),
tmbId: z.string().optional().meta({ description: '团队成员 ID(可选)' }) tmbId: z.string().optional().meta({ description: '团队成员 ID(可选)' }),
language: LanguageSchema.optional().meta({ description: '语言' })
}); });
export type UpdatePasswordByCodeBodyType = z.infer<typeof UpdatePasswordByCodeBodySchema>; export type UpdatePasswordByCodeBodyType = z.infer<typeof UpdatePasswordByCodeBodySchema>;
Subproject commit cd209c713a0263acb08492738c2c4265d8dc37b9 Subproject commit 38be4a68fbc7eecc4ef86d421aba5898ec014201
...@@ -21,7 +21,7 @@ const I18nLngSelector = () => { ...@@ -21,7 +21,7 @@ const I18nLngSelector = () => {
language: lng language: lng
}); });
} }
await onChangeLngI18n(lng, { reloadOnChange: true }); await onChangeLngI18n(lng);
}, },
[userInfo?.username, onChangeLngI18n, updateUserInfo] [userInfo?.username, onChangeLngI18n, updateUserInfo]
); );
......
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react'; import { Box, Flex, useTheme } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -44,7 +44,7 @@ const AccountContainer = ({ ...@@ -44,7 +44,7 @@ const AccountContainer = ({
return router.pathname.split('/').pop() as TabEnum; return router.pathname.split('/').pop() as TabEnum;
}, [router.pathname]); }, [router.pathname]);
const tabList = useRef([ const tabList = [
{ {
icon: 'support/user/userLight', icon: 'support/user/userLight',
label: t('account:personal_information'), label: t('account:personal_information'),
...@@ -130,7 +130,7 @@ const AccountContainer = ({ ...@@ -130,7 +130,7 @@ const AccountContainer = ({
label: t('account:logout'), label: t('account:logout'),
value: TabEnum.loginout value: TabEnum.loginout
} }
]); ];
const { openConfirm, ConfirmModal } = useConfirm({ const { openConfirm, ConfirmModal } = useConfirm({
content: t('account:confirm_logout') content: t('account:confirm_logout')
...@@ -168,7 +168,7 @@ const AccountContainer = ({ ...@@ -168,7 +168,7 @@ const AccountContainer = ({
mx={'auto'} mx={'auto'}
mt={2} mt={2}
w={'100%'} w={'100%'}
list={tabList.current} list={tabList}
value={currentTab} value={currentTab}
onChange={setCurrentTab} onChange={setCurrentTab}
/> />
...@@ -185,7 +185,7 @@ const AccountContainer = ({ ...@@ -185,7 +185,7 @@ const AccountContainer = ({
m={'auto'} m={'auto'}
w={'100%'} w={'100%'}
size={isPc ? 'md' : 'sm'} size={isPc ? 'md' : 'sm'}
list={tabList.current.map((item) => ({ list={tabList.map((item) => ({
value: item.value, value: item.value,
label: item.label label: item.label
}))} }))}
......
...@@ -10,6 +10,7 @@ import { useTranslation } from 'next-i18next'; ...@@ -10,6 +10,7 @@ import { useTranslation } from 'next-i18next';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { checkPasswordRule } from '@fastgpt/global/common/string/password'; import { checkPasswordRule } from '@fastgpt/global/common/string/password';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api'; import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
type LoginSuccessHandler = (res: LoginSuccessResponseType) => void | Promise<void>; type LoginSuccessHandler = (res: LoginSuccessResponseType) => void | Promise<void>;
...@@ -27,7 +28,7 @@ interface RegisterType { ...@@ -27,7 +28,7 @@ interface RegisterType {
const RegisterForm = ({ setPageType, loginSuccess }: Props) => { const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { const {
register, register,
...@@ -60,7 +61,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -60,7 +61,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const loginResponse = await postFindPassword({ const loginResponse = await postFindPassword({
username, username,
code, code,
password password,
language: i18n.language as LangEnum
}); });
await loginSuccess(loginResponse); await loginSuccess(loginResponse);
toast({ toast({
...@@ -69,7 +71,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -69,7 +71,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
}); });
}, },
{ {
refreshDeps: [loginSuccess, t, toast] refreshDeps: [i18n.language, loginSuccess, t, toast]
} }
); );
const onSubmitErr = (err: Record<string, any>) => { const onSubmitErr = (err: Record<string, any>) => {
......
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
} from '@/web/support/marketing/utils'; } from '@/web/support/marketing/utils';
import PolicyTip from './PolicyTip'; import PolicyTip from './PolicyTip';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api'; import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
type LoginSuccessHandler = (res: LoginSuccessResponseType) => void | Promise<void>; type LoginSuccessHandler = (res: LoginSuccessResponseType) => void | Promise<void>;
...@@ -28,7 +29,7 @@ interface Props { ...@@ -28,7 +29,7 @@ interface Props {
} }
const WechatForm = ({ setPageType, loginSuccess }: Props) => { const WechatForm = ({ setPageType, loginSuccess }: Props) => {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { data: wechatInfo } = useQuery(['getWXLoginQR'], getWXLoginQR, { const { data: wechatInfo } = useQuery(['getWXLoginQR'], getWXLoginQR, {
...@@ -41,7 +42,7 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -41,7 +42,7 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => {
}); });
useQuery( useQuery(
['getWXLoginResult', wechatInfo?.code], ['getWXLoginResult', wechatInfo?.code, i18n.language],
() => () =>
getWXLoginResult({ getWXLoginResult({
inviterId: getInviterId(), inviterId: getInviterId(),
...@@ -49,7 +50,8 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -49,7 +50,8 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => {
bd_vid: getBdVId(), bd_vid: getBdVId(),
msclkid: getMsclkid(), msclkid: getMsclkid(),
fastgpt_sem: getFastGPTSem(), fastgpt_sem: getFastGPTSem(),
sourceDomain: getSourceDomain() sourceDomain: getSourceDomain(),
language: i18n.language as LangEnum
}), }),
{ {
refetchInterval: 3 * 1000, refetchInterval: 3 * 1000,
......
...@@ -18,6 +18,7 @@ import { ...@@ -18,6 +18,7 @@ import {
} from '@/web/support/marketing/utils'; } from '@/web/support/marketing/utils';
import { checkPasswordRule } from '@fastgpt/global/common/string/password'; import { checkPasswordRule } from '@fastgpt/global/common/string/password';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api'; import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
type LoginSuccessHandler = (res: LoginSuccessResponseType) => void | Promise<void>; type LoginSuccessHandler = (res: LoginSuccessResponseType) => void | Promise<void>;
...@@ -35,7 +36,7 @@ interface RegisterType { ...@@ -35,7 +36,7 @@ interface RegisterType {
const RegisterForm = ({ setPageType, loginSuccess }: Props) => { const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { const {
...@@ -61,7 +62,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -61,7 +62,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
bd_vid: getBdVId(), bd_vid: getBdVId(),
msclkid: getMsclkid(), msclkid: getMsclkid(),
fastgpt_sem: getFastGPTSem(), fastgpt_sem: getFastGPTSem(),
sourceDomain: getSourceDomain() sourceDomain: getSourceDomain(),
language: i18n.language as LangEnum
}); });
await loginSuccess(loginResponse); await loginSuccess(loginResponse);
removeFastGPTSem(); removeFastGPTSem();
...@@ -72,7 +74,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { ...@@ -72,7 +74,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
}); });
}, },
{ {
refreshDeps: [loginSuccess, t, toast] refreshDeps: [i18n.language, loginSuccess, t, toast]
} }
); );
const onSubmitErr = (err: Record<string, any>) => { const onSubmitErr = (err: Record<string, any>) => {
......
...@@ -20,12 +20,16 @@ import { ...@@ -20,12 +20,16 @@ import {
} from '@fastgpt/global/openapi/support/user/account/login/api'; } from '@fastgpt/global/openapi/support/user/account/login/api';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { getClientIpFromRequest } from '@fastgpt/service/common/security/clientIp'; import { getClientIpFromRequest } from '@fastgpt/service/common/security/clientIp';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
async function handler( async function handler(
req: ApiRequestProps<LoginByPasswordBodyType>, req: ApiRequestProps<LoginByPasswordBodyType>,
res: ApiResponseType res: ApiResponseType
): Promise<LoginSuccessResponseType> { ): Promise<LoginSuccessResponseType> {
const { username, password, code, language } = LoginByPasswordBodySchema.parse(req.body); const { username, password, code, language } = parseApiInput({
req,
bodySchema: LoginByPasswordBodySchema
}).body;
// Auth prelogin code // Auth prelogin code
await authCode({ await authCode({
......
...@@ -11,6 +11,7 @@ import { useTranslation } from 'next-i18next'; ...@@ -11,6 +11,7 @@ import { useTranslation } from 'next-i18next';
import { validateRedirectUrl } from '@/web/common/utils/uri'; import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api'; import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect'; import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
const FastLogin = ({ const FastLogin = ({
code, code,
...@@ -26,7 +27,7 @@ const FastLogin = ({ ...@@ -26,7 +27,7 @@ const FastLogin = ({
const { setUserInfo } = useUserStore(); const { setUserInfo } = useUserStore();
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const resolveLoginRedirect = useLoginRedirectAfterLogin(); const resolveLoginRedirect = useLoginRedirectAfterLogin();
const loginSuccess = useCallback( const loginSuccess = useCallback(
async (res: LoginSuccessResponseType) => { async (res: LoginSuccessResponseType) => {
...@@ -53,7 +54,8 @@ const FastLogin = ({ ...@@ -53,7 +54,8 @@ const FastLogin = ({
try { try {
const res = await postFastLogin({ const res = await postFastLogin({
code, code,
token token,
language: i18n.language as LangEnum
}); });
if (!res) { if (!res) {
toast({ toast({
...@@ -75,7 +77,7 @@ const FastLogin = ({ ...@@ -75,7 +77,7 @@ const FastLogin = ({
}, 1000); }, 1000);
} }
}, },
[loginSuccess, router, t, toast] [i18n.language, loginSuccess, router, t, toast]
); );
useEffect(() => { useEffect(() => {
......
...@@ -20,10 +20,10 @@ import { ...@@ -20,10 +20,10 @@ import {
} from '@/web/support/marketing/utils'; } from '@/web/support/marketing/utils';
import { postAcceptInvitationLink } from '@/web/support/user/team/api'; import { postAcceptInvitationLink } from '@/web/support/user/team/api';
import { retryFn } from '@fastgpt/global/common/system/utils'; import { retryFn } from '@fastgpt/global/common/system/utils';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import { validateRedirectUrl } from '@/web/common/utils/uri'; import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api'; import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect'; import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
let isOauthLogging = false; let isOauthLogging = false;
......
...@@ -12,7 +12,10 @@ import type { ...@@ -12,7 +12,10 @@ import type {
WxLoginBodyType, WxLoginBodyType,
GetWXLoginQRResponseType GetWXLoginQRResponseType
} from '@fastgpt/global/openapi/support/user/account/login/api'; } from '@fastgpt/global/openapi/support/user/account/login/api';
import type { UpdatePasswordByOldBodyType } from '@fastgpt/global/openapi/support/user/account/password/api'; import type {
UpdatePasswordByCodeBodyType,
UpdatePasswordByOldBodyType
} from '@fastgpt/global/openapi/support/user/account/password/api';
import type { AccountRegisterBodyType } from '@fastgpt/global/openapi/support/user/account/register/api'; import type { AccountRegisterBodyType } from '@fastgpt/global/openapi/support/user/account/register/api';
import type { LangEnum } from '@fastgpt/global/common/i18n/type'; import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api'; import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
...@@ -63,7 +66,9 @@ export const postRegister = ({ ...@@ -63,7 +66,9 @@ export const postRegister = ({
inviterId, inviterId,
bd_vid, bd_vid,
msclkid, msclkid,
fastgpt_sem fastgpt_sem,
sourceDomain,
language
}: AccountRegisterBodyType) => }: AccountRegisterBodyType) =>
POST<LoginSuccessResponseType>(`/proApi/support/user/account/register/emailAndPhone`, { POST<LoginSuccessResponseType>(`/proApi/support/user/account/register/emailAndPhone`, {
username, username,
...@@ -72,6 +77,8 @@ export const postRegister = ({ ...@@ -72,6 +77,8 @@ export const postRegister = ({
bd_vid, bd_vid,
msclkid, msclkid,
fastgpt_sem, fastgpt_sem,
sourceDomain,
language,
password: hashStr(password) password: hashStr(password)
}); });
...@@ -79,15 +86,13 @@ export const postRegister = ({ ...@@ -79,15 +86,13 @@ export const postRegister = ({
export const postFindPassword = ({ export const postFindPassword = ({
username, username,
code, code,
password password,
}: { ...props
username: string; }: UpdatePasswordByCodeBodyType) =>
code: string;
password: string;
}) =>
POST<LoginSuccessResponseType>(`/proApi/support/user/account/password/updateByCode`, { POST<LoginSuccessResponseType>(`/proApi/support/user/account/password/updateByCode`, {
username, username,
code, code,
...props,
password: hashStr(password) password: hashStr(password)
}); });
export const updatePasswordByOld = ({ oldPsw, newPsw }: UpdatePasswordByOldBodyType) => export const updatePasswordByOld = ({ oldPsw, newPsw }: UpdatePasswordByOldBodyType) =>
......
...@@ -6,7 +6,6 @@ import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; ...@@ -6,7 +6,6 @@ import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import type { UserType } from '@fastgpt/global/support/user/type'; import type { UserType } from '@fastgpt/global/support/user/type';
import type { ClientTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type'; import type { ClientTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { getTeamPlanStatus } from './team/api'; import { getTeamPlanStatus } from './team/api';
import { setLangToStorage, getLangMapping } from '@fastgpt/web/i18n/utils';
import { setCurrentAuthTmbId } from './currentAuthTmbId'; import { setCurrentAuthTmbId } from './currentAuthTmbId';
type State = { type State = {
...@@ -71,11 +70,6 @@ export const useUserStore = create<State>()( ...@@ -71,11 +70,6 @@ export const useUserStore = create<State>()(
set((state) => { set((state) => {
state.userInfo = user ? user : null; state.userInfo = user ? user : null;
state.isTeamAdmin = !!user?.team?.permission?.hasManagePer; state.isTeamAdmin = !!user?.team?.permission?.hasManagePer;
const lang = user?.language;
if (lang) {
const mappedLang = getLangMapping(lang);
setLangToStorage(mappedLang);
}
}); });
}, },
async updateUserInfo(user: UserUpdateParams) { async updateUserInfo(user: UserUpdateParams) {
......
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { useUserStore as useUserStoreType } from '@/web/support/user/useUserStore';
import type { UserType } from '@fastgpt/global/support/user/type';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
const i18nMocks = vi.hoisted(() => ({
setLangToStorage: vi.fn(),
getLangMapping: vi.fn((lang: string) => lang)
}));
const localStorageMock = vi.hoisted(() => {
let store: Record<string, string> = {};
return {
getItem: vi.fn((key: string) => store[key] ?? null),
setItem: vi.fn((key: string, value: string) => {
store[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
key: vi.fn((index: number) => Object.keys(store)[index] ?? null),
get length() {
return Object.keys(store).length;
}
};
});
vi.mock('@fastgpt/web/i18n/utils', () => ({
setLangToStorage: i18nMocks.setLangToStorage,
getLangMapping: i18nMocks.getLangMapping
}));
let useUserStore: typeof useUserStoreType;
const buildUser = (language: UserType['language']): UserType =>
({
_id: 'user-id',
username: 'user@example.com',
avatar: '',
timezone: 'Asia/Shanghai',
language,
promotionRate: 0,
team: {
userId: 'user-id',
tmbId: 'tmb-id',
teamId: 'team-id',
teamName: 'Team',
memberName: 'Member',
avatar: '',
teamDomain: '',
role: 'owner',
status: 'active',
permission: new TeamPermission({ isOwner: true })
},
permission: new TeamPermission({ isOwner: true }),
tags: []
}) as UserType;
describe('useUserStore', () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
localStorageMock.clear();
vi.stubGlobal('localStorage', localStorageMock);
useUserStore = (await import('@/web/support/user/useUserStore')).useUserStore;
useUserStore.setState({
userInfo: null,
isTeamAdmin: false
});
});
it('does not persist backend language when setting user info', () => {
useUserStore.getState().setUserInfo(buildUser('en'));
expect(useUserStore.getState().userInfo?.language).toBe('en');
expect(i18nMocks.getLangMapping).not.toHaveBeenCalled();
expect(i18nMocks.setLangToStorage).not.toHaveBeenCalled();
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment