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>;
// ===== Fast Login =====
export const FastLoginBodySchema = z.object({
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>;
......@@ -120,7 +121,8 @@ export const WxLoginBodySchema = z.object({
bd_vid: z.string().optional(),
msclkid: 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 const GetWXLoginQRResponseSchema = z.object({
......
import { z } from 'zod';
import { LanguageSchema } from '../../../../../common/i18n/type';
// ===== Update password by old password =====
export const UpdatePasswordByOldBodySchema = z
......@@ -56,7 +57,8 @@ export const UpdatePasswordByCodeBodySchema = z.object({
username: z.string().trim().min(1).meta({ description: '用户名' }),
code: z.string().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>;
Subproject commit cd209c713a0263acb08492738c2c4265d8dc37b9
Subproject commit 38be4a68fbc7eecc4ef86d421aba5898ec014201
......@@ -21,7 +21,7 @@ const I18nLngSelector = () => {
language: lng
});
}
await onChangeLngI18n(lng, { reloadOnChange: true });
await onChangeLngI18n(lng);
},
[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 { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router';
......@@ -44,7 +44,7 @@ const AccountContainer = ({
return router.pathname.split('/').pop() as TabEnum;
}, [router.pathname]);
const tabList = useRef([
const tabList = [
{
icon: 'support/user/userLight',
label: t('account:personal_information'),
......@@ -130,7 +130,7 @@ const AccountContainer = ({
label: t('account:logout'),
value: TabEnum.loginout
}
]);
];
const { openConfirm, ConfirmModal } = useConfirm({
content: t('account:confirm_logout')
......@@ -168,7 +168,7 @@ const AccountContainer = ({
mx={'auto'}
mt={2}
w={'100%'}
list={tabList.current}
list={tabList}
value={currentTab}
onChange={setCurrentTab}
/>
......@@ -185,7 +185,7 @@ const AccountContainer = ({
m={'auto'}
w={'100%'}
size={isPc ? 'md' : 'sm'}
list={tabList.current.map((item) => ({
list={tabList.map((item) => ({
value: item.value,
label: item.label
}))}
......
......@@ -10,6 +10,7 @@ import { useTranslation } from 'next-i18next';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { checkPasswordRule } from '@fastgpt/global/common/string/password';
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>;
......@@ -27,7 +28,7 @@ interface RegisterType {
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const { feConfigs } = useSystemStore();
const {
register,
......@@ -60,7 +61,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const loginResponse = await postFindPassword({
username,
code,
password
password,
language: i18n.language as LangEnum
});
await loginSuccess(loginResponse);
toast({
......@@ -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>) => {
......
......@@ -19,6 +19,7 @@ import {
} from '@/web/support/marketing/utils';
import PolicyTip from './PolicyTip';
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>;
......@@ -28,7 +29,7 @@ interface Props {
}
const WechatForm = ({ setPageType, loginSuccess }: Props) => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const { toast } = useToast();
const { data: wechatInfo } = useQuery(['getWXLoginQR'], getWXLoginQR, {
......@@ -41,7 +42,7 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => {
});
useQuery(
['getWXLoginResult', wechatInfo?.code],
['getWXLoginResult', wechatInfo?.code, i18n.language],
() =>
getWXLoginResult({
inviterId: getInviterId(),
......@@ -49,7 +50,8 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => {
bd_vid: getBdVId(),
msclkid: getMsclkid(),
fastgpt_sem: getFastGPTSem(),
sourceDomain: getSourceDomain()
sourceDomain: getSourceDomain(),
language: i18n.language as LangEnum
}),
{
refetchInterval: 3 * 1000,
......
......@@ -18,6 +18,7 @@ import {
} from '@/web/support/marketing/utils';
import { checkPasswordRule } from '@fastgpt/global/common/string/password';
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>;
......@@ -35,7 +36,7 @@ interface RegisterType {
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const { feConfigs } = useSystemStore();
const {
......@@ -61,7 +62,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
bd_vid: getBdVId(),
msclkid: getMsclkid(),
fastgpt_sem: getFastGPTSem(),
sourceDomain: getSourceDomain()
sourceDomain: getSourceDomain(),
language: i18n.language as LangEnum
});
await loginSuccess(loginResponse);
removeFastGPTSem();
......@@ -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>) => {
......
......@@ -20,12 +20,16 @@ import {
} from '@fastgpt/global/openapi/support/user/account/login/api';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { getClientIpFromRequest } from '@fastgpt/service/common/security/clientIp';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
async function handler(
req: ApiRequestProps<LoginByPasswordBodyType>,
res: ApiResponseType
): Promise<LoginSuccessResponseType> {
const { username, password, code, language } = LoginByPasswordBodySchema.parse(req.body);
const { username, password, code, language } = parseApiInput({
req,
bodySchema: LoginByPasswordBodySchema
}).body;
// Auth prelogin code
await authCode({
......
......@@ -11,6 +11,7 @@ import { useTranslation } from 'next-i18next';
import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
const FastLogin = ({
code,
......@@ -26,7 +27,7 @@ const FastLogin = ({
const { setUserInfo } = useUserStore();
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const resolveLoginRedirect = useLoginRedirectAfterLogin();
const loginSuccess = useCallback(
async (res: LoginSuccessResponseType) => {
......@@ -53,7 +54,8 @@ const FastLogin = ({
try {
const res = await postFastLogin({
code,
token
token,
language: i18n.language as LangEnum
});
if (!res) {
toast({
......@@ -75,7 +77,7 @@ const FastLogin = ({
}, 1000);
}
},
[loginSuccess, router, t, toast]
[i18n.language, loginSuccess, router, t, toast]
);
useEffect(() => {
......
......@@ -20,10 +20,10 @@ import {
} from '@/web/support/marketing/utils';
import { postAcceptInvitationLink } from '@/web/support/user/team/api';
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 type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useLoginRedirectAfterLogin } from '@/web/support/user/loginRedirect';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
let isOauthLogging = false;
......
......@@ -12,7 +12,10 @@ import type {
WxLoginBodyType,
GetWXLoginQRResponseType
} 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 { LangEnum } from '@fastgpt/global/common/i18n/type';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
......@@ -63,7 +66,9 @@ export const postRegister = ({
inviterId,
bd_vid,
msclkid,
fastgpt_sem
fastgpt_sem,
sourceDomain,
language
}: AccountRegisterBodyType) =>
POST<LoginSuccessResponseType>(`/proApi/support/user/account/register/emailAndPhone`, {
username,
......@@ -72,6 +77,8 @@ export const postRegister = ({
bd_vid,
msclkid,
fastgpt_sem,
sourceDomain,
language,
password: hashStr(password)
});
......@@ -79,15 +86,13 @@ export const postRegister = ({
export const postFindPassword = ({
username,
code,
password
}: {
username: string;
code: string;
password: string;
}) =>
password,
...props
}: UpdatePasswordByCodeBodyType) =>
POST<LoginSuccessResponseType>(`/proApi/support/user/account/password/updateByCode`, {
username,
code,
...props,
password: hashStr(password)
});
export const updatePasswordByOld = ({ oldPsw, newPsw }: UpdatePasswordByOldBodyType) =>
......
......@@ -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 { ClientTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { getTeamPlanStatus } from './team/api';
import { setLangToStorage, getLangMapping } from '@fastgpt/web/i18n/utils';
import { setCurrentAuthTmbId } from './currentAuthTmbId';
type State = {
......@@ -71,11 +70,6 @@ export const useUserStore = create<State>()(
set((state) => {
state.userInfo = user ? user : null;
state.isTeamAdmin = !!user?.team?.permission?.hasManagePer;
const lang = user?.language;
if (lang) {
const mappedLang = getLangMapping(lang);
setLangToStorage(mappedLang);
}
});
},
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