From 34105e1579589b0a428aaabc6b51dc020d0d8993 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Tue, 18 Jul 2023 17:05:00 +0800 Subject: [PATCH] feat(core,phrases): add quota guard middleware (#4153) feat(core,phrases): add usage guard middleware --- packages/core/package.json | 4 +- packages/core/src/__mocks__/subscription.ts | 25 +++++ .../core/src/libraries/cloud-connection.ts | 3 +- .../src/middleware/koa-quota-guard.test.ts | 97 +++++++++++++++++++ .../core/src/middleware/koa-quota-guard.ts | 75 ++++++++++++++ packages/core/src/queries/application.ts | 12 ++- packages/core/src/routes/application.ts | 4 +- .../core/src/test-utils/cloud-connection.ts | 21 ++++ packages/core/src/utils/subscription/index.ts | 20 ++++ packages/core/src/utils/subscription/types.ts | 12 +++ .../phrases/src/locales/de/errors/index.ts | 2 + .../src/locales/de/errors/subscription.ts | 6 ++ .../phrases/src/locales/en/errors/index.ts | 2 + .../src/locales/en/errors/subscription.ts | 6 ++ .../phrases/src/locales/es/errors/index.ts | 2 + .../src/locales/es/errors/subscription.ts | 6 ++ .../phrases/src/locales/fr/errors/index.ts | 2 + .../src/locales/fr/errors/subscription.ts | 6 ++ .../phrases/src/locales/it/errors/index.ts | 2 + .../src/locales/it/errors/subscription.ts | 6 ++ .../phrases/src/locales/ja/errors/index.ts | 2 + .../src/locales/ja/errors/subscription.ts | 6 ++ .../phrases/src/locales/ko/errors/index.ts | 2 + .../src/locales/ko/errors/subscription.ts | 6 ++ .../phrases/src/locales/pl-pl/errors/index.ts | 2 + .../src/locales/pl-pl/errors/subscription.ts | 6 ++ .../phrases/src/locales/pt-br/errors/index.ts | 2 + .../src/locales/pt-br/errors/subscription.ts | 6 ++ .../phrases/src/locales/pt-pt/errors/index.ts | 2 + .../src/locales/pt-pt/errors/subscription.ts | 6 ++ .../phrases/src/locales/ru/errors/index.ts | 2 + .../src/locales/ru/errors/subscription.ts | 6 ++ .../phrases/src/locales/tr-tr/errors/index.ts | 2 + .../src/locales/tr-tr/errors/subscription.ts | 6 ++ .../phrases/src/locales/zh-cn/errors/index.ts | 2 + .../src/locales/zh-cn/errors/subscription.ts | 6 ++ .../phrases/src/locales/zh-hk/errors/index.ts | 2 + .../src/locales/zh-hk/errors/subscription.ts | 6 ++ .../phrases/src/locales/zh-tw/errors/index.ts | 2 + .../src/locales/zh-tw/errors/subscription.ts | 6 ++ pnpm-lock.yaml | 33 ++----- 41 files changed, 395 insertions(+), 31 deletions(-) create mode 100644 packages/core/src/__mocks__/subscription.ts create mode 100644 packages/core/src/middleware/koa-quota-guard.test.ts create mode 100644 packages/core/src/middleware/koa-quota-guard.ts create mode 100644 packages/core/src/test-utils/cloud-connection.ts create mode 100644 packages/core/src/utils/subscription/index.ts create mode 100644 packages/core/src/utils/subscription/types.ts create mode 100644 packages/phrases/src/locales/de/errors/subscription.ts create mode 100644 packages/phrases/src/locales/en/errors/subscription.ts create mode 100644 packages/phrases/src/locales/es/errors/subscription.ts create mode 100644 packages/phrases/src/locales/fr/errors/subscription.ts create mode 100644 packages/phrases/src/locales/it/errors/subscription.ts create mode 100644 packages/phrases/src/locales/ja/errors/subscription.ts create mode 100644 packages/phrases/src/locales/ko/errors/subscription.ts create mode 100644 packages/phrases/src/locales/pl-pl/errors/subscription.ts create mode 100644 packages/phrases/src/locales/pt-br/errors/subscription.ts create mode 100644 packages/phrases/src/locales/pt-pt/errors/subscription.ts create mode 100644 packages/phrases/src/locales/ru/errors/subscription.ts create mode 100644 packages/phrases/src/locales/tr-tr/errors/subscription.ts create mode 100644 packages/phrases/src/locales/zh-cn/errors/subscription.ts create mode 100644 packages/phrases/src/locales/zh-hk/errors/subscription.ts create mode 100644 packages/phrases/src/locales/zh-tw/errors/subscription.ts diff --git a/packages/core/package.json b/packages/core/package.json index d54dc7f94..fca99aeb8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -41,7 +41,7 @@ "@logto/shared": "workspace:^2.0.0", "@logto/ui": "workspace:*", "@silverhand/essentials": "^2.5.0", - "@withtyped/client": "^0.7.17", + "@withtyped/client": "^0.7.19", "chalk": "^5.0.0", "clean-deep": "^3.4.0", "date-fns": "^2.29.3", @@ -81,7 +81,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@logto/cloud": "0.2.5-cbbfdc2", + "@logto/cloud": "0.2.5-4d5e389", "@silverhand/eslint-config": "4.0.1", "@silverhand/ts-config": "4.0.0", "@types/debug": "^4.1.7", diff --git a/packages/core/src/__mocks__/subscription.ts b/packages/core/src/__mocks__/subscription.ts new file mode 100644 index 000000000..b561ac01c --- /dev/null +++ b/packages/core/src/__mocks__/subscription.ts @@ -0,0 +1,25 @@ +import { type SubscriptionPlan } from '#src/utils/subscription/types.js'; + +export const mockFreePlan: SubscriptionPlan = { + id: 'free', + name: 'Free', + stripeProducts: [], + quota: { + mauLimit: 5000, + hooksLimit: 1, + rolesLimit: 1, + resourcesLimit: 3, + applicationsLimit: 3, + omniSignInEnabled: true, + scopesPerRoleLimit: 1, + customDomainEnabled: false, + machineToMachineLimit: 0, + socialConnectorsLimit: 3, + auditLogsRetentionDays: 3, + scopesPerResourceLimit: 1, + standardConnectorsLimit: 0, + builtInEmailConnectorEnabled: true, + }, + createdAt: new Date(), + updatedAt: new Date(), +}; diff --git a/packages/core/src/libraries/cloud-connection.ts b/packages/core/src/libraries/cloud-connection.ts index a50a857a4..20238d739 100644 --- a/packages/core/src/libraries/cloud-connection.ts +++ b/packages/core/src/libraries/cloud-connection.ts @@ -87,7 +87,8 @@ export class CloudConnectionLibrary { const { endpoint } = await this.getCloudConnectionData(); this.client = new Client({ - baseUrl: endpoint, + // TODO @sijie @darcy remove the 'api' appending in getCloudConnectionData() + baseUrl: endpoint.replace('/api', ''), headers: async () => { return { Authorization: `Bearer ${await this.getAccessToken()}` }; }, diff --git a/packages/core/src/middleware/koa-quota-guard.test.ts b/packages/core/src/middleware/koa-quota-guard.test.ts new file mode 100644 index 000000000..a6a3eed21 --- /dev/null +++ b/packages/core/src/middleware/koa-quota-guard.test.ts @@ -0,0 +1,97 @@ +import { GlobalValues } from '@logto/shared'; +import { createMockUtils } from '@logto/shared/esm'; +import { type Context } from 'koa'; + +import { mockFreePlan } from '#src/__mocks__/subscription.js'; +import { createMockCloudConnectionLibrary } from '#src/test-utils/cloud-connection.js'; +import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; +import { MockQueries } from '#src/test-utils/tenant.js'; + +const { jest } = import.meta; + +const { mockEsmWithActual } = createMockUtils(jest); + +const getValues = jest.fn(() => ({ + ...new GlobalValues(), + isCloud: true, +})); + +await mockEsmWithActual('#src/env-set/index.js', () => ({ + EnvSet: { + get values() { + return getValues(); + }, + }, +})); + +const { getTenantSubscriptionPlan } = await mockEsmWithActual( + '#src/utils/subscription/index.js', + () => ({ + getTenantSubscriptionPlan: jest.fn().mockResolvedValue(mockFreePlan), + }) +); + +const { default: koaQuotaGuard } = await import('./koa-quota-guard.js'); + +const createContext = (): Context => { + return createMockContext(); +}; + +const countNonM2MApplications = jest.fn(); +const queries = new MockQueries({ + applications: { countNonM2MApplications }, +}); + +const cloudConnection = createMockCloudConnectionLibrary(); + +describe('koaQuotaGuard() middleware', () => { + afterEach(() => { + getTenantSubscriptionPlan.mockClear(); + getValues.mockReturnValue({ + ...new GlobalValues(), + isCloud: true, + }); + }); + + it('should skip on non-cloud', async () => { + getValues.mockReturnValueOnce({ + ...new GlobalValues(), + isCloud: false, + }); + + const ctx = createContext(); + await koaQuotaGuard({ + key: 'applicationsLimit', + queries, + cloudConnection, + })(ctx, jest.fn()); + + expect(getTenantSubscriptionPlan).not.toHaveBeenCalled(); + }); + + it('should pass when limit is not exeeded', async () => { + countNonM2MApplications.mockResolvedValueOnce(0); + + const ctx = createContext(); + await expect( + koaQuotaGuard({ + key: 'applicationsLimit', + queries, + cloudConnection, + })(ctx, jest.fn()) + ).resolves.not.toThrow(); + }); + + it('should throw when limit is exeeded', async () => { + countNonM2MApplications.mockResolvedValueOnce(mockFreePlan.quota.applicationsLimit); + + const ctx = createContext(); + await expect( + koaQuotaGuard({ + key: 'applicationsLimit', + queries, + cloudConnection, + })(ctx, jest.fn()) + ).rejects.toThrow(); + }); +}); diff --git a/packages/core/src/middleware/koa-quota-guard.ts b/packages/core/src/middleware/koa-quota-guard.ts new file mode 100644 index 000000000..6a74ce23b --- /dev/null +++ b/packages/core/src/middleware/koa-quota-guard.ts @@ -0,0 +1,75 @@ +import type { MiddlewareType } from 'koa'; + +import { EnvSet } from '#src/env-set/index.js'; +import RequestError from '#src/errors/RequestError/index.js'; +import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'; +import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; +import { getTenantSubscriptionPlan } from '#src/utils/subscription/index.js'; +import { type FeatureQuota } from '#src/utils/subscription/types.js'; + +type UsageGuardConfig = { + key: keyof FeatureQuota; + cloudConnection: CloudConnectionLibrary; + queries: Queries; +}; + +const getTenantUsage = async (key: keyof FeatureQuota, queries: Queries): Promise => { + if (key === 'applicationsLimit') { + return queries.applications.countNonM2MApplications(); + } + + // TODO: add other keys + + throw new Error('Unsupported subscription quota key'); +}; + +export default function koaQuotaGuard({ + key, + queries, + cloudConnection, +}: UsageGuardConfig): MiddlewareType { + return async (ctx, next) => { + const { isCloud, isIntegrationTest, isProduction } = EnvSet.values; + + // Disable in production until pricing is ready + if (!isCloud || isIntegrationTest || isProduction) { + return next(); + } + + const plan = await getTenantSubscriptionPlan(cloudConnection); + const limit = plan.quota[key]; + + if (typeof limit === 'boolean') { + assertThat( + limit, + new RequestError({ + code: 'subscription.limit_exceeded', + status: 403, + data: { + key, + }, + }) + ); + } else if (typeof limit === 'number') { + const tenantUsage = await getTenantUsage(key, queries); + + assertThat( + tenantUsage < limit, + new RequestError({ + code: 'subscription.limit_exceeded', + status: 403, + data: { + key, + limit, + usage: tenantUsage, + }, + }) + ); + } else { + throw new TypeError('Unsupported subscription quota type'); + } + + return next(); + }; +} diff --git a/packages/core/src/queries/application.ts b/packages/core/src/queries/application.ts index 5032bb820..c8d5b9f89 100644 --- a/packages/core/src/queries/application.ts +++ b/packages/core/src/queries/application.ts @@ -1,5 +1,5 @@ import type { CreateApplication } from '@logto/schemas'; -import { Applications } from '@logto/schemas'; +import { ApplicationType, Applications } from '@logto/schemas'; import type { OmitAutoSetFields } from '@logto/shared'; import { convertToIdentifiers } from '@logto/shared'; import type { CommonQueryMethods } from 'slonik'; @@ -28,6 +28,15 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => { id: string, set: Partial> ) => updateApplication({ set, where: { id }, jsonbMode: 'merge' }); + const countNonM2MApplications = async () => { + const { count } = await pool.one<{ count: string }>(sql` + select count(*) + from ${table} + where ${fields.type} != ${ApplicationType.MachineToMachine} + `); + + return Number(count); + }; const deleteApplicationById = async (id: string) => { const { rowCount } = await pool.query(sql` @@ -47,6 +56,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => { insertApplication, updateApplication, updateApplicationById, + countNonM2MApplications, deleteApplicationById, }; }; diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index fc709732d..8f7dbee4e 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -11,6 +11,7 @@ import { boolean, object, string, z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import koaPagination from '#src/middleware/koa-pagination.js'; +import koaQuotaGuard from '#src/middleware/koa-quota-guard.js'; import { buildOidcClientMetadata } from '#src/oidc/utils.js'; import assertThat from '#src/utils/assert-that.js'; @@ -21,7 +22,7 @@ const includesInternalAdminRole = (roles: Readonly>) => roles.some(({ role: { name } }) => name === InternalRole.Admin); export default function applicationRoutes( - ...[router, { queries, id: tenantId }]: RouterInitArgs + ...[router, { queries, id: tenantId, cloudConnection }]: RouterInitArgs ) { const { deleteApplicationById, @@ -57,6 +58,7 @@ export default function applicationRoutes( router.post( '/applications', + koaQuotaGuard({ key: 'applicationsLimit', cloudConnection, queries }), koaGuard({ body: Applications.createGuard .omit({ id: true, createdAt: true }) diff --git a/packages/core/src/test-utils/cloud-connection.ts b/packages/core/src/test-utils/cloud-connection.ts new file mode 100644 index 000000000..bd9775f39 --- /dev/null +++ b/packages/core/src/test-utils/cloud-connection.ts @@ -0,0 +1,21 @@ +import { mockGetCloudConnectionData } from '#src/__mocks__/cloud-connection.js'; +import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'; + +const { jest } = import.meta; + +type PublicPart = { [K in keyof T]: T[K] }; + +export const createMockCloudConnectionLibrary = (): CloudConnectionLibrary => { + class MockLibrary implements PublicPart { + public getCloudConnectionData = mockGetCloudConnectionData; + + public getAccessToken = jest.fn(); + + public getClient = jest.fn(); + } + + const library = new MockLibrary(); + + // eslint-disable-next-line no-restricted-syntax + return library as unknown as CloudConnectionLibrary; +}; diff --git a/packages/core/src/utils/subscription/index.ts b/packages/core/src/utils/subscription/index.ts new file mode 100644 index 000000000..884abb553 --- /dev/null +++ b/packages/core/src/utils/subscription/index.ts @@ -0,0 +1,20 @@ +import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'; + +import assertThat from '../assert-that.js'; + +import { type SubscriptionPlan } from './types.js'; + +export const getTenantSubscriptionPlan = async ( + cloudConnection: CloudConnectionLibrary +): Promise => { + const client = await cloudConnection.getClient(); + const [subscription, plans] = await Promise.all([ + client.get('/api/tenants/my/subscription'), + client.get('/api/subscription-plans'), + ]); + const plan = plans.find(({ id }) => id === subscription.planId); + + assertThat(plan, 'subscription.get_plan_failed'); + + return plan; +}; diff --git a/packages/core/src/utils/subscription/types.ts b/packages/core/src/utils/subscription/types.ts new file mode 100644 index 000000000..6f7a29016 --- /dev/null +++ b/packages/core/src/utils/subscription/types.ts @@ -0,0 +1,12 @@ +import type router from '@logto/cloud/routes'; +import { type RouterRoutes } from '@withtyped/client'; +import { type z, type ZodType } from 'zod'; + +type GetRoutes = RouterRoutes['get']; + +type RouteResponseType = + z.infer>; + +export type SubscriptionPlan = RouteResponseType[number]; + +export type FeatureQuota = Omit; diff --git a/packages/phrases/src/locales/de/errors/index.ts b/packages/phrases/src/locales/de/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/de/errors/index.ts +++ b/packages/phrases/src/locales/de/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/de/errors/subscription.ts b/packages/phrases/src/locales/de/errors/subscription.ts new file mode 100644 index 000000000..39f623a5d --- /dev/null +++ b/packages/phrases/src/locales/de/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Sie haben das Limit Ihres Abonnementplans erreicht.', + get_plan_failed: 'Fehler beim Abrufen des Abonnementplans für den Mandanten.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/en/errors/index.ts b/packages/phrases/src/locales/en/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/en/errors/index.ts +++ b/packages/phrases/src/locales/en/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/en/errors/subscription.ts b/packages/phrases/src/locales/en/errors/subscription.ts new file mode 100644 index 000000000..0085a0bb0 --- /dev/null +++ b/packages/phrases/src/locales/en/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'You have reached the limit of your subscription plan.', + get_plan_failed: 'Unable to get subscription plan for tenant.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/es/errors/index.ts b/packages/phrases/src/locales/es/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/es/errors/index.ts +++ b/packages/phrases/src/locales/es/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/es/errors/subscription.ts b/packages/phrases/src/locales/es/errors/subscription.ts new file mode 100644 index 000000000..a88a4aa28 --- /dev/null +++ b/packages/phrases/src/locales/es/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Has alcanzado el límite de tu plan de suscripción.', + get_plan_failed: 'No se pudo obtener el plan de suscripción para el inquilino.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/fr/errors/index.ts b/packages/phrases/src/locales/fr/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/fr/errors/index.ts +++ b/packages/phrases/src/locales/fr/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/fr/errors/subscription.ts b/packages/phrases/src/locales/fr/errors/subscription.ts new file mode 100644 index 000000000..bd91b05b1 --- /dev/null +++ b/packages/phrases/src/locales/fr/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: "Vous avez atteint la limite de votre plan d'abonnement.", + get_plan_failed: "Échec de l'obtention du plan d'abonnement pour le locataire.", +}; + +export default subscription; diff --git a/packages/phrases/src/locales/it/errors/index.ts b/packages/phrases/src/locales/it/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/it/errors/index.ts +++ b/packages/phrases/src/locales/it/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/it/errors/subscription.ts b/packages/phrases/src/locales/it/errors/subscription.ts new file mode 100644 index 000000000..2025695a7 --- /dev/null +++ b/packages/phrases/src/locales/it/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Hai raggiunto il limite del tuo piano di abbonamento.', + get_plan_failed: "Impossibile ottenere il piano di abbonamento per l'inquilino.", +}; + +export default subscription; diff --git a/packages/phrases/src/locales/ja/errors/index.ts b/packages/phrases/src/locales/ja/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/ja/errors/index.ts +++ b/packages/phrases/src/locales/ja/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/ja/errors/subscription.ts b/packages/phrases/src/locales/ja/errors/subscription.ts new file mode 100644 index 000000000..8fea3506a --- /dev/null +++ b/packages/phrases/src/locales/ja/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: '定めた定期プランの上限に達しました。', + get_plan_failed: 'テナントのサブスクリプションプランを取得できませんでした。', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/ko/errors/index.ts b/packages/phrases/src/locales/ko/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/ko/errors/index.ts +++ b/packages/phrases/src/locales/ko/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/ko/errors/subscription.ts b/packages/phrases/src/locales/ko/errors/subscription.ts new file mode 100644 index 000000000..916c62a01 --- /dev/null +++ b/packages/phrases/src/locales/ko/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: '당신은 구독 플랜 한도에 도달하였습니다.', + get_plan_failed: '임차인에 대한 구독 플랜을 가져올 수 없습니다.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/pl-pl/errors/index.ts b/packages/phrases/src/locales/pl-pl/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/pl-pl/errors/index.ts +++ b/packages/phrases/src/locales/pl-pl/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/pl-pl/errors/subscription.ts b/packages/phrases/src/locales/pl-pl/errors/subscription.ts new file mode 100644 index 000000000..617853204 --- /dev/null +++ b/packages/phrases/src/locales/pl-pl/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Osiągnąłeś limit swojego planu subskrypcji.', + get_plan_failed: 'Nie można pobrać planu subskrypcji dla najemcy.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/pt-br/errors/index.ts b/packages/phrases/src/locales/pt-br/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/pt-br/errors/index.ts +++ b/packages/phrases/src/locales/pt-br/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/pt-br/errors/subscription.ts b/packages/phrases/src/locales/pt-br/errors/subscription.ts new file mode 100644 index 000000000..db122e24e --- /dev/null +++ b/packages/phrases/src/locales/pt-br/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Você atingiu o limite do seu plano de assinatura.', + get_plan_failed: 'Não foi possível obter o plano de assinatura para o inquilino.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/pt-pt/errors/index.ts b/packages/phrases/src/locales/pt-pt/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/pt-pt/errors/index.ts +++ b/packages/phrases/src/locales/pt-pt/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/pt-pt/errors/subscription.ts b/packages/phrases/src/locales/pt-pt/errors/subscription.ts new file mode 100644 index 000000000..9047d7f11 --- /dev/null +++ b/packages/phrases/src/locales/pt-pt/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Você atingiu o limite do seu plano de assinatura.', + get_plan_failed: 'Não foi possível obter o plano de assinatura para o inquilino.', // UNTRANSLATED +}; + +export default subscription; diff --git a/packages/phrases/src/locales/ru/errors/index.ts b/packages/phrases/src/locales/ru/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/ru/errors/index.ts +++ b/packages/phrases/src/locales/ru/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/ru/errors/subscription.ts b/packages/phrases/src/locales/ru/errors/subscription.ts new file mode 100644 index 000000000..c483c01a3 --- /dev/null +++ b/packages/phrases/src/locales/ru/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Вы достигли лимита вашего плана подписки.', + get_plan_failed: 'Не удалось получить план подписки для арендатора.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/tr-tr/errors/index.ts b/packages/phrases/src/locales/tr-tr/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/tr-tr/errors/index.ts +++ b/packages/phrases/src/locales/tr-tr/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/tr-tr/errors/subscription.ts b/packages/phrases/src/locales/tr-tr/errors/subscription.ts new file mode 100644 index 000000000..7bc7c8891 --- /dev/null +++ b/packages/phrases/src/locales/tr-tr/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: 'Abonelik planınızın limitine ulaştınız.', + get_plan_failed: 'Abonelik planınızı almak için başarısız oldu.', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/zh-cn/errors/index.ts b/packages/phrases/src/locales/zh-cn/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/zh-cn/errors/index.ts +++ b/packages/phrases/src/locales/zh-cn/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/zh-cn/errors/subscription.ts b/packages/phrases/src/locales/zh-cn/errors/subscription.ts new file mode 100644 index 000000000..67ebfa998 --- /dev/null +++ b/packages/phrases/src/locales/zh-cn/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: '您已达到订阅计划的限制。', + get_plan_failed: '无法获取租户的订阅计划。', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/zh-hk/errors/index.ts b/packages/phrases/src/locales/zh-hk/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/zh-hk/errors/index.ts +++ b/packages/phrases/src/locales/zh-hk/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/zh-hk/errors/subscription.ts b/packages/phrases/src/locales/zh-hk/errors/subscription.ts new file mode 100644 index 000000000..d460ae973 --- /dev/null +++ b/packages/phrases/src/locales/zh-hk/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: '您已達到訂閱計劃的限制。', + get_plan_failed: '無法取得租戶的訂閱計劃。', +}; + +export default subscription; diff --git a/packages/phrases/src/locales/zh-tw/errors/index.ts b/packages/phrases/src/locales/zh-tw/errors/index.ts index 02b5432dd..81f69cc5f 100644 --- a/packages/phrases/src/locales/zh-tw/errors/index.ts +++ b/packages/phrases/src/locales/zh-tw/errors/index.ts @@ -15,6 +15,7 @@ import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; import storage from './storage.js'; +import subscription from './subscription.js'; import swagger from './swagger.js'; import user from './user.js'; import verification_code from './verification-code.js'; @@ -40,6 +41,7 @@ const errors = { resource, hook, domain, + subscription, }; export default errors; diff --git a/packages/phrases/src/locales/zh-tw/errors/subscription.ts b/packages/phrases/src/locales/zh-tw/errors/subscription.ts new file mode 100644 index 000000000..181f76ed8 --- /dev/null +++ b/packages/phrases/src/locales/zh-tw/errors/subscription.ts @@ -0,0 +1,6 @@ +const subscription = { + limit_exceeded: '您已達到訂閱計劃的限制。', + get_plan_failed: '無法為租戶獲取訂閱計劃。', +}; + +export default subscription; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9891afad..23a482ec4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3079,8 +3079,8 @@ importers: specifier: ^2.5.0 version: 2.5.0 '@withtyped/client': - specifier: ^0.7.17 - version: 0.7.17(zod@3.20.2) + specifier: ^0.7.19 + version: 0.7.19(zod@3.20.2) chalk: specifier: ^5.0.0 version: 5.1.2 @@ -3194,8 +3194,8 @@ importers: version: 3.20.2 devDependencies: '@logto/cloud': - specifier: 0.2.5-cbbfdc2 - version: 0.2.5-cbbfdc2(zod@3.20.2) + specifier: 0.2.5-4d5e389 + version: 0.2.5-4d5e389(zod@3.20.2) '@silverhand/eslint-config': specifier: 4.0.1 version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2) @@ -7220,12 +7220,12 @@ packages: - zod dev: true - /@logto/cloud@0.2.5-cbbfdc2(zod@3.20.2): - resolution: {integrity: sha512-qul8lxkbAczEHJSyramQJW3BaNAdJQHlryIzTAHpcWpNGZogP3aacJuF0neBnnn8mJqAWy1rIGAGnb4d16fEFQ==} + /@logto/cloud@0.2.5-4f1d80b(zod@3.20.2): + resolution: {integrity: sha512-AF0YnJiXDMS3HQ2ugZcwJRBkspvNtlXk992IwTNFZxbZdinpPoVmPnDnHuekACcQY5bFRgHjMB2/o/GKkdLpWg==} engines: {node: ^18.12.0} dependencies: '@silverhand/essentials': 2.7.0 - '@withtyped/server': 0.12.5(zod@3.20.2) + '@withtyped/server': 0.12.7(zod@3.20.2) transitivePeerDependencies: - zod dev: true @@ -9848,15 +9848,6 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@withtyped/client@0.7.17(zod@3.20.2): - resolution: {integrity: sha512-D8kwJBKryALjNcjHRLyARRTVnlGU+iUwJ1AdDsoSv+Sum17GDxi0s4F5DYh97bfSmdn/K4SGFBI2e0MdHPP+mg==} - dependencies: - '@withtyped/server': 0.12.5(zod@3.20.2) - '@withtyped/shared': 0.2.2 - transitivePeerDependencies: - - zod - dev: false - /@withtyped/client@0.7.19(zod@3.20.2): resolution: {integrity: sha512-cHmqCIsEMonrE9kDbu8YAPWWPHmy6imUGbUWQ2PGaPLTJagbgH3ABsxgnLiM0lm0LeuZFd3BHh+JYKRDUmS8cw==} dependencies: @@ -9864,16 +9855,6 @@ packages: '@withtyped/shared': 0.2.2 transitivePeerDependencies: - zod - dev: true - - /@withtyped/server@0.12.5(zod@3.20.2): - resolution: {integrity: sha512-mKDPGCJzh0xna4Vi2zVsHg/ZfMNQHDV3+jvsral2PJdRzI8qU7au7I/ytU6Vr6BU1fFESkNrIuUpCmyfBAwX7g==} - peerDependencies: - zod: ^3.19.1 - dependencies: - '@silverhand/essentials': 2.7.0 - '@withtyped/shared': 0.2.2 - zod: 3.20.2 /@withtyped/server@0.12.7(zod@3.20.2): resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==}