mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
fix(ui): prevent infinite getPhrase api call (#3160)
This commit is contained in:
parent
e3f88f5250
commit
03099debbb
7 changed files with 85 additions and 40 deletions
|
@ -163,4 +163,16 @@ describe('when the application is not admin-console', () => {
|
|||
expect(getPhrases).toBeCalledTimes(1);
|
||||
expect(getPhrases).toBeCalledWith(customizedLanguage, [customizedLanguage]);
|
||||
});
|
||||
|
||||
it('should call getPhrases with specific language is provided in params', async () => {
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: customizedLanguage,
|
||||
},
|
||||
});
|
||||
await expect(phraseRequest.get('/phrase?lng=fr')).resolves.toHaveProperty('status', 200);
|
||||
expect(getPhrases).toBeCalledWith('fr', [customizedLanguage]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
||||
import { adminConsoleApplicationId, adminConsoleSignInExperience } from '@logto/schemas';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import detectLanguage from '#src/i18n/detect-language.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
|
@ -24,26 +26,39 @@ export default function phraseRoutes<T extends AnonymousRouter>(
|
|||
return languageInfo;
|
||||
};
|
||||
|
||||
router.get('/phrase', async (ctx, next) => {
|
||||
const interaction = await provider
|
||||
.interactionDetails(ctx.req, ctx.res)
|
||||
// Should not block when failed to get interaction
|
||||
.catch(() => null);
|
||||
router.get(
|
||||
'/phrase',
|
||||
koaGuard({
|
||||
query: object({
|
||||
lng: string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const interaction = await provider
|
||||
.interactionDetails(ctx.req, ctx.res)
|
||||
// Should not block when failed to get interaction
|
||||
.catch(() => null);
|
||||
|
||||
const applicationId = interaction?.params.client_id;
|
||||
const { autoDetect, fallbackLanguage } = await getLanguageInfo(applicationId);
|
||||
const {
|
||||
query: { lng },
|
||||
} = ctx.guard;
|
||||
|
||||
const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
|
||||
const acceptableLanguages = [...detectedLanguages, fallbackLanguage];
|
||||
const customLanguages = await findAllCustomLanguageTags();
|
||||
const language =
|
||||
acceptableLanguages.find(
|
||||
(tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag)
|
||||
) ?? 'en';
|
||||
const applicationId = interaction?.params.client_id;
|
||||
const { autoDetect, fallbackLanguage } = await getLanguageInfo(applicationId);
|
||||
|
||||
ctx.set('Content-Language', language);
|
||||
ctx.body = await getPhrases(language, customLanguages);
|
||||
const targetLanguage = lng ? [lng] : [];
|
||||
const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
|
||||
const acceptableLanguages = [...targetLanguage, ...detectedLanguages, fallbackLanguage];
|
||||
const customLanguages = await findAllCustomLanguageTags();
|
||||
const language =
|
||||
acceptableLanguages.find(
|
||||
(tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag)
|
||||
) ?? 'en';
|
||||
|
||||
return next();
|
||||
});
|
||||
ctx.set('Content-Language', language);
|
||||
ctx.body = await getPhrases(language, customLanguages);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ const App = () => {
|
|||
customCssRef.current.textContent = settings.customCss;
|
||||
|
||||
// Note: i18n must be initialized ahead of page render
|
||||
await initI18n(settings.languageInfo);
|
||||
await initI18n();
|
||||
|
||||
// Init the page settings and render
|
||||
setExperienceSettings(settings);
|
||||
|
|
|
@ -11,17 +11,23 @@ export const getSignInExperience = async <T extends SignInExperienceResponse>():
|
|||
return ky.get('/api/.well-known/sign-in-exp').json<T>();
|
||||
};
|
||||
|
||||
export const getPhrases = async (lng?: string) =>
|
||||
export const getPhrases = async ({
|
||||
localLanguage,
|
||||
language,
|
||||
}: {
|
||||
localLanguage?: string;
|
||||
language?: string;
|
||||
}) =>
|
||||
ky
|
||||
.extend({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
(request) => {
|
||||
if (lng) {
|
||||
request.headers.set('Accept-Language', lng);
|
||||
if (localLanguage) {
|
||||
request.headers.set('Accept-Language', localLanguage);
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.get('/api/phrase');
|
||||
.get(`/api/phrase${language ? `?lng=${language}` : ''}`);
|
||||
|
|
|
@ -23,12 +23,12 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
|||
}
|
||||
|
||||
// Init i18n
|
||||
void initI18n();
|
||||
const i18nInit = initI18n();
|
||||
|
||||
// Block pointer event
|
||||
document.body.classList.add(conditionalString(styles.preview));
|
||||
|
||||
const previewMessageHandler = (event: MessageEvent) => {
|
||||
const previewMessageHandler = async (event: MessageEvent) => {
|
||||
// TODO: @simeng: we can check allowed origins via `/.well-known/endpoints`
|
||||
// if (event.origin !== window.location.origin) {
|
||||
// return;
|
||||
|
@ -36,6 +36,8 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
|||
|
||||
if (event.data.sender === 'ac_preview') {
|
||||
// #event.data should be guarded at the provider's side
|
||||
await i18nInit;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setPreviewConfig(event.data.config as PreviewConfig);
|
||||
}
|
||||
|
@ -78,12 +80,20 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
|||
|
||||
setPlatform(platform);
|
||||
|
||||
await changeLanguage(language);
|
||||
|
||||
setExperienceSettings(experienceSettings);
|
||||
})();
|
||||
}, [isPreview, previewConfig, setExperienceSettings, setPlatform, setTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPreview || !previewConfig?.language) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await changeLanguage(previewConfig.language);
|
||||
})();
|
||||
}, [previewConfig?.language, isPreview]);
|
||||
|
||||
return [isPreview, previewConfig];
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import type { LanguageInfo } from '@logto/schemas';
|
||||
import type { InitOptions } from 'i18next';
|
||||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import { getI18nResource, detectLanguage } from '@/i18n/utils';
|
||||
import { getI18nResource } from '@/i18n/utils';
|
||||
|
||||
const storageKey = 'i18nextLogtoUiLng';
|
||||
|
||||
const initI18n = async (languageSettings?: LanguageInfo) => {
|
||||
// Get language settings from the SIE
|
||||
const locale = detectLanguage(languageSettings);
|
||||
|
||||
const { resources, lng } = await getI18nResource(locale);
|
||||
const initI18n = async () => {
|
||||
const { resources, lng } = await getI18nResource();
|
||||
|
||||
const options: InitOptions = {
|
||||
resources,
|
||||
|
|
|
@ -8,10 +8,18 @@ import LanguageDetector from 'i18next-browser-languagedetector';
|
|||
import { getPhrases } from '@/apis/settings';
|
||||
|
||||
export const getI18nResource = async (
|
||||
locale?: string | string[]
|
||||
language?: string
|
||||
): Promise<{ resources: Resource; lng: string }> => {
|
||||
const detectedLanguage = detectLanguage();
|
||||
|
||||
try {
|
||||
const response = await getPhrases(Array.isArray(locale) ? locale.join(' ') : locale);
|
||||
const response = await getPhrases({
|
||||
localLanguage: Array.isArray(detectedLanguage)
|
||||
? detectedLanguage.join(' ')
|
||||
: detectedLanguage,
|
||||
language,
|
||||
});
|
||||
|
||||
const phrases = await response.json<LocalePhrase>();
|
||||
const lng = response.headers.get('Content-Language');
|
||||
|
||||
|
@ -53,8 +61,8 @@ export const detectLanguage = (languageSettings?: LanguageInfo) => {
|
|||
};
|
||||
|
||||
// Must be called after i18n's initialization
|
||||
export const changeLanguage = async (targetLanguage: string) => {
|
||||
const { resources, lng } = await getI18nResource(targetLanguage);
|
||||
export const changeLanguage = async (language: string) => {
|
||||
const { resources, lng } = await getI18nResource(language);
|
||||
|
||||
for (const [namespace, resource] of Object.entries(resources[lng] ?? {})) {
|
||||
i18next.addResourceBundle(lng, namespace, resource);
|
||||
|
|
Loading…
Add table
Reference in a new issue