mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core,phrases): add quota guard middleware (#4153)
feat(core,phrases): add usage guard middleware
This commit is contained in:
parent
e2fc6cb545
commit
34105e1579
41 changed files with 395 additions and 31 deletions
|
@ -41,7 +41,7 @@
|
||||||
"@logto/shared": "workspace:^2.0.0",
|
"@logto/shared": "workspace:^2.0.0",
|
||||||
"@logto/ui": "workspace:*",
|
"@logto/ui": "workspace:*",
|
||||||
"@silverhand/essentials": "^2.5.0",
|
"@silverhand/essentials": "^2.5.0",
|
||||||
"@withtyped/client": "^0.7.17",
|
"@withtyped/client": "^0.7.19",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
"clean-deep": "^3.4.0",
|
"clean-deep": "^3.4.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@logto/cloud": "0.2.5-cbbfdc2",
|
"@logto/cloud": "0.2.5-4d5e389",
|
||||||
"@silverhand/eslint-config": "4.0.1",
|
"@silverhand/eslint-config": "4.0.1",
|
||||||
"@silverhand/ts-config": "4.0.0",
|
"@silverhand/ts-config": "4.0.0",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
|
|
25
packages/core/src/__mocks__/subscription.ts
Normal file
25
packages/core/src/__mocks__/subscription.ts
Normal file
|
@ -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(),
|
||||||
|
};
|
|
@ -87,7 +87,8 @@ export class CloudConnectionLibrary {
|
||||||
const { endpoint } = await this.getCloudConnectionData();
|
const { endpoint } = await this.getCloudConnectionData();
|
||||||
|
|
||||||
this.client = new Client<typeof router>({
|
this.client = new Client<typeof router>({
|
||||||
baseUrl: endpoint,
|
// TODO @sijie @darcy remove the 'api' appending in getCloudConnectionData()
|
||||||
|
baseUrl: endpoint.replace('/api', ''),
|
||||||
headers: async () => {
|
headers: async () => {
|
||||||
return { Authorization: `Bearer ${await this.getAccessToken()}` };
|
return { Authorization: `Bearer ${await this.getAccessToken()}` };
|
||||||
},
|
},
|
||||||
|
|
97
packages/core/src/middleware/koa-quota-guard.test.ts
Normal file
97
packages/core/src/middleware/koa-quota-guard.test.ts
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
75
packages/core/src/middleware/koa-quota-guard.ts
Normal file
75
packages/core/src/middleware/koa-quota-guard.ts
Normal file
|
@ -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<number> => {
|
||||||
|
if (key === 'applicationsLimit') {
|
||||||
|
return queries.applications.countNonM2MApplications();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add other keys
|
||||||
|
|
||||||
|
throw new Error('Unsupported subscription quota key');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function koaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
||||||
|
key,
|
||||||
|
queries,
|
||||||
|
cloudConnection,
|
||||||
|
}: UsageGuardConfig): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import type { CreateApplication } from '@logto/schemas';
|
import type { CreateApplication } from '@logto/schemas';
|
||||||
import { Applications } from '@logto/schemas';
|
import { ApplicationType, Applications } from '@logto/schemas';
|
||||||
import type { OmitAutoSetFields } from '@logto/shared';
|
import type { OmitAutoSetFields } from '@logto/shared';
|
||||||
import { convertToIdentifiers } from '@logto/shared';
|
import { convertToIdentifiers } from '@logto/shared';
|
||||||
import type { CommonQueryMethods } from 'slonik';
|
import type { CommonQueryMethods } from 'slonik';
|
||||||
|
@ -28,6 +28,15 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
||||||
id: string,
|
id: string,
|
||||||
set: Partial<OmitAutoSetFields<CreateApplication>>
|
set: Partial<OmitAutoSetFields<CreateApplication>>
|
||||||
) => updateApplication({ set, where: { id }, jsonbMode: 'merge' });
|
) => 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 deleteApplicationById = async (id: string) => {
|
||||||
const { rowCount } = await pool.query(sql`
|
const { rowCount } = await pool.query(sql`
|
||||||
|
@ -47,6 +56,7 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => {
|
||||||
insertApplication,
|
insertApplication,
|
||||||
updateApplication,
|
updateApplication,
|
||||||
updateApplicationById,
|
updateApplicationById,
|
||||||
|
countNonM2MApplications,
|
||||||
deleteApplicationById,
|
deleteApplicationById,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { boolean, object, string, z } from 'zod';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import koaPagination from '#src/middleware/koa-pagination.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 { buildOidcClientMetadata } from '#src/oidc/utils.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ const includesInternalAdminRole = (roles: Readonly<Array<{ role: Role }>>) =>
|
||||||
roles.some(({ role: { name } }) => name === InternalRole.Admin);
|
roles.some(({ role: { name } }) => name === InternalRole.Admin);
|
||||||
|
|
||||||
export default function applicationRoutes<T extends AuthedRouter>(
|
export default function applicationRoutes<T extends AuthedRouter>(
|
||||||
...[router, { queries, id: tenantId }]: RouterInitArgs<T>
|
...[router, { queries, id: tenantId, cloudConnection }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
deleteApplicationById,
|
deleteApplicationById,
|
||||||
|
@ -57,6 +58,7 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/applications',
|
'/applications',
|
||||||
|
koaQuotaGuard({ key: 'applicationsLimit', cloudConnection, queries }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: Applications.createGuard
|
body: Applications.createGuard
|
||||||
.omit({ id: true, createdAt: true })
|
.omit({ id: true, createdAt: true })
|
||||||
|
|
21
packages/core/src/test-utils/cloud-connection.ts
Normal file
21
packages/core/src/test-utils/cloud-connection.ts
Normal file
|
@ -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<T> = { [K in keyof T]: T[K] };
|
||||||
|
|
||||||
|
export const createMockCloudConnectionLibrary = (): CloudConnectionLibrary => {
|
||||||
|
class MockLibrary implements PublicPart<CloudConnectionLibrary> {
|
||||||
|
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;
|
||||||
|
};
|
20
packages/core/src/utils/subscription/index.ts
Normal file
20
packages/core/src/utils/subscription/index.ts
Normal file
|
@ -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<SubscriptionPlan> => {
|
||||||
|
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;
|
||||||
|
};
|
12
packages/core/src/utils/subscription/types.ts
Normal file
12
packages/core/src/utils/subscription/types.ts
Normal file
|
@ -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<typeof router>['get'];
|
||||||
|
|
||||||
|
type RouteResponseType<T extends { search?: unknown; body?: unknown; response?: ZodType }> =
|
||||||
|
z.infer<NonNullable<T['response']>>;
|
||||||
|
|
||||||
|
export type SubscriptionPlan = RouteResponseType<GetRoutes['/api/subscription-plans']>[number];
|
||||||
|
|
||||||
|
export type FeatureQuota = Omit<SubscriptionPlan['quota'], 'tenantLimit' | 'mauLimit'>;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/de/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/de/errors/subscription.ts
Normal file
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/en/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/en/errors/subscription.ts
Normal file
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/es/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/es/errors/subscription.ts
Normal file
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/fr/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/fr/errors/subscription.ts
Normal file
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/it/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/it/errors/subscription.ts
Normal file
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/ja/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/ja/errors/subscription.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const subscription = {
|
||||||
|
limit_exceeded: '定めた定期プランの上限に達しました。',
|
||||||
|
get_plan_failed: 'テナントのサブスクリプションプランを取得できませんでした。',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default subscription;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/ko/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/ko/errors/subscription.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const subscription = {
|
||||||
|
limit_exceeded: '당신은 구독 플랜 한도에 도달하였습니다.',
|
||||||
|
get_plan_failed: '임차인에 대한 구독 플랜을 가져올 수 없습니다.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default subscription;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
6
packages/phrases/src/locales/ru/errors/subscription.ts
Normal file
6
packages/phrases/src/locales/ru/errors/subscription.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const subscription = {
|
||||||
|
limit_exceeded: 'Вы достигли лимита вашего плана подписки.',
|
||||||
|
get_plan_failed: 'Не удалось получить план подписки для арендатора.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default subscription;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -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;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
const subscription = {
|
||||||
|
limit_exceeded: '您已达到订阅计划的限制。',
|
||||||
|
get_plan_failed: '无法获取租户的订阅计划。',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default subscription;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
const subscription = {
|
||||||
|
limit_exceeded: '您已達到訂閱計劃的限制。',
|
||||||
|
get_plan_failed: '無法取得租戶的訂閱計劃。',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default subscription;
|
|
@ -15,6 +15,7 @@ import scope from './scope.js';
|
||||||
import session from './session.js';
|
import session from './session.js';
|
||||||
import sign_in_experiences from './sign-in-experiences.js';
|
import sign_in_experiences from './sign-in-experiences.js';
|
||||||
import storage from './storage.js';
|
import storage from './storage.js';
|
||||||
|
import subscription from './subscription.js';
|
||||||
import swagger from './swagger.js';
|
import swagger from './swagger.js';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import verification_code from './verification-code.js';
|
import verification_code from './verification-code.js';
|
||||||
|
@ -40,6 +41,7 @@ const errors = {
|
||||||
resource,
|
resource,
|
||||||
hook,
|
hook,
|
||||||
domain,
|
domain,
|
||||||
|
subscription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default errors;
|
export default errors;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
const subscription = {
|
||||||
|
limit_exceeded: '您已達到訂閱計劃的限制。',
|
||||||
|
get_plan_failed: '無法為租戶獲取訂閱計劃。',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default subscription;
|
|
@ -3079,8 +3079,8 @@ importers:
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
'@withtyped/client':
|
'@withtyped/client':
|
||||||
specifier: ^0.7.17
|
specifier: ^0.7.19
|
||||||
version: 0.7.17(zod@3.20.2)
|
version: 0.7.19(zod@3.20.2)
|
||||||
chalk:
|
chalk:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
|
@ -3194,8 +3194,8 @@ importers:
|
||||||
version: 3.20.2
|
version: 3.20.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-cbbfdc2
|
specifier: 0.2.5-4d5e389
|
||||||
version: 0.2.5-cbbfdc2(zod@3.20.2)
|
version: 0.2.5-4d5e389(zod@3.20.2)
|
||||||
'@silverhand/eslint-config':
|
'@silverhand/eslint-config':
|
||||||
specifier: 4.0.1
|
specifier: 4.0.1
|
||||||
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
version: 4.0.1(eslint@8.44.0)(prettier@3.0.0)(typescript@5.0.2)
|
||||||
|
@ -7220,12 +7220,12 @@ packages:
|
||||||
- zod
|
- zod
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@logto/cloud@0.2.5-cbbfdc2(zod@3.20.2):
|
/@logto/cloud@0.2.5-4f1d80b(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-qul8lxkbAczEHJSyramQJW3BaNAdJQHlryIzTAHpcWpNGZogP3aacJuF0neBnnn8mJqAWy1rIGAGnb4d16fEFQ==}
|
resolution: {integrity: sha512-AF0YnJiXDMS3HQ2ugZcwJRBkspvNtlXk992IwTNFZxbZdinpPoVmPnDnHuekACcQY5bFRgHjMB2/o/GKkdLpWg==}
|
||||||
engines: {node: ^18.12.0}
|
engines: {node: ^18.12.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@silverhand/essentials': 2.7.0
|
'@silverhand/essentials': 2.7.0
|
||||||
'@withtyped/server': 0.12.5(zod@3.20.2)
|
'@withtyped/server': 0.12.7(zod@3.20.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -9848,15 +9848,6 @@ packages:
|
||||||
eslint-visitor-keys: 3.4.1
|
eslint-visitor-keys: 3.4.1
|
||||||
dev: true
|
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):
|
/@withtyped/client@0.7.19(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-cHmqCIsEMonrE9kDbu8YAPWWPHmy6imUGbUWQ2PGaPLTJagbgH3ABsxgnLiM0lm0LeuZFd3BHh+JYKRDUmS8cw==}
|
resolution: {integrity: sha512-cHmqCIsEMonrE9kDbu8YAPWWPHmy6imUGbUWQ2PGaPLTJagbgH3ABsxgnLiM0lm0LeuZFd3BHh+JYKRDUmS8cw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -9864,16 +9855,6 @@ packages:
|
||||||
'@withtyped/shared': 0.2.2
|
'@withtyped/shared': 0.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- 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):
|
/@withtyped/server@0.12.7(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==}
|
resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==}
|
||||||
|
|
Loading…
Reference in a new issue