mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat(core): route for add protected application (#5114)
This commit is contained in:
parent
ee268ac028
commit
3e39e2e428
22 changed files with 145 additions and 7 deletions
|
@ -130,6 +130,21 @@ describe('application route', () => {
|
|||
},
|
||||
})
|
||||
).resolves.toHaveProperty('status', 400);
|
||||
await expect(
|
||||
applicationRequest.post('/applications').send({
|
||||
name,
|
||||
type: ApplicationType.Protected,
|
||||
})
|
||||
).resolves.toHaveProperty('status', 400);
|
||||
await expect(
|
||||
applicationRequest.post('/applications').send({
|
||||
name,
|
||||
type: ApplicationType.Protected,
|
||||
protectedAppMetadata: {
|
||||
host: 'https://example.com',
|
||||
},
|
||||
})
|
||||
).resolves.toHaveProperty('status', 400);
|
||||
});
|
||||
|
||||
it('GET /applications/:id', async () => {
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
buildDemoAppDataForTenant,
|
||||
InternalRole,
|
||||
ApplicationType,
|
||||
applicationPatchGuard,
|
||||
} from '@logto/schemas';
|
||||
import { generateStandardId, generateStandardSecret } from '@logto/shared';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
@ -20,7 +19,12 @@ import { parseSearchParamsForSearch } from '#src/utils/search.js';
|
|||
|
||||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
import { applicationResponseGuard, applicationCreateGuard } from './types.js';
|
||||
import {
|
||||
applicationResponseGuard,
|
||||
applicationCreateGuard,
|
||||
applicationPatchGuard,
|
||||
} from './types.js';
|
||||
import { buildProtectedAppMetadata } from './utils.js';
|
||||
|
||||
const includesInternalAdminRole = (roles: Readonly<Array<{ role: Role }>>) =>
|
||||
roles.some(({ role: { name } }) => name === InternalRole.Admin);
|
||||
|
@ -127,7 +131,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
status: [200, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { oidcClientMetadata, ...rest } = ctx.guard.body;
|
||||
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
||||
|
||||
// When creating a m2m app, should check both m2m limit and application limit.
|
||||
if (rest.type === ApplicationType.MachineToMachine) {
|
||||
|
@ -135,6 +139,11 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
await quota.guardKey('applicationsLimit');
|
||||
|
||||
assertThat(
|
||||
rest.type !== ApplicationType.Protected || protectedAppMetadata,
|
||||
'application.protected_app_metadata_is_required'
|
||||
);
|
||||
|
||||
// Third party applications must be traditional type
|
||||
if (rest.isThirdParty) {
|
||||
assertThat(
|
||||
|
@ -143,10 +152,19 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO LOG-7794: check and add domain to Cloudflare
|
||||
|
||||
// TODO LOG-7520: sync configs to Cloudflare KV
|
||||
|
||||
ctx.body = await insertApplication({
|
||||
id: generateStandardId(),
|
||||
secret: generateStandardSecret(),
|
||||
oidcClientMetadata: buildOidcClientMetadata(oidcClientMetadata),
|
||||
...conditional(
|
||||
rest.type === ApplicationType.Protected &&
|
||||
protectedAppMetadata &&
|
||||
buildProtectedAppMetadata(protectedAppMetadata)
|
||||
),
|
||||
...rest,
|
||||
});
|
||||
|
||||
|
|
10
packages/core/src/routes/applications/constants.ts
Normal file
10
packages/core/src/routes/applications/constants.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { type ProtectedAppMetadata } from '@logto/schemas';
|
||||
|
||||
export const defaultProtectedAppSessionDuration: ProtectedAppMetadata['sessionDuration'] =
|
||||
60 * 60 * 24 * 14; // 14 days
|
||||
|
||||
export const defaultProtectedAppPageRules: ProtectedAppMetadata['pageRules'] = [
|
||||
{
|
||||
path: '^/', // Match all paths
|
||||
},
|
||||
];
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Applications,
|
||||
applicationCreateGuard as originalApplicationCreateGuard,
|
||||
applicationPatchGuard as originalApplicationPatchGuard,
|
||||
} from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -23,10 +24,31 @@ export const applicationResponseGuard: typeof Applications.guard = EnvSet.values
|
|||
.omit({ isThirdParty: true, type: true, protectedAppMetadata: true })
|
||||
.extend({ type: z.nativeEnum(OriginalApplicationType) });
|
||||
|
||||
const applicationCreateGuardWithProtectedAppMetadata = originalApplicationCreateGuard
|
||||
.omit({
|
||||
protectedAppMetadata: true,
|
||||
})
|
||||
.extend({
|
||||
protectedAppMetadata: z
|
||||
.object({
|
||||
host: z.string(),
|
||||
origin: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// FIXME: @wangsijie Remove this guard once protected app is ready
|
||||
// @ts-expect-error -- hide the dev feature field from the guard type, but always return the full type to make the api logic simpler
|
||||
export const applicationCreateGuard: typeof originalApplicationCreateGuard = EnvSet.values
|
||||
.isDevFeaturesEnabled
|
||||
? originalApplicationCreateGuard
|
||||
: originalApplicationCreateGuard
|
||||
export const applicationCreateGuard: typeof applicationCreateGuardWithProtectedAppMetadata = EnvSet
|
||||
.values.isDevFeaturesEnabled
|
||||
? applicationCreateGuardWithProtectedAppMetadata
|
||||
: applicationCreateGuardWithProtectedAppMetadata
|
||||
.omit({ isThirdParty: true, type: true, protectedAppMetadata: true })
|
||||
.extend({ type: z.nativeEnum(OriginalApplicationType) });
|
||||
|
||||
// FIXME: @wangsijie Remove this guard once protected app is ready
|
||||
// @ts-expect-error -- hide the dev feature field from the guard type, but always return the full type to make the api logic simpler
|
||||
export const applicationPatchGuard: typeof originalApplicationPatchGuard = EnvSet.values
|
||||
.isDevFeaturesEnabled
|
||||
? originalApplicationPatchGuard
|
||||
: originalApplicationPatchGuard.omit({ protectedAppMetadata: true });
|
||||
|
|
10
packages/core/src/routes/applications/utils.ts
Normal file
10
packages/core/src/routes/applications/utils.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defaultProtectedAppPageRules, defaultProtectedAppSessionDuration } from './constants.js';
|
||||
|
||||
export const buildProtectedAppMetadata = ({ host, origin }: { host: string; origin: string }) => ({
|
||||
protectedAppMetadata: {
|
||||
host,
|
||||
origin,
|
||||
sessionDuration: defaultProtectedAppSessionDuration,
|
||||
pageRules: defaultProtectedAppPageRules,
|
||||
},
|
||||
});
|
|
@ -48,6 +48,33 @@ describe('admin console application', () => {
|
|||
await deleteApplication(application.id);
|
||||
});
|
||||
|
||||
it('should create protected application successfully', async () => {
|
||||
const applicationName = 'test-protected-app';
|
||||
const metadata = {
|
||||
origin: 'https://example.com',
|
||||
host: 'example.protected.app',
|
||||
};
|
||||
|
||||
const application = await createApplication(applicationName, ApplicationType.Protected, {
|
||||
// @ts-expect-error the create guard has been modified
|
||||
protectedAppMetadata: metadata,
|
||||
});
|
||||
|
||||
expect(application.name).toBe(applicationName);
|
||||
expect(application.type).toBe(ApplicationType.Protected);
|
||||
expect(application.protectedAppMetadata).toHaveProperty('origin', metadata.origin);
|
||||
expect(application.protectedAppMetadata).toHaveProperty('host', metadata.host);
|
||||
expect(application.protectedAppMetadata).toHaveProperty('sessionDuration');
|
||||
await deleteApplication(application.id);
|
||||
});
|
||||
|
||||
it('should throw error when creating a protected application with invalid type', async () => {
|
||||
await expectRejects(createApplication('test-create-app', ApplicationType.Protected), {
|
||||
code: 'application.protected_app_metadata_is_required',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update application details successfully', async () => {
|
||||
const application = await createApplication('test-update-app', ApplicationType.Traditional);
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ApplicationType } from '@logto/schemas';
|
||||
|
||||
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
|
||||
import {
|
||||
expectConfirmModalAndAct,
|
||||
|
@ -222,6 +224,12 @@ describe('applications', () => {
|
|||
it.each(applicationTypesMetadata)(
|
||||
'can create and modify a(n) $type application without framework',
|
||||
async (app: ApplicationMetadata) => {
|
||||
if (app.type === ApplicationType.Protected) {
|
||||
// TODO @wangsijie: Remove this guard once protected app is ready
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(page).toClick('div[class$=main] div[class$=headline] button span', {
|
||||
text: 'Create application',
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@ const application = {
|
|||
'Nur traditionelle Webanwendungen können als Drittanbieter-App markiert werden.',
|
||||
third_party_application_only: 'Das Feature ist nur für Drittanbieter-Anwendungen verfügbar.',
|
||||
user_consent_scopes_not_found: 'Ungültige Benutzerzustimmungsbereiche.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -6,6 +6,7 @@ const application = {
|
|||
'Only traditional web applications can be marked as a third-party app.',
|
||||
third_party_application_only: 'The feature is only available for third-party applications.',
|
||||
user_consent_scopes_not_found: 'Invalid user consent scopes.',
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -7,6 +7,8 @@ const application = {
|
|||
'Solo las aplicaciones web tradicionales pueden ser marcadas como una aplicación de terceros.',
|
||||
third_party_application_only: 'La función solo está disponible para aplicaciones de terceros.',
|
||||
user_consent_scopes_not_found: 'Ámbitos de consentimiento de usuario no válidos.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -8,6 +8,8 @@ const application = {
|
|||
third_party_application_only:
|
||||
'La fonctionnalité est uniquement disponible pour les applications tierces.',
|
||||
user_consent_scopes_not_found: 'Portées de consentement utilisateur invalides.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -8,6 +8,8 @@ const application = {
|
|||
third_party_application_only:
|
||||
'La funzionalità è disponibile solo per le applicazioni di terze parti.',
|
||||
user_consent_scopes_not_found: 'Scopi di consenso utente non validi.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -7,6 +7,8 @@ const application = {
|
|||
'伝統的なWebアプリケーションにのみ、サードパーティアプリとしてマークできます。',
|
||||
third_party_application_only: 'この機能はサードパーティアプリケーションにのみ利用可能です。',
|
||||
user_consent_scopes_not_found: '無効なユーザー同意スコープ。',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -6,6 +6,7 @@ const application = {
|
|||
'Only traditional web applications can be marked as a third-party app.',
|
||||
third_party_application_only: 'The feature is only available for third-party applications.',
|
||||
user_consent_scopes_not_found: '유효하지 않은 사용자 동의 범위입니다.',
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -6,6 +6,8 @@ const application = {
|
|||
'Tylko tradycyjne aplikacje internetowe mogą być oznaczone jako aplikacja zewnętrzna.',
|
||||
third_party_application_only: 'Ta funkcja jest dostępna tylko dla aplikacji zewnętrznych.',
|
||||
user_consent_scopes_not_found: 'Nieprawidłowe zakresy zgody użytkownika.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -7,6 +7,8 @@ const application = {
|
|||
'Apenas aplicativos da web tradicionais podem ser marcados como um aplicativo de terceiros.',
|
||||
third_party_application_only: 'O recurso está disponível apenas para aplicativos de terceiros.',
|
||||
user_consent_scopes_not_found: 'Escopos de consentimento do usuário inválidos.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -7,6 +7,8 @@ const application = {
|
|||
'Apenas aplicações web tradicionais podem ser marcadas como uma aplicação de terceiros.',
|
||||
third_party_application_only: 'A funcionalidade só está disponível para aplicações de terceiros.',
|
||||
user_consent_scopes_not_found: 'Escopos de consentimento de utilizador inválidos.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -8,6 +8,8 @@ const application = {
|
|||
third_party_application_only:
|
||||
'Эта функция доступна только для приложений сторонних разработчиков.',
|
||||
user_consent_scopes_not_found: 'Недействительные области согласия пользователя.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -6,6 +6,8 @@ const application = {
|
|||
'Sadece geleneksel web uygulamaları üçüncü taraf uygulaması olarak işaretlenebilir.',
|
||||
third_party_application_only: 'Bu özellik sadece üçüncü taraf uygulamalar için geçerlidir.',
|
||||
user_consent_scopes_not_found: 'Geçersiz kullanıcı onay kapsamları.',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -5,6 +5,8 @@ const application = {
|
|||
invalid_third_party_application_type: '只有传统网络应用程序可以标记为第三方应用。',
|
||||
third_party_application_only: '该功能仅适用于第三方应用程序。',
|
||||
user_consent_scopes_not_found: '无效的用户同意范围。',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -5,6 +5,8 @@ const application = {
|
|||
invalid_third_party_application_type: '只有傳統網頁應用程式才能被標記為第三方應用程式。',
|
||||
third_party_application_only: '此功能只適用於第三方應用程式。',
|
||||
user_consent_scopes_not_found: '無效的使用者同意範圍。',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
|
@ -5,6 +5,8 @@ const application = {
|
|||
invalid_third_party_application_type: '僅傳統網路應用程式可以標記為第三方應用程式。',
|
||||
third_party_application_only: '該功能僅適用於第三方應用程式。',
|
||||
user_consent_scopes_not_found: '無效的使用者同意範圍。',
|
||||
/** UNTRANSLATED */
|
||||
protected_app_metadata_is_required: 'Protected app metadata is required.',
|
||||
};
|
||||
|
||||
export default Object.freeze(application);
|
||||
|
|
Loading…
Add table
Reference in a new issue