From f176bdc90ac54418b9a50b25c990d3d2e7cadab8 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Tue, 2 Jun 2026 11:08:21 +0800 Subject: [PATCH] feat(experience): add generic app access denied page --- .../src/middleware/koa-auto-consent.test.ts | 131 ++++++++++++++++++ .../core/src/middleware/koa-auto-consent.ts | 26 +++- .../src/routes/interaction/consent/index.ts | 5 +- packages/core/src/tenants/Tenant.ts | 4 +- .../src/pages/Consent/index.test.tsx | 110 +++++++++++++++ .../experience/src/pages/Consent/index.tsx | 40 +++++- .../api/application-access-control.test.ts | 13 +- .../src/locales/ar/error/index.ts | 3 + .../src/locales/cs/error/index.ts | 3 + .../src/locales/de/error/index.ts | 3 + .../src/locales/en/error/index.ts | 3 + .../src/locales/es/error/index.ts | 3 + .../src/locales/fr/error/index.ts | 3 + .../src/locales/it/error/index.ts | 3 + .../src/locales/ja/error/index.ts | 3 + .../src/locales/ko/error/index.ts | 3 + .../src/locales/pl-pl/error/index.ts | 3 + .../src/locales/pt-br/error/index.ts | 3 + .../src/locales/pt-pt/error/index.ts | 3 + .../src/locales/ru/error/index.ts | 3 + .../src/locales/th/error/index.ts | 3 + .../src/locales/tr-tr/error/index.ts | 3 + .../src/locales/uk-ua/error/index.ts | 3 + .../src/locales/zh-cn/error/index.ts | 2 + .../src/locales/zh-hk/error/index.ts | 2 + .../src/locales/zh-tw/error/index.ts | 2 + 26 files changed, 369 insertions(+), 14 deletions(-) create mode 100644 packages/core/src/middleware/koa-auto-consent.test.ts create mode 100644 packages/experience/src/pages/Consent/index.test.tsx diff --git a/packages/core/src/middleware/koa-auto-consent.test.ts b/packages/core/src/middleware/koa-auto-consent.test.ts new file mode 100644 index 000000000000..93c3005f892b --- /dev/null +++ b/packages/core/src/middleware/koa-auto-consent.test.ts @@ -0,0 +1,131 @@ +import type { Provider } from 'oidc-provider'; + +import RequestError from '#src/errors/RequestError/index.js'; +import type Queries from '#src/tenants/Queries.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; +import { createContextWithRouteParameters } from '#src/utils/test-utils.js'; + +import koaAutoConsent from './koa-auto-consent.js'; + +const { jest } = import.meta; +type InteractionDetails = Awaited>; +type ApplicationQueries = Queries['applications']; +const prompt = { + name: 'consent', + reasons: [], + details: {}, +} satisfies InteractionDetails['prompt']; + +const createContext = (interactionDetails: Partial, redirect = jest.fn()) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- minimal interaction details stub for middleware testing + const details = { + params: {}, + ...interactionDetails, + } as InteractionDetails; + + return { + ...createContextWithRouteParameters({ + url: '/consent', + redirect, + }), + interactionDetails: details, + }; +}; + +const createTenant = ({ + isThirdParty = false, + assertUserHasApplicationAccess = jest.fn(async () => { + await Promise.resolve(); + }), +}: { + readonly isThirdParty?: boolean; + readonly assertUserHasApplicationAccess?: jest.Mock; +} = {}) => { + const findApplicationById = jest.fn(async () => ({ + isThirdParty, + })) as unknown as ApplicationQueries['findApplicationById']; + + return { + findApplicationById, + tenant: new MockTenant( + undefined, + { + applications: { findApplicationById }, + }, + undefined, + { + applicationAccessControl: { assertUserHasApplicationAccess }, + } + ), + }; +}; + +describe('koaAutoConsent middleware', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should continue to the consent SPA when auto-consent application access is denied', async () => { + const accessDeniedError = new RequestError('oidc.access_denied'); + const assertUserHasApplicationAccess = jest.fn(async () => { + throw accessDeniedError; + }); + const { findApplicationById, tenant } = createTenant({ assertUserHasApplicationAccess }); + const redirect = jest.fn(); + const next = jest.fn(); + const ctx = createContext( + { + params: { client_id: 'app-id' }, + prompt, + // @ts-expect-error + session: { accountId: 'user-id' }, + }, + redirect + ); + const guard = koaAutoConsent({} as Provider, tenant.queries, tenant.libraries); + + await guard(ctx, next); + + expect(findApplicationById).toHaveBeenCalledWith('app-id'); + expect(assertUserHasApplicationAccess).toHaveBeenCalledWith('app-id', 'user-id'); + expect(redirect).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + it('should not check application access for third-party applications', async () => { + const assertUserHasApplicationAccess = jest.fn(); + const { tenant } = createTenant({ isThirdParty: true, assertUserHasApplicationAccess }); + const next = jest.fn(); + const ctx = createContext({ + params: { client_id: 'app-id' }, + prompt, + // @ts-expect-error + session: { accountId: 'user-id' }, + }); + const guard = koaAutoConsent({} as Provider, tenant.queries, tenant.libraries); + + await guard(ctx, next); + + expect(assertUserHasApplicationAccess).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + it('should throw non-access-denied application access errors', async () => { + const error = new RequestError({ code: 'request.invalid_input' }); + const assertUserHasApplicationAccess = jest.fn(async () => { + throw error; + }); + const { tenant } = createTenant({ assertUserHasApplicationAccess }); + const next = jest.fn(); + const ctx = createContext({ + params: { client_id: 'app-id' }, + prompt, + // @ts-expect-error + session: { accountId: 'user-id' }, + }); + const guard = koaAutoConsent({} as Provider, tenant.queries, tenant.libraries); + + await expect(guard(ctx, next)).rejects.toBe(error); + expect(next).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/middleware/koa-auto-consent.ts b/packages/core/src/middleware/koa-auto-consent.ts index 858f3ad5a1ef..b3fd37b4d258 100644 --- a/packages/core/src/middleware/koa-auto-consent.ts +++ b/packages/core/src/middleware/koa-auto-consent.ts @@ -3,8 +3,10 @@ import { type MiddlewareType } from 'koa'; import type { Provider } from 'oidc-provider'; import { errors } from 'oidc-provider'; +import RequestError from '#src/errors/RequestError/index.js'; import { consent, getMissingScopes } from '#src/libraries/session/index.js'; import type { WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js'; +import type Libraries from '#src/tenants/Libraries.js'; import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; @@ -24,18 +26,27 @@ const shouldAutoConsentApplication = async (clientId: string, query: Queries) => return !application.isThirdParty; }; +const isApplicationAccessDeniedError = (error: unknown) => + error instanceof RequestError && error.code === 'oidc.access_denied'; + export default function koaAutoConsent< StateT, ContextT extends WithInteractionDetailsContext, ResponseBodyT, ->(provider: Provider, query: Queries): MiddlewareType { +>( + provider: Provider, + query: Queries, + libraries: Libraries +): MiddlewareType { return async (ctx, next) => { const { interactionDetails } = ctx; const { params: { client_id: clientId }, prompt, + session, } = interactionDetails; + assertThat(session, new RequestError({ code: 'session.not_found' })); assertThat( clientId && typeof clientId === 'string', new errors.InvalidClient('client must be available') @@ -44,6 +55,19 @@ export default function koaAutoConsent< const shouldAutoConsent = await shouldAutoConsentApplication(clientId, query); if (shouldAutoConsent) { + try { + await libraries.applicationAccessControl.assertUserHasApplicationAccess( + clientId, + session.accountId + ); + } catch (error: unknown) { + if (isApplicationAccessDeniedError(error)) { + return next(); + } + + throw error; + } + const { missingOIDCScope: missingOIDCScopes, missingResourceScopes: resourceScopesToGrant } = getMissingScopes(prompt); diff --git a/packages/core/src/routes/interaction/consent/index.ts b/packages/core/src/routes/interaction/consent/index.ts index dc1d1b391d66..b5a8c6e08c3f 100644 --- a/packages/core/src/routes/interaction/consent/index.ts +++ b/packages/core/src/routes/interaction/consent/index.ts @@ -44,7 +44,7 @@ export default function consentRoutes( body: z.object({ organizationIds: z.string().array().optional(), }), - status: [200], + status: [200, 400], }), koaAppAccessControl(libraries), async (ctx, next) => { @@ -202,9 +202,10 @@ export default function consentRoutes( router.get( consentPath, koaGuard({ - status: [200], + status: [200, 400], response: consentInfoResponseGuard, }), + koaAppAccessControl(libraries), async (ctx, next) => { const { interactionDetails } = ctx; diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index 32a14459bfc4..f23438fb7293 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -13,7 +13,6 @@ import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js'; import { createCloudConnectionLibrary } from '#src/libraries/cloud-connection.js'; import { createConnectorLibrary } from '#src/libraries/connector.js'; import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js'; -import koaAppAccessControl from '#src/middleware/koa-app-access-control.js'; import koaAutoConsent from '#src/middleware/koa-auto-consent.js'; import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js'; import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js'; @@ -246,8 +245,7 @@ export default class Tenant implements TenantContext { compose([ koaInteractionDetails(provider), koaConsentGuard(libraries, queries), - koaAppAccessControl(libraries), - koaAutoConsent(provider, queries), + koaAutoConsent(provider, queries, libraries), ]) ), koaSpaProxy({ mountedApps, queries }), diff --git a/packages/experience/src/pages/Consent/index.test.tsx b/packages/experience/src/pages/Consent/index.test.tsx new file mode 100644 index 000000000000..5ecc99e266d0 --- /dev/null +++ b/packages/experience/src/pages/Consent/index.test.tsx @@ -0,0 +1,110 @@ +import type { ConsentInfoResponse, RequestErrorBody } from '@logto/schemas'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { HTTPError } from 'ky'; + +import UserInteractionContextProvider from '@/Providers/UserInteractionContextProvider'; +import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; +import { consent, getConsentInfo } from '@/apis/consent'; + +import Consent from '.'; + +jest.mock('@/apis/consent', () => ({ + consent: jest.fn(), + getConsentInfo: jest.fn(), +})); + +const mockedConsent = consent as jest.MockedFunction; +const mockedGetConsentInfo = getConsentInfo as jest.MockedFunction; + +const consentInfo: ConsentInfoResponse = { + application: { + id: 'application_id', + name: 'Application', + displayName: null, + privacyPolicyUrl: null, + termsOfUseUrl: null, + }, + user: { + id: 'user_id', + name: null, + avatar: null, + username: 'user', + primaryEmail: 'user@example.com', + primaryPhone: null, + }, + missingOIDCScope: [], + missingResourceScopes: [], + redirectUri: 'https://example.com/callback', +}; + +const createHttpError = (body: RequestErrorBody) => + new HTTPError( + { + status: 400, + statusText: 'Bad Request', + json: async () => body, + clone: () => ({ + json: async () => body, + }), + } as Response, + {} as Request, + {} as ConstructorParameters[2] + ); + +const accessDeniedError = () => + createHttpError({ + code: 'oidc.access_denied', + data: undefined, + message: 'Access denied.', + }); + +const renderConsent = () => + renderWithPageContext( + + + + + + ); + +describe('Consent', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders generic access denied page when consent info is denied', async () => { + mockedGetConsentInfo.mockRejectedValueOnce(accessDeniedError()); + + const { queryByText } = renderConsent(); + + await waitFor(() => { + expect(queryByText('error.access_denied')).not.toBeNull(); + }); + + expect(queryByText('error.application_access_denied')).not.toBeNull(); + expect(queryByText('action.authorize')).toBeNull(); + expect(queryByText('action.cancel')).toBeNull(); + }); + + it('renders generic access denied page when consent submission is denied', async () => { + mockedGetConsentInfo.mockResolvedValueOnce(consentInfo); + mockedConsent.mockRejectedValueOnce(accessDeniedError()); + + const { getByText, queryByText } = renderConsent(); + + await waitFor(() => { + expect(queryByText('action.authorize')).not.toBeNull(); + }); + + fireEvent.click(getByText('action.authorize')); + + await waitFor(() => { + expect(queryByText('error.access_denied')).not.toBeNull(); + }); + + expect(queryByText('error.application_access_denied')).not.toBeNull(); + expect(queryByText('action.authorize')).toBeNull(); + expect(queryByText('action.cancel')).toBeNull(); + }); +}); diff --git a/packages/experience/src/pages/Consent/index.tsx b/packages/experience/src/pages/Consent/index.tsx index 885165c62509..e24fd4a69f19 100644 --- a/packages/experience/src/pages/Consent/index.tsx +++ b/packages/experience/src/pages/Consent/index.tsx @@ -1,6 +1,6 @@ import { ReservedResource } from '@logto/core-kit'; import { type ConsentInfoResponse } from '@logto/schemas'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import LandingPageLayout from '@/Layout/LandingPageLayout'; @@ -8,8 +8,9 @@ import { consent, getConsentInfo } from '@/apis/consent'; import TermsLinks from '@/components/TermsLinks'; import TextLink from '@/components/TextLink'; import useApi from '@/hooks/use-api'; -import useErrorHandler from '@/hooks/use-error-handler'; +import useErrorHandler, { type ErrorHandlers } from '@/hooks/use-error-handler'; import useGlobalRedirectTo from '@/hooks/use-global-redirect-to'; +import ErrorPage from '@/pages/ErrorPage'; import Button from '@/shared/components/Button'; import OrganizationSelector, { type Organization } from './OrganizationSelector'; @@ -26,18 +27,35 @@ const Consent = () => { const [consentData, setConsentData] = useState(); const [selectedOrganization, setSelectedOrganization] = useState(); + const [isAccessDenied, setIsAccessDenied] = useState(false); const [isConsentLoading, setIsConsentLoading] = useState(false); const asyncGetConsentInfo = useApi(getConsentInfo); + const consentErrorHandlers: ErrorHandlers = useMemo( + () => ({ + 'oidc.access_denied': () => { + setIsAccessDenied(true); + }, + }), + [] + ); + + const handleConsentError = useCallback( + async (error: unknown) => { + await handleError(error, consentErrorHandlers); + }, + [consentErrorHandlers, handleError] + ); + const consentHandler = useCallback(async () => { setIsConsentLoading(true); const [error, result] = await asyncConsent(selectedOrganization?.id); setIsConsentLoading(false); if (error) { - await handleError(error); + await handleConsentError(error); return; } @@ -45,14 +63,14 @@ const Consent = () => { if (result?.redirectTo) { await redirectTo(result.redirectTo); } - }, [asyncConsent, handleError, redirectTo, selectedOrganization?.id]); + }, [asyncConsent, handleConsentError, redirectTo, selectedOrganization?.id]); useEffect(() => { const getConsentInfoHandler = async () => { const [error, result] = await asyncGetConsentInfo(); if (error) { - await handleError(error); + await handleConsentError(error); return; } @@ -68,7 +86,17 @@ const Consent = () => { }; void getConsentInfoHandler(); - }, [asyncGetConsentInfo, handleError]); + }, [asyncGetConsentInfo, handleConsentError]); + + if (isAccessDenied) { + return ( + + ); + } if (!consentData) { return null; diff --git a/packages/integration-tests/src/tests/api/application-access-control.test.ts b/packages/integration-tests/src/tests/api/application-access-control.test.ts index e24020a9b680..c307e9a2de39 100644 --- a/packages/integration-tests/src/tests/api/application-access-control.test.ts +++ b/packages/integration-tests/src/tests/api/application-access-control.test.ts @@ -172,8 +172,17 @@ devFeatureTest.describe('application access control OIDC enforcement', () => { throwHttpErrors: false, }); - expect(consentResponse.status).toBe(400); - await expect(consentResponse.json()).resolves.toMatchObject({ + expect(consentResponse.status).toBe(200); + + const consentInfoResponse = await ky.get(`${logtoUrl}/api/interaction/consent`, { + headers: { + cookie: client.interactionCookie, + }, + throwHttpErrors: false, + }); + + expect(consentInfoResponse.status).toBe(400); + await expect(consentInfoResponse.json()).resolves.toMatchObject({ code: 'oidc.access_denied', }); } finally { diff --git a/packages/phrases-experience/src/locales/ar/error/index.ts b/packages/phrases-experience/src/locales/ar/error/index.ts index ae920e2310da..5fbc52878af0 100644 --- a/packages/phrases-experience/src/locales/ar/error/index.ts +++ b/packages/phrases-experience/src/locales/ar/error/index.ts @@ -31,6 +31,9 @@ const error = { terms_acceptance_required_description: 'يجب أن توافق على الشروط للمتابعة. يرجى المحاولة مرة أخرى.', something_went_wrong: 'حدث خطأ ما', + access_denied: 'تم رفض الوصول', + application_access_denied: + 'ليس لديك إذن للوصول إلى هذا التطبيق. يرجى الاتصال بالمسؤول للحصول على المساعدة.', feature_not_enabled: 'ليس لديك إذن للوصول إلى هذه الميزة. يرجى الاتصال بالمسؤول للحصول على المساعدة.', }; diff --git a/packages/phrases-experience/src/locales/cs/error/index.ts b/packages/phrases-experience/src/locales/cs/error/index.ts index 16c9ff72b9f1..6a132b1d30d7 100644 --- a/packages/phrases-experience/src/locales/cs/error/index.ts +++ b/packages/phrases-experience/src/locales/cs/error/index.ts @@ -30,6 +30,9 @@ const error = { terms_acceptance_required: 'Je nutné souhlasit s podmínkami', terms_acceptance_required_description: 'Pro pokračování je nutné souhlasit s podmínkami.', something_went_wrong: 'Něco se pokazilo.', + access_denied: 'Přístup odepřen', + application_access_denied: + 'Nemáš oprávnění k přístupu k této aplikaci. Kontaktuj prosím svého administrátora pro pomoc.', feature_not_enabled: 'Nemáš oprávnění k přístupu k této funkci. Kontaktuj prosím svého administrátora pro pomoc.', }; diff --git a/packages/phrases-experience/src/locales/de/error/index.ts b/packages/phrases-experience/src/locales/de/error/index.ts index 03ea86fc2a4d..f397fccfad89 100644 --- a/packages/phrases-experience/src/locales/de/error/index.ts +++ b/packages/phrases-experience/src/locales/de/error/index.ts @@ -32,6 +32,9 @@ const error = { terms_acceptance_required: 'Zustimmung zu den Bedingungen erforderlich', terms_acceptance_required_description: 'Du musst den Bedingungen zustimmen, um fortzufahren.', something_went_wrong: 'Etwas ist schiefgegangen', + access_denied: 'Zugriff verweigert', + application_access_denied: + 'Sie haben keine Berechtigung, auf diese Anwendung zuzugreifen. Bitte kontaktieren Sie Ihren Administrator um Hilfe.', feature_not_enabled: 'Sie haben keine Berechtigung, auf diese Funktion zuzugreifen. Bitte kontaktieren Sie Ihren Administrator um Hilfe.', }; diff --git a/packages/phrases-experience/src/locales/en/error/index.ts b/packages/phrases-experience/src/locales/en/error/index.ts index 435491d0bb71..7a11d11eb7f0 100644 --- a/packages/phrases-experience/src/locales/en/error/index.ts +++ b/packages/phrases-experience/src/locales/en/error/index.ts @@ -31,6 +31,9 @@ const error = { terms_acceptance_required: 'Terms acceptance required', terms_acceptance_required_description: 'You must agree to the terms to continue.', something_went_wrong: 'Something went wrong', + access_denied: 'Access denied', + application_access_denied: + 'You do not have permission to access this application. Please contact your administrator for assistance.', feature_not_enabled: 'You do not have permission to access this feature. Please contact your administrator for assistance.', }; diff --git a/packages/phrases-experience/src/locales/es/error/index.ts b/packages/phrases-experience/src/locales/es/error/index.ts index 6f726ad9852f..115e04de0085 100644 --- a/packages/phrases-experience/src/locales/es/error/index.ts +++ b/packages/phrases-experience/src/locales/es/error/index.ts @@ -34,6 +34,9 @@ const error = { terms_acceptance_required_description: 'Debes aceptar los términos para continuar. Por favor, inténtalo de nuevo.', something_went_wrong: 'Algo salió mal', + access_denied: 'Acceso denegado', + application_access_denied: + 'No tiene permiso para acceder a esta aplicación. Por favor, contacte a su administrador para obtener ayuda.', feature_not_enabled: 'No tiene permiso para acceder a esta función. Por favor, contacte a su administrador para obtener ayuda.', }; diff --git a/packages/phrases-experience/src/locales/fr/error/index.ts b/packages/phrases-experience/src/locales/fr/error/index.ts index 5f8033923b09..800203268fae 100644 --- a/packages/phrases-experience/src/locales/fr/error/index.ts +++ b/packages/phrases-experience/src/locales/fr/error/index.ts @@ -33,6 +33,9 @@ const error = { terms_acceptance_required: 'Acceptation des conditions requise', terms_acceptance_required_description: 'Vous devez accepter les conditions pour continuer.', something_went_wrong: 'Quelque chose a mal tourné', + access_denied: 'Accès refusé', + application_access_denied: + "Vous n'avez pas la permission d'accéder à cette application. Veuillez contacter votre administrateur pour obtenir de l'aide.", feature_not_enabled: "Vous n'avez pas la permission d'accéder à cette fonctionnalité. Veuillez contacter votre administrateur pour obtenir de l'aide.", }; diff --git a/packages/phrases-experience/src/locales/it/error/index.ts b/packages/phrases-experience/src/locales/it/error/index.ts index d69f95c14580..0eba1191c8ea 100644 --- a/packages/phrases-experience/src/locales/it/error/index.ts +++ b/packages/phrases-experience/src/locales/it/error/index.ts @@ -31,6 +31,9 @@ const error = { terms_acceptance_required: 'Accettazione dei termini richiesta', terms_acceptance_required_description: 'Devi accettare i termini per continuare.', something_went_wrong: 'Qualcosa è andato storto', + access_denied: 'Accesso negato', + application_access_denied: + 'Non hai il permesso di accedere a questa applicazione. Per assistenza, contatta il tuo amministratore.', feature_not_enabled: 'Non hai il permesso di accedere a questa funzionalità. Per assistenza, contatta il tuo amministratore.', }; diff --git a/packages/phrases-experience/src/locales/ja/error/index.ts b/packages/phrases-experience/src/locales/ja/error/index.ts index c7deecee9a68..3f3da339b6c1 100644 --- a/packages/phrases-experience/src/locales/ja/error/index.ts +++ b/packages/phrases-experience/src/locales/ja/error/index.ts @@ -33,6 +33,9 @@ const error = { terms_acceptance_required_description: '続行するには利用規約に同意する必要があります。もう一度お試しください。', something_went_wrong: '問題が発生しました', + access_denied: 'アクセスが拒否されました', + application_access_denied: + 'このアプリケーションにアクセスする権限がありません。管理者にお問い合わせください。', feature_not_enabled: 'この機能にアクセスする権限がありません。管理者にお問い合わせください。', }; diff --git a/packages/phrases-experience/src/locales/ko/error/index.ts b/packages/phrases-experience/src/locales/ko/error/index.ts index 2cf4959cb3a7..803f2b6c22d9 100644 --- a/packages/phrases-experience/src/locales/ko/error/index.ts +++ b/packages/phrases-experience/src/locales/ko/error/index.ts @@ -30,6 +30,9 @@ const error = { terms_acceptance_required: '약관 동의가 필요해요', terms_acceptance_required_description: '계속하려면 약관에 동의해야 해요.', something_went_wrong: '문제가 발생했어요', + access_denied: '접근이 거부되었어요', + application_access_denied: + '이 애플리케이션에 액세스할 권한이 없습니다. 관리자에게 도움을 요청하세요.', feature_not_enabled: '이 기능에 액세스할 권한이 없습니다. 관리자에게 도움을 요청하세요.', }; diff --git a/packages/phrases-experience/src/locales/pl-pl/error/index.ts b/packages/phrases-experience/src/locales/pl-pl/error/index.ts index eb89413a4403..1624b532738c 100644 --- a/packages/phrases-experience/src/locales/pl-pl/error/index.ts +++ b/packages/phrases-experience/src/locales/pl-pl/error/index.ts @@ -31,6 +31,9 @@ const error = { terms_acceptance_required: 'Wymagana akceptacja warunków', terms_acceptance_required_description: 'Musisz zaakceptować warunki, aby kontynuować.', something_went_wrong: 'Coś poszło nie tak', + access_denied: 'Odmowa dostępu', + application_access_denied: + 'Nie masz uprawnień do dostępu do tej aplikacji. Skontaktuj się z administratorem, aby uzyskać pomoc.', feature_not_enabled: 'Nie masz uprawnień do dostępu do tej funkcji. Skontaktuj się z administratorem, aby uzyskać pomoc.', }; diff --git a/packages/phrases-experience/src/locales/pt-br/error/index.ts b/packages/phrases-experience/src/locales/pt-br/error/index.ts index 76b891801586..7ba7f4a1dcde 100644 --- a/packages/phrases-experience/src/locales/pt-br/error/index.ts +++ b/packages/phrases-experience/src/locales/pt-br/error/index.ts @@ -31,6 +31,9 @@ const error = { terms_acceptance_required: 'Aceitação dos termos obrigatória', terms_acceptance_required_description: 'Você deve aceitar os termos para continuar.', something_went_wrong: 'Algo deu errado', + access_denied: 'Acesso negado', + application_access_denied: + 'Você não tem permissão para acessar este aplicativo. Entre em contato com seu administrador para obter ajuda.', feature_not_enabled: 'Você não tem permissão para acessar esse recurso. Entre em contato com seu administrador para obter ajuda.', }; diff --git a/packages/phrases-experience/src/locales/pt-pt/error/index.ts b/packages/phrases-experience/src/locales/pt-pt/error/index.ts index 016bb9e99bbf..885f26c767ea 100644 --- a/packages/phrases-experience/src/locales/pt-pt/error/index.ts +++ b/packages/phrases-experience/src/locales/pt-pt/error/index.ts @@ -33,6 +33,9 @@ const error = { terms_acceptance_required_description: 'Deves aceitar os termos para continuar. Por favor, tenta novamente.', something_went_wrong: 'Algo correu mal', + access_denied: 'Acesso negado', + application_access_denied: + 'Não tem permissão para aceder a esta aplicação. Por favor, contacte o seu administrador para obter ajuda.', feature_not_enabled: 'Não tem permissão para aceder a esta funcionalidade. Por favor, contacte o seu administrador para obter ajuda.', }; diff --git a/packages/phrases-experience/src/locales/ru/error/index.ts b/packages/phrases-experience/src/locales/ru/error/index.ts index 976c1ce2b085..88f2e986020f 100644 --- a/packages/phrases-experience/src/locales/ru/error/index.ts +++ b/packages/phrases-experience/src/locales/ru/error/index.ts @@ -33,6 +33,9 @@ const error = { terms_acceptance_required_description: 'Вы должны согласиться с условиями, чтобы продолжить. Пожалуйста, попробуйте снова.', something_went_wrong: 'Что-то пошло не так', + access_denied: 'Доступ запрещен', + application_access_denied: + 'У вас нет прав на доступ к этому приложению. Обратитесь к администратору за помощью.', feature_not_enabled: 'У вас нет прав на доступ к этой функции. Обратитесь к администратору за помощью.', }; diff --git a/packages/phrases-experience/src/locales/th/error/index.ts b/packages/phrases-experience/src/locales/th/error/index.ts index 1c470881092f..52c05a3fb875 100644 --- a/packages/phrases-experience/src/locales/th/error/index.ts +++ b/packages/phrases-experience/src/locales/th/error/index.ts @@ -30,6 +30,9 @@ const error = { terms_acceptance_required: 'จำเป็นต้องยอมรับเงื่อนไข', terms_acceptance_required_description: 'คุณต้องยอมรับเงื่อนไขเพื่อดำเนินการต่อ', something_went_wrong: 'เกิดข้อผิดพลาดบางอย่าง', + access_denied: 'ปฏิเสธการเข้าถึง', + application_access_denied: + 'คุณไม่มีสิทธิ์เข้าถึงแอปพลิเคชันนี้ กรุณาติดต่อผู้ดูแลระบบเพื่อขอความช่วยเหลือ', feature_not_enabled: 'คุณไม่มีสิทธิ์เข้าถึงฟีเจอร์นี้ กรุณาติดต่อผู้ดูแลระบบเพื่อขอความช่วยเหลือ', }; diff --git a/packages/phrases-experience/src/locales/tr-tr/error/index.ts b/packages/phrases-experience/src/locales/tr-tr/error/index.ts index e909496a577e..48eec3d02175 100644 --- a/packages/phrases-experience/src/locales/tr-tr/error/index.ts +++ b/packages/phrases-experience/src/locales/tr-tr/error/index.ts @@ -32,6 +32,9 @@ const error = { terms_acceptance_required: 'Şartların kabulü gerekli', terms_acceptance_required_description: 'Devam etmek için şartları kabul etmelisiniz.', something_went_wrong: 'Bir şeyler yanlış gitti', + access_denied: 'Erişim reddedildi', + application_access_denied: + 'Bu uygulamaya erişim izniniz yok. Yardım için lütfen yöneticinizle iletişime geçin.', feature_not_enabled: 'Bu özelliğe erişim izniniz yok. Yardım için lütfen yöneticinizle iletişime geçin.', }; diff --git a/packages/phrases-experience/src/locales/uk-ua/error/index.ts b/packages/phrases-experience/src/locales/uk-ua/error/index.ts index 67847d5c0b0d..ae7d6376d383 100644 --- a/packages/phrases-experience/src/locales/uk-ua/error/index.ts +++ b/packages/phrases-experience/src/locales/uk-ua/error/index.ts @@ -31,6 +31,9 @@ const error = { terms_acceptance_required_description: 'Ви повинні погодитися з умовами, щоб продовжити. Будь ласка, спробуйте ще раз.', something_went_wrong: 'Щось пішло не так', + access_denied: 'Доступ заборонено', + application_access_denied: + 'Ви не маєте дозволу на доступ до цього застосунку. Зверніться до адміністратора за допомогою.', feature_not_enabled: 'Ви не маєте дозволу на доступ до цієї функції. Зверніться до адміністратора за допомогою.', }; diff --git a/packages/phrases-experience/src/locales/zh-cn/error/index.ts b/packages/phrases-experience/src/locales/zh-cn/error/index.ts index 7fcb9cf7625c..e203abb38181 100644 --- a/packages/phrases-experience/src/locales/zh-cn/error/index.ts +++ b/packages/phrases-experience/src/locales/zh-cn/error/index.ts @@ -30,6 +30,8 @@ const error = { terms_acceptance_required: '需要同意条款', terms_acceptance_required_description: '必须同意条款后才能继续。', something_went_wrong: '出现错误', + access_denied: '访问被拒绝', + application_access_denied: '您没有权限访问此应用。请联系管理员寻求帮助。', feature_not_enabled: '您没有权限访问此功能。请联系管理员寻求帮助。', }; diff --git a/packages/phrases-experience/src/locales/zh-hk/error/index.ts b/packages/phrases-experience/src/locales/zh-hk/error/index.ts index bccb80efc08a..9e63dc1340d1 100644 --- a/packages/phrases-experience/src/locales/zh-hk/error/index.ts +++ b/packages/phrases-experience/src/locales/zh-hk/error/index.ts @@ -30,6 +30,8 @@ const error = { terms_acceptance_required: '需要同意條款', terms_acceptance_required_description: '必須同意條款後才能繼續。', something_went_wrong: '出錯了', + access_denied: '拒絕訪問', + application_access_denied: '您沒有權限訪問此應用。請聯繫管理員尋求幫助。', feature_not_enabled: '您沒有權限訪問此功能。請聯繫管理員尋求幫助。', }; diff --git a/packages/phrases-experience/src/locales/zh-tw/error/index.ts b/packages/phrases-experience/src/locales/zh-tw/error/index.ts index 818e4b2ca478..a1209c6ec8e3 100644 --- a/packages/phrases-experience/src/locales/zh-tw/error/index.ts +++ b/packages/phrases-experience/src/locales/zh-tw/error/index.ts @@ -30,6 +30,8 @@ const error = { terms_acceptance_required: '需要同意條款', terms_acceptance_required_description: '必須同意條款後才能繼續。', something_went_wrong: '出了些問題', + access_denied: '拒絕存取', + application_access_denied: '您沒有權限存取此應用程式。請聯繫管理員尋求協助。', feature_not_enabled: '您沒有權限存取此功能。請聯繫管理員尋求協助。', };