mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(core,connector): update koaQuotaGuard to fit new pricing model (#5123)
This commit is contained in:
parent
e28822997f
commit
22e9580d68
8 changed files with 44 additions and 28 deletions
|
@ -49,6 +49,6 @@
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@logto/cloud": "0.2.5-5a698db"
|
"@logto/cloud": "0.2.5-5deb133"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@logto/cloud": "0.2.5-5a698db",
|
"@logto/cloud": "0.2.5-5deb133",
|
||||||
"@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",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ConnectorType, DemoConnector } from '@logto/connector-kit';
|
import { ConnectorType, DemoConnector } from '@logto/connector-kit';
|
||||||
|
import { RoleType, ReservedPlanId } from '@logto/schemas';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
@ -44,7 +45,9 @@ export const createQuotaLibrary = (
|
||||||
// Ignore the default management API resource
|
// Ignore the default management API resource
|
||||||
return { count: count - 1 };
|
return { count: count - 1 };
|
||||||
},
|
},
|
||||||
rolesLimit: async () => countRoles(),
|
rolesLimit: async () => countRoles(undefined, { type: RoleType.User }),
|
||||||
|
machineToMachineRolesLimit: async () =>
|
||||||
|
countRoles(undefined, { type: RoleType.MachineToMachine }),
|
||||||
scopesPerResourceLimit: async (queryKey) => {
|
scopesPerResourceLimit: async (queryKey) => {
|
||||||
assertThat(queryKey, new TypeError('queryKey for scopesPerResourceLimit is required'));
|
assertThat(queryKey, new TypeError('queryKey for scopesPerResourceLimit is required'));
|
||||||
return countScopesByResourceId(queryKey);
|
return countScopesByResourceId(queryKey);
|
||||||
|
@ -61,13 +64,6 @@ export const createQuotaLibrary = (
|
||||||
).length;
|
).length;
|
||||||
return { count };
|
return { count };
|
||||||
},
|
},
|
||||||
standardConnectorsLimit: async () => {
|
|
||||||
const connectors = await getLogtoConnectors();
|
|
||||||
const count = connectors.filter(
|
|
||||||
({ metadata: { isStandard, id } }) => isStandard && id !== DemoConnector.Social
|
|
||||||
).length;
|
|
||||||
return { count };
|
|
||||||
},
|
|
||||||
customDomainEnabled: notNumber,
|
customDomainEnabled: notNumber,
|
||||||
mfaEnabled: notNumber,
|
mfaEnabled: notNumber,
|
||||||
organizationsEnabled: notNumber,
|
organizationsEnabled: notNumber,
|
||||||
|
@ -96,8 +92,9 @@ export const createQuotaLibrary = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = await getTenantSubscriptionPlan(cloudConnection);
|
const { id: planId, quota } = await getTenantSubscriptionPlan(cloudConnection);
|
||||||
const limit = plan.quota[key];
|
// Only apply hard quota limit for free plan, o/w it's soft limit (use `null` to bypass quota check for soft limit cases).
|
||||||
|
const limit = planId === ReservedPlanId.Free ? quota[key] : null;
|
||||||
|
|
||||||
if (limit === null) {
|
if (limit === null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -129,11 +129,11 @@ export default function applicationRoutes<T extends AuthedRouter>(
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { oidcClientMetadata, ...rest } = ctx.guard.body;
|
const { oidcClientMetadata, ...rest } = ctx.guard.body;
|
||||||
|
|
||||||
await quota.guardKey(
|
// When creating a m2m app, should check both m2m limit and application limit.
|
||||||
rest.type === ApplicationType.MachineToMachine
|
if (rest.type === ApplicationType.MachineToMachine) {
|
||||||
? 'machineToMachineLimit'
|
await quota.guardKey('machineToMachineLimit');
|
||||||
: 'applicationsLimit'
|
}
|
||||||
);
|
await quota.guardKey('applicationsLimit');
|
||||||
|
|
||||||
// Third party applications must be traditional type
|
// Third party applications must be traditional type
|
||||||
if (rest.isThirdParty) {
|
if (rest.isThirdParty) {
|
||||||
|
|
|
@ -25,9 +25,6 @@ const guardConnectorsQuota = async (
|
||||||
factory: ConnectorFactory<typeof router>,
|
factory: ConnectorFactory<typeof router>,
|
||||||
quota: QuotaLibrary
|
quota: QuotaLibrary
|
||||||
) => {
|
) => {
|
||||||
if (factory.metadata.isStandard) {
|
|
||||||
await quota.guardKey('standardConnectorsLimit');
|
|
||||||
}
|
|
||||||
if (factory.type === ConnectorType.Social) {
|
if (factory.type === ConnectorType.Social) {
|
||||||
await quota.guardKey('socialConnectorsLimit');
|
await quota.guardKey('socialConnectorsLimit');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { RoleResponse } from '@logto/schemas';
|
import type { RoleResponse } from '@logto/schemas';
|
||||||
import { Roles, featuredApplicationGuard, featuredUserGuard } from '@logto/schemas';
|
import { RoleType, Roles, featuredApplicationGuard, featuredUserGuard } from '@logto/schemas';
|
||||||
import { generateStandardId } from '@logto/shared';
|
import { generateStandardId } from '@logto/shared';
|
||||||
import { pickState, tryThat } from '@silverhand/essentials';
|
import { pickState, tryThat } from '@silverhand/essentials';
|
||||||
import { object, string, z, number } from 'zod';
|
import { object, string, z, number } from 'zod';
|
||||||
|
@ -7,7 +7,6 @@ import { object, string, z, number } 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 koaRoleRlsErrorHandler from '#src/middleware/koa-role-rls-error-handler.js';
|
import koaRoleRlsErrorHandler from '#src/middleware/koa-role-rls-error-handler.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { parseSearchParamsForSearch } from '#src/utils/search.js';
|
import { parseSearchParamsForSearch } from '#src/utils/search.js';
|
||||||
|
@ -133,7 +132,6 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/roles',
|
'/roles',
|
||||||
koaQuotaGuard({ key: 'rolesLimit', quota }),
|
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: Roles.createGuard
|
body: Roles.createGuard
|
||||||
.omit({ id: true })
|
.omit({ id: true })
|
||||||
|
@ -145,6 +143,13 @@ export default function roleRoutes<T extends AuthedRouter>(...[router, tenant]:
|
||||||
const { body } = ctx.guard;
|
const { body } = ctx.guard;
|
||||||
const { scopeIds, ...roleBody } = body;
|
const { scopeIds, ...roleBody } = body;
|
||||||
|
|
||||||
|
// `rolesLimit` is actually the limit of user roles, keep this name for backward compatibility.
|
||||||
|
// We have optional `type` when creating a new role, if `type` is not provided, use `User` as default.
|
||||||
|
// `machineToMachineRolesLimit` is the limit of machine to machine roles, and is independent to `rolesLimit`.
|
||||||
|
await quota.guardKey(
|
||||||
|
roleBody.type === RoleType.MachineToMachine ? 'machineToMachineRolesLimit' : 'rolesLimit'
|
||||||
|
);
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
!(await findRoleByRoleName(roleBody.name)),
|
!(await findRoleByRoleName(roleBody.name)),
|
||||||
new RequestError({
|
new RequestError({
|
||||||
|
|
|
@ -9,7 +9,9 @@ type RouteResponseType<T extends { search?: unknown; body?: unknown; response?:
|
||||||
|
|
||||||
export type SubscriptionPlan = RouteResponseType<GetRoutes['/api/subscription-plans']>[number];
|
export type SubscriptionPlan = RouteResponseType<GetRoutes['/api/subscription-plans']>[number];
|
||||||
|
|
||||||
|
// Since `standardConnectorsLimit` will be removed in the upcoming pricing V2, no need to guard it.
|
||||||
|
// `tokenLimit` is not guarded in backend.
|
||||||
export type FeatureQuota = Omit<
|
export type FeatureQuota = Omit<
|
||||||
SubscriptionPlan['quota'],
|
SubscriptionPlan['quota'],
|
||||||
'tenantLimit' | 'mauLimit' | 'auditLogsRetentionDays'
|
'tenantLimit' | 'mauLimit' | 'auditLogsRetentionDays' | 'standardConnectorsLimit' | 'tokenLimit'
|
||||||
>;
|
>;
|
||||||
|
|
|
@ -1322,8 +1322,8 @@ importers:
|
||||||
specifier: ^29.5.0
|
specifier: ^29.5.0
|
||||||
version: 29.5.0
|
version: 29.5.0
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-5a698db
|
specifier: 0.2.5-5deb133
|
||||||
version: 0.2.5-5a698db(zod@3.22.4)
|
version: 0.2.5-5deb133(zod@3.22.4)
|
||||||
'@rollup/plugin-commonjs':
|
'@rollup/plugin-commonjs':
|
||||||
specifier: ^25.0.0
|
specifier: ^25.0.0
|
||||||
version: 25.0.7(rollup@4.1.4)
|
version: 25.0.7(rollup@4.1.4)
|
||||||
|
@ -3324,8 +3324,8 @@ importers:
|
||||||
version: 3.22.4
|
version: 3.22.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-5a698db
|
specifier: 0.2.5-5deb133
|
||||||
version: 0.2.5-5a698db(zod@3.22.4)
|
version: 0.2.5-5deb133(zod@3.22.4)
|
||||||
'@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)
|
||||||
|
@ -7522,6 +7522,16 @@ packages:
|
||||||
- zod
|
- zod
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@logto/cloud@0.2.5-5deb133(zod@3.22.4):
|
||||||
|
resolution: {integrity: sha512-3itFraudBBc3bcS6qiBFHXQKoDYPFihxRu+L4HqDVEwTCBJfVVgGgD4F0fMpSpC+ouJY+VVdZGh7Yn9772B5fg==}
|
||||||
|
engines: {node: ^18.12.0}
|
||||||
|
dependencies:
|
||||||
|
'@silverhand/essentials': 2.8.6
|
||||||
|
'@withtyped/server': 0.12.9(zod@3.22.4)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- zod
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@logto/js@3.0.1:
|
/@logto/js@3.0.1:
|
||||||
resolution: {integrity: sha512-vsU6mH5oiiW3k00pMyVA4V31K2Bd0rOT9qWch2l5e5o1yCQLJ3zUIOjGjChu3m2TRu1d920iiUpZU3Lzf6Pwdw==}
|
resolution: {integrity: sha512-vsU6mH5oiiW3k00pMyVA4V31K2Bd0rOT9qWch2l5e5o1yCQLJ3zUIOjGjChu3m2TRu1d920iiUpZU3Lzf6Pwdw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -9221,6 +9231,11 @@ packages:
|
||||||
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0}
|
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@silverhand/essentials@2.8.6:
|
||||||
|
resolution: {integrity: sha512-qNyc6CvZRngP66hTA8sbpXGsiu9j6Hu62jCy7LV3tJiytg/hmxJB5oM9k5tpaUa9kHwYJ2X9CManX7SYYNrzHg==}
|
||||||
|
engines: {node: ^16.13.0 || ^18.12.0 || ^19.2.0, pnpm: ^8.0.0}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@silverhand/ts-config-react@4.0.0(typescript@5.0.2):
|
/@silverhand/ts-config-react@4.0.0(typescript@5.0.2):
|
||||||
resolution: {integrity: sha512-IkZka1iuIBgw0AUbsknghw1vOIs4zOgUxR8jL38Kuk63hmSj687N4BWBb8KhVMhqaG4U/DYkbSEZuwsyHBe68g==}
|
resolution: {integrity: sha512-IkZka1iuIBgw0AUbsknghw1vOIs4zOgUxR8jL38Kuk63hmSj687N4BWBb8KhVMhqaG4U/DYkbSEZuwsyHBe68g==}
|
||||||
engines: {node: ^18.12.0}
|
engines: {node: ^18.12.0}
|
||||||
|
|
Loading…
Reference in a new issue