mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(core)!: move phrases api to well-known path (#3374)
This commit is contained in:
parent
268679b02e
commit
dfc1f20d27
8 changed files with 69 additions and 73 deletions
6
.changeset-staged/stupid-jokes-brush.md
Normal file
6
.changeset-staged/stupid-jokes-brush.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@logto/core": major
|
||||
"@logto/ui": major
|
||||
---
|
||||
|
||||
**💥 BREAKING CHANGE 💥** Move `/api/phrase` API to `/api/.well-known/phrases`
|
|
@ -19,7 +19,6 @@ import hookRoutes from './hook.js';
|
|||
import interactionRoutes from './interaction/index.js';
|
||||
import logRoutes from './log.js';
|
||||
import logtoConfigRoutes from './logto-config.js';
|
||||
import phraseRoutes from './phrase.js';
|
||||
import resourceRoutes from './resource.js';
|
||||
import roleRoutes from './role.js';
|
||||
import roleScopeRoutes from './role.scope.js';
|
||||
|
@ -54,7 +53,6 @@ const createRouters = (tenant: TenantContext) => {
|
|||
userAssetsRoutes(managementRouter, tenant);
|
||||
|
||||
const anonymousRouter: AnonymousRouter = new Router();
|
||||
phraseRoutes(anonymousRouter, tenant);
|
||||
wellKnownRoutes(anonymousRouter, tenant);
|
||||
statusRoutes(anonymousRouter, tenant);
|
||||
authnRoutes(anonymousRouter, tenant);
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
||||
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';
|
||||
|
||||
export default function phraseRoutes<T extends AnonymousRouter>(
|
||||
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
customPhrases: { findAllCustomLanguageTags },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
} = queries;
|
||||
const { getPhrases } = libraries.phrases;
|
||||
|
||||
const getLanguageInfo = async () => {
|
||||
const { languageInfo } = await findDefaultSignInExperience();
|
||||
|
||||
return languageInfo;
|
||||
};
|
||||
|
||||
router.get(
|
||||
'/phrase',
|
||||
koaGuard({
|
||||
query: object({
|
||||
lng: string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
query: { lng },
|
||||
} = ctx.guard;
|
||||
|
||||
const { autoDetect, fallbackLanguage } = await getLanguageInfo();
|
||||
|
||||
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';
|
||||
|
||||
ctx.set('Content-Language', language);
|
||||
ctx.body = await getPhrases(language, customLanguages);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -32,7 +32,7 @@ const tenantContext = new MockTenant(
|
|||
{ phrases: { getPhrases: jest.fn().mockResolvedValue(en) } }
|
||||
);
|
||||
|
||||
const phraseRoutes = await pickDefault(import('./phrase.js'));
|
||||
const phraseRoutes = await pickDefault(import('./well-known.js'));
|
||||
|
||||
const phraseRequest = createRequester({
|
||||
anonymousRoutes: phraseRoutes,
|
||||
|
@ -54,7 +54,7 @@ describe('when auto-detect is not enabled', () => {
|
|||
},
|
||||
});
|
||||
const response = await phraseRequest
|
||||
.get('/phrase')
|
||||
.get('/.well-known/phrases')
|
||||
.set('Accept-Language', `${zhCnTag},${zhHkTag}`);
|
||||
expect(response.headers['content-language']).toEqual('en');
|
||||
});
|
||||
|
@ -71,13 +71,13 @@ describe('when auto-detect is not enabled', () => {
|
|||
});
|
||||
|
||||
it('when there is no detected language', async () => {
|
||||
const response = await phraseRequest.get('/phrase');
|
||||
const response = await phraseRequest.get('/.well-known/phrases');
|
||||
expect(response.headers['content-language']).toEqual(fallbackLanguage);
|
||||
});
|
||||
|
||||
it('when there are detected languages', async () => {
|
||||
const response = await phraseRequest
|
||||
.get('/phrase')
|
||||
.get('/.well-known/phrases')
|
||||
.set('Accept-Language', `${zhCnTag},${zhHkTag}`);
|
||||
expect(response.headers['content-language']).toEqual(fallbackLanguage);
|
||||
});
|
||||
|
@ -95,7 +95,7 @@ describe('when auto-detect is enabled', () => {
|
|||
},
|
||||
});
|
||||
const response = await phraseRequest
|
||||
.get('/phrase')
|
||||
.get('/.well-known/phrases')
|
||||
.set('Accept-Language', unsupportedLanguageY);
|
||||
expect(response.headers['content-language']).toEqual('en');
|
||||
});
|
||||
|
@ -113,7 +113,7 @@ describe('when auto-detect is enabled', () => {
|
|||
|
||||
describe('when there is no detected language', () => {
|
||||
it('should be fallback language from sign-in experience', async () => {
|
||||
const response = await phraseRequest.get('/phrase');
|
||||
const response = await phraseRequest.get('/.well-known/phrases');
|
||||
expect(response.headers['content-language']).toEqual(fallbackLanguage);
|
||||
});
|
||||
});
|
||||
|
@ -121,7 +121,7 @@ describe('when auto-detect is enabled', () => {
|
|||
describe('when there are detected languages but all of them is unsupported', () => {
|
||||
it('should be first supported detected language', async () => {
|
||||
const response = await phraseRequest
|
||||
.get('/phrase')
|
||||
.get('/.well-known/phrases')
|
||||
.set('Accept-Language', `${unsupportedLanguageX},${unsupportedLanguageY}`);
|
||||
expect(response.headers['content-language']).toEqual(fallbackLanguage);
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ describe('when auto-detect is enabled', () => {
|
|||
it('should be first supported detected language', async () => {
|
||||
const firstSupportedLanguage = zhCnTag;
|
||||
const response = await phraseRequest
|
||||
.get('/phrase')
|
||||
.get('/.well-known/phrases')
|
||||
.set('Accept-Language', `${unsupportedLanguageX},${firstSupportedLanguage},${zhHkTag}`);
|
||||
expect(response.headers['content-language']).toEqual(firstSupportedLanguage);
|
||||
});
|
|
@ -49,7 +49,7 @@ const tenantContext = new MockTenant(
|
|||
{ phrases: { getPhrases } }
|
||||
);
|
||||
|
||||
const phraseRoutes = await pickDefault(import('./phrase.js'));
|
||||
const phraseRoutes = await pickDefault(import('./well-known.js'));
|
||||
|
||||
const { createRequester } = await import('#src/utils/test-utils.js');
|
||||
const phraseRequest = createRequester({
|
||||
|
@ -63,7 +63,7 @@ describe('when the application is not admin-console', () => {
|
|||
});
|
||||
|
||||
it('should call findDefaultSignInExperience', async () => {
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
|
||||
expect(findDefaultSignInExperience).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
|
@ -75,7 +75,7 @@ describe('when the application is not admin-console', () => {
|
|||
autoDetect: true,
|
||||
},
|
||||
});
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
|
||||
expect(detectLanguageSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
|
@ -87,12 +87,12 @@ describe('when the application is not admin-console', () => {
|
|||
autoDetect: false,
|
||||
},
|
||||
});
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
|
||||
expect(detectLanguageSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call findAllCustomLanguageTags', async () => {
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
|
||||
expect(findAllCustomLanguageTags).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
|
@ -104,7 +104,7 @@ describe('when the application is not admin-console', () => {
|
|||
fallbackLanguage: customizedLanguage,
|
||||
},
|
||||
});
|
||||
await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200);
|
||||
await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
|
||||
expect(getPhrases).toBeCalledTimes(1);
|
||||
expect(getPhrases).toBeCalledWith(customizedLanguage, [customizedLanguage]);
|
||||
});
|
||||
|
@ -117,7 +117,10 @@ describe('when the application is not admin-console', () => {
|
|||
fallbackLanguage: customizedLanguage,
|
||||
},
|
||||
});
|
||||
await expect(phraseRequest.get('/phrase?lng=fr')).resolves.toHaveProperty('status', 200);
|
||||
await expect(phraseRequest.get('/.well-known/phrases?lng=fr')).resolves.toHaveProperty(
|
||||
'status',
|
||||
200
|
||||
);
|
||||
expect(getPhrases).toBeCalledWith('fr', [customizedLanguage]);
|
||||
});
|
||||
});
|
|
@ -1,18 +1,27 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
||||
import { adminTenantId } from '@logto/schemas';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import detectLanguage from '#src/i18n/detect-language.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||
...[router, { libraries, id }]: RouterInitArgs<T>
|
||||
...[router, { queries, libraries, id }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
customPhrases: { findAllCustomLanguageTags },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
} = queries;
|
||||
const {
|
||||
signInExperiences: { getSignInExperience },
|
||||
connectors: { getLogtoConnectors },
|
||||
phrases: { getPhrases },
|
||||
} = libraries;
|
||||
|
||||
if (id === adminTenantId) {
|
||||
|
@ -61,4 +70,36 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/.well-known/phrases',
|
||||
koaGuard({
|
||||
query: object({
|
||||
lng: string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
query: { lng },
|
||||
} = ctx.guard;
|
||||
|
||||
const {
|
||||
languageInfo: { autoDetect, fallbackLanguage },
|
||||
} = await findDefaultSignInExperience();
|
||||
|
||||
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';
|
||||
|
||||
ctx.set('Content-Language', language);
|
||||
ctx.body = await getPhrases(language, customLanguages);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* The API will be deprecated in the future once SSR is implemented.
|
||||
*/
|
||||
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import ky from 'ky';
|
||||
|
||||
import type { SignInExperienceResponse } from '@/types';
|
||||
|
@ -30,4 +31,4 @@ export const getPhrases = async ({
|
|||
],
|
||||
},
|
||||
})
|
||||
.get(`/api/phrase${language ? `?lng=${language}` : ''}`);
|
||||
.get(`/api/.well-known/phrases${conditionalString(language && `?lng=${language}`)}`);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Logto</title>
|
||||
<link rel="preload" href="/api/.well-known/sign-in-exp" as="fetch" crossorigin="anonymous">
|
||||
<link rel="preload" href="/api/phrase" as="fetch" crossorigin="anonymous">
|
||||
<link rel="preload" href="/api/.well-known/phrases" as="fetch" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
Loading…
Reference in a new issue