mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #6259 from logto-io/yemq-update-cloud-api-usages-core
refactor: update @logto/core quota guard logics
This commit is contained in:
commit
b322b9a037
22 changed files with 339 additions and 41 deletions
|
@ -5,8 +5,12 @@ import { EnvSet } from '#src/env-set/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { getTenantSubscriptionPlan } from '#src/utils/subscription/index.js';
|
import {
|
||||||
import { type FeatureQuota } from '#src/utils/subscription/types.js';
|
getTenantSubscriptionPlan,
|
||||||
|
getTenantSubscriptionQuotaAndUsage,
|
||||||
|
getTenantSubscriptionScopeUsage,
|
||||||
|
} from '#src/utils/subscription/index.js';
|
||||||
|
import { type SubscriptionQuota, type FeatureQuota } from '#src/utils/subscription/types.js';
|
||||||
|
|
||||||
import { type CloudConnectionLibrary } from './cloud-connection.js';
|
import { type CloudConnectionLibrary } from './cloud-connection.js';
|
||||||
import { type ConnectorLibrary } from './connector.js';
|
import { type ConnectorLibrary } from './connector.js';
|
||||||
|
@ -33,6 +37,7 @@ export const createQuotaLibrary = (
|
||||||
|
|
||||||
const { getLogtoConnectors } = connectorLibrary;
|
const { getLogtoConnectors } = connectorLibrary;
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
const tenantUsageQueries: Record<
|
const tenantUsageQueries: Record<
|
||||||
keyof FeatureQuota,
|
keyof FeatureQuota,
|
||||||
(queryKey?: string) => Promise<{ count: number }>
|
(queryKey?: string) => Promise<{ count: number }>
|
||||||
|
@ -77,6 +82,7 @@ export const createQuotaLibrary = (
|
||||||
bringYourUiEnabled: notNumber,
|
bringYourUiEnabled: notNumber,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
const getTenantUsage = async (key: keyof FeatureQuota, queryKey?: string): Promise<number> => {
|
const getTenantUsage = async (key: keyof FeatureQuota, queryKey?: string): Promise<number> => {
|
||||||
const query = tenantUsageQueries[key];
|
const query = tenantUsageQueries[key];
|
||||||
const { count } = await query(queryKey);
|
const { count } = await query(queryKey);
|
||||||
|
@ -84,6 +90,7 @@ export const createQuotaLibrary = (
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
const guardKey = async (key: keyof FeatureQuota, queryKey?: string) => {
|
const guardKey = async (key: keyof FeatureQuota, queryKey?: string) => {
|
||||||
const { isCloud, isIntegrationTest } = EnvSet.values;
|
const { isCloud, isIntegrationTest } = EnvSet.values;
|
||||||
|
|
||||||
|
@ -136,5 +143,124 @@ export const createQuotaLibrary = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { guardKey };
|
const guardTenantUsageByKey = async (key: keyof SubscriptionQuota) => {
|
||||||
|
const { isCloud, isIntegrationTest } = EnvSet.values;
|
||||||
|
|
||||||
|
// Cloud only feature, skip in non-cloud environments
|
||||||
|
if (!isCloud) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable in integration tests
|
||||||
|
if (isIntegrationTest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { quota: fullQuota, usage: fullUsage } = await getTenantSubscriptionQuotaAndUsage(
|
||||||
|
cloudConnection
|
||||||
|
);
|
||||||
|
|
||||||
|
// Type `SubscriptionQuota` and type `SubscriptionUsage` are sharing keys, this design helps us to compare the usage with the quota limit in a easier way.
|
||||||
|
const { [key]: limit } = fullQuota;
|
||||||
|
const { [key]: usage } = fullUsage;
|
||||||
|
|
||||||
|
if (limit === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof limit === 'boolean') {
|
||||||
|
assertThat(
|
||||||
|
limit,
|
||||||
|
new RequestError({
|
||||||
|
code: 'subscription.limit_exceeded',
|
||||||
|
status: 403,
|
||||||
|
data: {
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof limit === 'number') {
|
||||||
|
// See the definition of `SubscriptionQuota` and `SubscriptionUsage` in `types.ts`, this should never happen.
|
||||||
|
assertThat(
|
||||||
|
typeof usage === 'number',
|
||||||
|
new TypeError('Usage must be with the same type as the limit.')
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
usage < limit,
|
||||||
|
new RequestError({
|
||||||
|
code: 'subscription.limit_exceeded',
|
||||||
|
status: 403,
|
||||||
|
data: {
|
||||||
|
key,
|
||||||
|
limit,
|
||||||
|
usage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('Unsupported subscription quota type');
|
||||||
|
};
|
||||||
|
|
||||||
|
const guardEntityScopesUsage = async (entityName: 'resources' | 'roles', entityId: string) => {
|
||||||
|
const { isCloud, isIntegrationTest } = EnvSet.values;
|
||||||
|
|
||||||
|
// Cloud only feature, skip in non-cloud environments
|
||||||
|
if (!isCloud) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable in integration tests
|
||||||
|
if (isIntegrationTest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
quota: { scopesPerResourceLimit, scopesPerRoleLimit },
|
||||||
|
},
|
||||||
|
scopeUsages,
|
||||||
|
] = await Promise.all([
|
||||||
|
getTenantSubscriptionQuotaAndUsage(cloudConnection),
|
||||||
|
getTenantSubscriptionScopeUsage(cloudConnection, entityName),
|
||||||
|
]);
|
||||||
|
const usage = scopeUsages[entityId] ?? 0;
|
||||||
|
|
||||||
|
if (entityName === 'resources') {
|
||||||
|
assertThat(
|
||||||
|
scopesPerResourceLimit === null || scopesPerResourceLimit > usage,
|
||||||
|
new RequestError({
|
||||||
|
code: 'subscription.limit_exceeded',
|
||||||
|
status: 403,
|
||||||
|
data: {
|
||||||
|
key: 'scopesPerResourceLimit',
|
||||||
|
limit: scopesPerResourceLimit,
|
||||||
|
usage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
scopesPerRoleLimit === null || scopesPerRoleLimit > usage,
|
||||||
|
new RequestError({
|
||||||
|
code: 'subscription.limit_exceeded',
|
||||||
|
status: 403,
|
||||||
|
data: {
|
||||||
|
key: 'scopesPerRoleLimit',
|
||||||
|
limit: scopesPerRoleLimit,
|
||||||
|
usage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { guardKey, guardTenantUsageByKey, guardEntityScopesUsage };
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
||||||
SignInExperience,
|
SignInExperience,
|
||||||
SsoConnectorMetadata,
|
SsoConnectorMetadata,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType, ReservedPlanId } from '@logto/schemas';
|
||||||
import { deduplicate, pick, trySafe } from '@silverhand/essentials';
|
import { deduplicate, pick, trySafe } from '@silverhand/essentials';
|
||||||
import deepmerge from 'deepmerge';
|
import deepmerge from 'deepmerge';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import type { SsoConnectorLibrary } from '#src/libraries/sso-connector.js';
|
||||||
import { ssoConnectorFactories } from '#src/sso/index.js';
|
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { getTenantSubscriptionPlan } from '#src/utils/subscription/index.js';
|
import { getTenantSubscription, getTenantSubscriptionPlan } from '#src/utils/subscription/index.js';
|
||||||
import { isKeyOfI18nPhrases } from '#src/utils/translation.js';
|
import { isKeyOfI18nPhrases } from '#src/utils/translation.js';
|
||||||
|
|
||||||
import { type CloudConnectionLibrary } from '../cloud-connection.js';
|
import { type CloudConnectionLibrary } from '../cloud-connection.js';
|
||||||
|
@ -113,8 +113,12 @@ export const createSignInExperienceLibrary = (
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = await getTenantSubscriptionPlan(cloudConnection);
|
if (EnvSet.values.isDevFeaturesEnabled) {
|
||||||
|
const subscription = await getTenantSubscription(cloudConnection);
|
||||||
|
return subscription.planId === ReservedPlanId.Development;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plan = await getTenantSubscriptionPlan(cloudConnection);
|
||||||
return plan.id === developmentTenantPlanId;
|
return plan.id === developmentTenantPlanId;
|
||||||
}, ['is-development-tenant']);
|
}, ['is-development-tenant']);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import type { MiddlewareType } from 'koa';
|
import type { MiddlewareType } from 'koa';
|
||||||
|
|
||||||
import { type QuotaLibrary } from '#src/libraries/quota.js';
|
import { type QuotaLibrary } from '#src/libraries/quota.js';
|
||||||
import { type FeatureQuota } from '#src/utils/subscription/types.js';
|
import { type SubscriptionQuota, type FeatureQuota } from '#src/utils/subscription/types.js';
|
||||||
|
|
||||||
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'COPY' | 'HEAD' | 'OPTIONS';
|
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'COPY' | 'HEAD' | 'OPTIONS';
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
type UsageGuardConfig = {
|
type UsageGuardConfig = {
|
||||||
key: keyof FeatureQuota;
|
key: keyof FeatureQuota;
|
||||||
quota: QuotaLibrary;
|
quota: QuotaLibrary;
|
||||||
|
@ -12,6 +13,11 @@ type UsageGuardConfig = {
|
||||||
methods?: Method[];
|
methods?: Method[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NewUsageGuardConfig = Omit<UsageGuardConfig, 'key'> & {
|
||||||
|
key: keyof SubscriptionQuota;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
export default function koaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
export default function koaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
||||||
key,
|
key,
|
||||||
quota,
|
quota,
|
||||||
|
@ -25,3 +31,17 @@ export default function koaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function newKoaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
||||||
|
key,
|
||||||
|
quota,
|
||||||
|
methods,
|
||||||
|
}: NewUsageGuardConfig): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||||
|
return async (ctx, next) => {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
if (!methods || methods.includes(ctx.method.toUpperCase() as Method)) {
|
||||||
|
await quota.guardTenantUsageByKey(key);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// TODO: @darcyYe refactor this file later to remove disable max line comment
|
||||||
|
/* eslint-disable max-lines */
|
||||||
import type { Role } from '@logto/schemas';
|
import type { Role } from '@logto/schemas';
|
||||||
import {
|
import {
|
||||||
Applications,
|
Applications,
|
||||||
|
@ -146,13 +148,26 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
response: Applications.guard,
|
response: Applications.guard,
|
||||||
status: [200, 400, 422, 500],
|
status: [200, 400, 422, 500],
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body;
|
||||||
|
|
||||||
|
const {
|
||||||
|
values: { isDevFeaturesEnabled },
|
||||||
|
} = EnvSet;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
rest.type === ApplicationType.MachineToMachine && quota.guardKey('machineToMachineLimit'),
|
rest.type === ApplicationType.MachineToMachine &&
|
||||||
rest.isThirdParty && quota.guardKey('thirdPartyApplicationsLimit'),
|
(isDevFeaturesEnabled
|
||||||
quota.guardKey('applicationsLimit'),
|
? quota.guardTenantUsageByKey('machineToMachineLimit')
|
||||||
|
: quota.guardKey('machineToMachineLimit')),
|
||||||
|
rest.isThirdParty &&
|
||||||
|
(isDevFeaturesEnabled
|
||||||
|
? quota.guardTenantUsageByKey('thirdPartyApplicationsLimit')
|
||||||
|
: quota.guardKey('thirdPartyApplicationsLimit')),
|
||||||
|
isDevFeaturesEnabled
|
||||||
|
? quota.guardTenantUsageByKey('applicationsLimit')
|
||||||
|
: quota.guardKey('applicationsLimit'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
|
@ -349,3 +364,4 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
applicationCustomDataRoutes(router, tenant);
|
applicationCustomDataRoutes(router, tenant);
|
||||||
}
|
}
|
||||||
|
/* eslint-enable max-lines */
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { conditional } from '@silverhand/essentials';
|
||||||
import cleanDeep from 'clean-deep';
|
import cleanDeep from 'clean-deep';
|
||||||
import { string, object } from 'zod';
|
import { string, object } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import { type QuotaLibrary } from '#src/libraries/quota.js';
|
import { type QuotaLibrary } from '#src/libraries/quota.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
|
@ -26,7 +27,9 @@ const guardConnectorsQuota = async (
|
||||||
quota: QuotaLibrary
|
quota: QuotaLibrary
|
||||||
) => {
|
) => {
|
||||||
if (factory.type === ConnectorType.Social) {
|
if (factory.type === ConnectorType.Social) {
|
||||||
await quota.guardKey('socialConnectorsLimit');
|
await (EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? quota.guardTenantUsageByKey('socialConnectorsLimit')
|
||||||
|
: quota.guardKey('socialConnectorsLimit'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Domains, domainResponseGuard, domainSelectFields } from '@logto/schemas
|
||||||
import { pick } from '@silverhand/essentials';
|
import { pick } from '@silverhand/essentials';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
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 koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
||||||
|
@ -56,7 +57,12 @@ export default function domainRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/domains',
|
'/domains',
|
||||||
koaQuotaGuard({ key: 'customDomainEnabled', quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? // We removed custom domain paywall in new pricing model
|
||||||
|
async (ctx, next) => {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
: koaQuotaGuard({ key: 'customDomainEnabled', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: Domains.createGuard.pick({ domain: true }),
|
body: Domains.createGuard.pick({ domain: true }),
|
||||||
response: domainResponseGuard,
|
response: domainResponseGuard,
|
||||||
|
|
|
@ -14,10 +14,11 @@ import { conditional, deduplicate, yes } from '@silverhand/essentials';
|
||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
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 koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import { type AllowedKeyPrefix } from '#src/queries/log.js';
|
import { type AllowedKeyPrefix } from '#src/queries/log.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
|
|
||||||
|
@ -157,7 +158,9 @@ export default function hookRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/hooks',
|
'/hooks',
|
||||||
koaQuotaGuard({ key: 'hooksLimit', quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'hooksLimit', quota })
|
||||||
|
: koaQuotaGuard({ key: 'hooksLimit', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({
|
body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({
|
||||||
event: hookEventGuard.optional(),
|
event: hookEventGuard.optional(),
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { EnvSet } from '#src/env-set/index.js';
|
||||||
import RequestError, { formatZodError } from '#src/errors/RequestError/index.js';
|
import RequestError, { formatZodError } from '#src/errors/RequestError/index.js';
|
||||||
import { JwtCustomizerLibrary } from '#src/libraries/jwt-customizer.js';
|
import { JwtCustomizerLibrary } from '#src/libraries/jwt-customizer.js';
|
||||||
import koaGuard, { parse } from '#src/middleware/koa-guard.js';
|
import koaGuard, { parse } from '#src/middleware/koa-guard.js';
|
||||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||||
|
|
||||||
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
|
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
|
||||||
|
@ -61,7 +61,9 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
||||||
response: accessTokenJwtCustomizerGuard.or(clientCredentialsJwtCustomizerGuard),
|
response: accessTokenJwtCustomizerGuard.or(clientCredentialsJwtCustomizerGuard),
|
||||||
status: [200, 201, 400, 403],
|
status: [200, 201, 400, 403],
|
||||||
}),
|
}),
|
||||||
koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota })
|
||||||
|
: koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { isCloud, isIntegrationTest } = EnvSet.values;
|
const { isCloud, isIntegrationTest } = EnvSet.values;
|
||||||
if (tenantId === adminTenantId && isCloud && !isIntegrationTest) {
|
if (tenantId === adminTenantId && isCloud && !isIntegrationTest) {
|
||||||
|
@ -112,7 +114,9 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
||||||
response: accessTokenJwtCustomizerGuard.or(clientCredentialsJwtCustomizerGuard),
|
response: accessTokenJwtCustomizerGuard.or(clientCredentialsJwtCustomizerGuard),
|
||||||
status: [200, 400, 404],
|
status: [200, 400, 404],
|
||||||
}),
|
}),
|
||||||
koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota })
|
||||||
|
: koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { isIntegrationTest } = EnvSet.values;
|
const { isIntegrationTest } = EnvSet.values;
|
||||||
|
|
||||||
|
@ -215,7 +219,9 @@ export default function logtoConfigJwtCustomizerRoutes<T extends ManagementApiRo
|
||||||
response: jsonObjectGuard,
|
response: jsonObjectGuard,
|
||||||
status: [200, 400, 403, 422],
|
status: [200, 400, 403, 422],
|
||||||
}),
|
}),
|
||||||
koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota })
|
||||||
|
: koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { body } = ctx.guard;
|
const { body } = ctx.guard;
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,11 @@ import {
|
||||||
import { generateStandardId } from '@logto/shared';
|
import { generateStandardId } from '@logto/shared';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
import { buildManagementApiContext } from '#src/libraries/hook/utils.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 koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import { organizationRoleSearchKeys } from '#src/queries/organization/index.js';
|
import { organizationRoleSearchKeys } from '#src/queries/organization/index.js';
|
||||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||||
import { parseSearchOptions } from '#src/utils/search.js';
|
import { parseSearchOptions } from '#src/utils/search.js';
|
||||||
|
@ -44,7 +45,11 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
|
||||||
unknown,
|
unknown,
|
||||||
ManagementApiRouterContext
|
ManagementApiRouterContext
|
||||||
>(OrganizationRoles, roles, {
|
>(OrganizationRoles, roles, {
|
||||||
middlewares: [koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })],
|
middlewares: [
|
||||||
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })
|
||||||
|
: koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
||||||
|
],
|
||||||
disabled: { get: true, post: true },
|
disabled: { get: true, post: true },
|
||||||
errorHandler,
|
errorHandler,
|
||||||
searchFields: ['name'],
|
searchFields: ['name'],
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { OrganizationScopes } from '@logto/schemas';
|
import { OrganizationScopes } from '@logto/schemas';
|
||||||
|
|
||||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
|
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||||
|
|
||||||
import { errorHandler } from '../organization/utils.js';
|
import { errorHandler } from '../organization/utils.js';
|
||||||
|
@ -18,7 +19,11 @@ export default function organizationScopeRoutes<T extends ManagementApiRouter>(
|
||||||
]: RouterInitArgs<T>
|
]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const router = new SchemaRouter(OrganizationScopes, scopes, {
|
const router = new SchemaRouter(OrganizationScopes, scopes, {
|
||||||
middlewares: [koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })],
|
middlewares: [
|
||||||
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })
|
||||||
|
: koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
||||||
|
],
|
||||||
errorHandler,
|
errorHandler,
|
||||||
searchFields: ['name'],
|
searchFields: ['name'],
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { type OrganizationWithFeatured, Organizations, featuredUserGuard } from
|
||||||
import { yes } from '@silverhand/essentials';
|
import { yes } from '@silverhand/essentials';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/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 koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||||
import { parseSearchOptions } from '#src/utils/search.js';
|
import { parseSearchOptions } from '#src/utils/search.js';
|
||||||
|
|
||||||
|
@ -30,7 +31,11 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
|
||||||
] = args;
|
] = args;
|
||||||
|
|
||||||
const router = new SchemaRouter(Organizations, organizations, {
|
const router = new SchemaRouter(Organizations, organizations, {
|
||||||
middlewares: [koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })],
|
middlewares: [
|
||||||
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] })
|
||||||
|
: koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }),
|
||||||
|
],
|
||||||
errorHandler,
|
errorHandler,
|
||||||
searchFields: ['name'],
|
searchFields: ['name'],
|
||||||
disabled: { get: true },
|
disabled: { get: true },
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { generateStandardId } from '@logto/shared';
|
||||||
import { tryThat } from '@silverhand/essentials';
|
import { tryThat } from '@silverhand/essentials';
|
||||||
import { object, string } from 'zod';
|
import { object, string } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
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';
|
||||||
|
@ -89,7 +90,9 @@ export default function resourceScopeRoutes<T extends ManagementApiRouter>(
|
||||||
body,
|
body,
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
await quota.guardKey('scopesPerResourceLimit', resourceId);
|
await (EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? quota.guardEntityScopesUsage('resources', resourceId)
|
||||||
|
: quota.guardKey('scopesPerResourceLimit', resourceId));
|
||||||
|
|
||||||
assertThat(!/\s/.test(body.name), 'scope.name_with_space');
|
assertThat(!/\s/.test(body.name), 'scope.name_with_space');
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@ import { generateStandardId } from '@logto/shared';
|
||||||
import { yes } from '@silverhand/essentials';
|
import { yes } from '@silverhand/essentials';
|
||||||
import { boolean, object, string } from 'zod';
|
import { boolean, object, string } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
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 koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { attachScopesToResources } from '#src/utils/resource.js';
|
import { attachScopesToResources } from '#src/utils/resource.js';
|
||||||
|
|
||||||
|
@ -76,7 +77,9 @@ export default function resourceRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/resources',
|
'/resources',
|
||||||
koaQuotaGuard({ key: 'resourcesLimit', quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'resourcesLimit', quota })
|
||||||
|
: koaQuotaGuard({ key: 'resourcesLimit', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
// Intentionally omit `isDefault` since it'll affect other rows.
|
// Intentionally omit `isDefault` since it'll affect other rows.
|
||||||
// Use the dedicated API `PATCH /resources/:id/is-default` to update.
|
// Use the dedicated API `PATCH /resources/:id/is-default` to update.
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { generateStandardId } from '@logto/shared';
|
||||||
import { tryThat } from '@silverhand/essentials';
|
import { tryThat } from '@silverhand/essentials';
|
||||||
import { object, string } from 'zod';
|
import { object, string } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
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';
|
||||||
|
@ -93,7 +94,9 @@ export default function roleScopeRoutes<T extends ManagementApiRouter>(
|
||||||
body: { scopeIds },
|
body: { scopeIds },
|
||||||
} = ctx.guard;
|
} = ctx.guard;
|
||||||
|
|
||||||
await quota.guardKey('scopesPerRoleLimit', id);
|
await (EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? quota.guardEntityScopesUsage('roles', id)
|
||||||
|
: quota.guardKey('scopesPerRoleLimit', id));
|
||||||
|
|
||||||
await validateRoleScopeAssignment(scopeIds, id);
|
await validateRoleScopeAssignment(scopeIds, id);
|
||||||
await insertRolesScopes(
|
await insertRolesScopes(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { generateStandardId } from '@logto/shared';
|
||||||
import { pickState, trySafe, tryThat } from '@silverhand/essentials';
|
import { pickState, trySafe, tryThat } from '@silverhand/essentials';
|
||||||
import { number, object, string, z } from 'zod';
|
import { number, object, string, z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
|
@ -150,9 +151,18 @@ export default function roleRoutes<T extends ManagementApiRouter>(
|
||||||
// `rolesLimit` is actually the limit of user roles, keep this name for backward compatibility.
|
// `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.
|
// 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`.
|
// `machineToMachineRolesLimit` is the limit of machine to machine roles, and is independent to `rolesLimit`.
|
||||||
await quota.guardKey(
|
await (EnvSet.values.isDevFeaturesEnabled
|
||||||
roleBody.type === RoleType.MachineToMachine ? 'machineToMachineRolesLimit' : 'rolesLimit'
|
? quota.guardTenantUsageByKey(
|
||||||
);
|
roleBody.type === RoleType.MachineToMachine
|
||||||
|
? 'machineToMachineRolesLimit'
|
||||||
|
: // In new pricing model, we rename `rolesLimit` to `userRolesLimit`, which is easier to be distinguished from `machineToMachineRolesLimit`.
|
||||||
|
'userRolesLimit'
|
||||||
|
)
|
||||||
|
: quota.guardKey(
|
||||||
|
roleBody.type === RoleType.MachineToMachine
|
||||||
|
? 'machineToMachineRolesLimit'
|
||||||
|
: 'rolesLimit'
|
||||||
|
));
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
!(await findRoleByRoleName(roleBody.name)),
|
!(await findRoleByRoleName(roleBody.name)),
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { object, z } from 'zod';
|
||||||
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';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import SystemContext from '#src/tenants/SystemContext.js';
|
import SystemContext from '#src/tenants/SystemContext.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
import { getConsoleLogFromContext } from '#src/utils/console.js';
|
||||||
|
@ -35,7 +35,11 @@ export default function customUiAssetsRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/sign-in-exp/default/custom-ui-assets',
|
'/sign-in-exp/default/custom-ui-assets',
|
||||||
koaQuotaGuard({ key: 'bringYourUiEnabled', quota }),
|
// Manually add this to avoid the case that the dev feature guard is removed but the quota guard is not being updated accordingly.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'bringYourUiEnabled', quota })
|
||||||
|
: koaQuotaGuard({ key: 'bringYourUiEnabled', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
files: object({
|
files: object({
|
||||||
file: uploadFileGuard.array().min(1).max(1),
|
file: uploadFileGuard.array().min(1).max(1),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { DemoConnector } from '@logto/connector-kit';
|
||||||
import { ConnectorType, SignInExperiences } from '@logto/schemas';
|
import { ConnectorType, SignInExperiences } from '@logto/schemas';
|
||||||
import { literal, object, string, z } from 'zod';
|
import { literal, object, string, z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import { validateSignUp, validateSignIn } from '#src/libraries/sign-in-experience/index.js';
|
import { validateSignUp, validateSignIn } from '#src/libraries/sign-in-experience/index.js';
|
||||||
import { validateMfa } from '#src/libraries/sign-in-experience/mfa.js';
|
import { validateMfa } from '#src/libraries/sign-in-experience/mfa.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
|
@ -18,7 +19,7 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
||||||
const { deleteConnectorById } = queries.connectors;
|
const { deleteConnectorById } = queries.connectors;
|
||||||
const {
|
const {
|
||||||
signInExperiences: { validateLanguageInfo },
|
signInExperiences: { validateLanguageInfo },
|
||||||
quota: { guardKey },
|
quota: { guardKey, guardTenantUsageByKey },
|
||||||
} = libraries;
|
} = libraries;
|
||||||
const { getLogtoConnectors } = connectors;
|
const { getLogtoConnectors } = connectors;
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
||||||
response: SignInExperiences.guard,
|
response: SignInExperiences.guard,
|
||||||
status: [200, 400, 404, 422],
|
status: [200, 400, 404, 422],
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const {
|
const {
|
||||||
query: { removeUnusedDemoSocialConnector },
|
query: { removeUnusedDemoSocialConnector },
|
||||||
|
@ -89,7 +91,9 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
if (mfa) {
|
if (mfa) {
|
||||||
if (mfa.factors.length > 0) {
|
if (mfa.factors.length > 0) {
|
||||||
await guardKey('mfaEnabled');
|
await (EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? guardTenantUsageByKey('mfaEnabled')
|
||||||
|
: guardKey('mfaEnabled'));
|
||||||
}
|
}
|
||||||
validateMfa(mfa);
|
validateMfa(mfa);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,11 @@ import { generateStandardShortId } from '@logto/shared';
|
||||||
import { assert, conditional } from '@silverhand/essentials';
|
import { assert, conditional } from '@silverhand/essentials';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
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 koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
import { ssoConnectorCreateGuard, ssoConnectorPatchGuard } from '#src/routes/sso-connector/type.js';
|
import { ssoConnectorCreateGuard, ssoConnectorPatchGuard } from '#src/routes/sso-connector/type.js';
|
||||||
import { ssoConnectorFactories } from '#src/sso/index.js';
|
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||||
import { isSupportedSsoConnector, isSupportedSsoProvider } from '#src/sso/utils.js';
|
import { isSupportedSsoConnector, isSupportedSsoProvider } from '#src/sso/utils.js';
|
||||||
|
@ -68,7 +69,9 @@ export default function singleSignOnConnectorsRoutes<T extends ManagementApiRout
|
||||||
/* Create a new single sign on connector */
|
/* Create a new single sign on connector */
|
||||||
router.post(
|
router.post(
|
||||||
pathname,
|
pathname,
|
||||||
koaQuotaGuard({ key: 'ssoEnabled', quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'enterpriseSsoLimit', quota })
|
||||||
|
: koaQuotaGuard({ key: 'ssoEnabled', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: ssoConnectorCreateGuard,
|
body: ssoConnectorCreateGuard,
|
||||||
response: SsoConnectors.guard,
|
response: SsoConnectors.guard,
|
||||||
|
|
|
@ -4,8 +4,9 @@ import { addSeconds } from 'date-fns';
|
||||||
import { object, string } from 'zod';
|
import { object, string } from 'zod';
|
||||||
|
|
||||||
import { subjectTokenExpiresIn, subjectTokenPrefix } from '#src/constants/index.js';
|
import { subjectTokenExpiresIn, subjectTokenPrefix } from '#src/constants/index.js';
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
import koaQuotaGuard, { newKoaQuotaGuard } from '#src/middleware/koa-quota-guard.js';
|
||||||
|
|
||||||
import { type RouterInitArgs, type ManagementApiRouter } from './types.js';
|
import { type RouterInitArgs, type ManagementApiRouter } from './types.js';
|
||||||
|
|
||||||
|
@ -25,7 +26,9 @@ export default function subjectTokenRoutes<T extends ManagementApiRouter>(
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/subject-tokens',
|
'/subject-tokens',
|
||||||
koaQuotaGuard({ key: 'subjectTokenEnabled', quota }),
|
EnvSet.values.isDevFeaturesEnabled
|
||||||
|
? newKoaQuotaGuard({ key: 'subjectTokenEnabled', quota })
|
||||||
|
: koaQuotaGuard({ key: 'subjectTokenEnabled', quota }),
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: object({
|
body: object({
|
||||||
userId: string(),
|
userId: string(),
|
||||||
|
|
|
@ -5,5 +5,7 @@ const { jest } = import.meta;
|
||||||
export const createMockQuotaLibrary = (): QuotaLibrary => {
|
export const createMockQuotaLibrary = (): QuotaLibrary => {
|
||||||
return {
|
return {
|
||||||
guardKey: jest.fn(),
|
guardKey: jest.fn(),
|
||||||
|
guardTenantUsageByKey: jest.fn(),
|
||||||
|
guardEntityScopesUsage: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,28 @@ import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'
|
||||||
|
|
||||||
import assertThat from '../assert-that.js';
|
import assertThat from '../assert-that.js';
|
||||||
|
|
||||||
import { type SubscriptionPlan } from './types.js';
|
import {
|
||||||
|
type SubscriptionQuota,
|
||||||
|
type SubscriptionUsage,
|
||||||
|
type SubscriptionPlan,
|
||||||
|
type Subscription,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
export const getTenantSubscription = async (
|
||||||
|
cloudConnection: CloudConnectionLibrary
|
||||||
|
): Promise<Subscription> => {
|
||||||
|
const client = await cloudConnection.getClient();
|
||||||
|
const subscription = await client.get('/api/tenants/my/subscription');
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
};
|
||||||
|
|
||||||
export const getTenantSubscriptionPlan = async (
|
export const getTenantSubscriptionPlan = async (
|
||||||
cloudConnection: CloudConnectionLibrary
|
cloudConnection: CloudConnectionLibrary
|
||||||
): Promise<SubscriptionPlan> => {
|
): Promise<SubscriptionPlan> => {
|
||||||
const client = await cloudConnection.getClient();
|
const client = await cloudConnection.getClient();
|
||||||
const [subscription, plans] = await Promise.all([
|
const [subscription, plans] = await Promise.all([
|
||||||
client.get('/api/tenants/my/subscription'),
|
getTenantSubscription(cloudConnection),
|
||||||
client.get('/api/subscription-plans'),
|
client.get('/api/subscription-plans'),
|
||||||
]);
|
]);
|
||||||
const plan = plans.find(({ id }) => id === subscription.planId);
|
const plan = plans.find(({ id }) => id === subscription.planId);
|
||||||
|
@ -18,3 +32,31 @@ export const getTenantSubscriptionPlan = async (
|
||||||
|
|
||||||
return plan;
|
return plan;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTenantSubscriptionQuotaAndUsage = async (
|
||||||
|
cloudConnection: CloudConnectionLibrary
|
||||||
|
): Promise<{
|
||||||
|
quota: SubscriptionQuota;
|
||||||
|
usage: SubscriptionUsage;
|
||||||
|
}> => {
|
||||||
|
const client = await cloudConnection.getClient();
|
||||||
|
const [quota, usage] = await Promise.all([
|
||||||
|
client.get('/api/tenants/my/subscription/quota'),
|
||||||
|
client.get('/api/tenants/my/subscription/usage'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { quota, usage };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTenantSubscriptionScopeUsage = async (
|
||||||
|
cloudConnection: CloudConnectionLibrary,
|
||||||
|
entityName: 'resources' | 'roles'
|
||||||
|
): Promise<Record<string, number>> => {
|
||||||
|
const client = await cloudConnection.getClient();
|
||||||
|
const scopeUsages = await client.get('/api/tenants/my/subscription/usage/:entityName/scopes', {
|
||||||
|
params: { entityName },
|
||||||
|
search: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return scopeUsages;
|
||||||
|
};
|
||||||
|
|
|
@ -9,9 +9,31 @@ 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];
|
||||||
|
|
||||||
|
export type Subscription = RouteResponseType<GetRoutes['/api/tenants/:tenantId/subscription']>;
|
||||||
|
|
||||||
// Since `standardConnectorsLimit` will be removed in the upcoming pricing V2, no need to guard it.
|
// Since `standardConnectorsLimit` will be removed in the upcoming pricing V2, no need to guard it.
|
||||||
// `tokenLimit` is not guarded in backend.
|
// `tokenLimit` is not guarded in backend.
|
||||||
export type FeatureQuota = Omit<
|
export type FeatureQuota = Omit<
|
||||||
SubscriptionPlan['quota'],
|
SubscriptionPlan['quota'],
|
||||||
'tenantLimit' | 'mauLimit' | 'auditLogsRetentionDays' | 'standardConnectorsLimit' | 'tokenLimit'
|
'tenantLimit' | 'mauLimit' | 'auditLogsRetentionDays' | 'standardConnectorsLimit' | 'tokenLimit'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the response of the `GET /api/tenants/:tenantId/subscription/quota` endpoint.
|
||||||
|
* It is the same as the response type of `GET /api/tenants/my/subscription/quota` endpoint.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The `auditLogsRetentionDays` will be handled by cron job in Azure Functions, outdated audit logs will be removed automatically.
|
||||||
|
*/
|
||||||
|
export type SubscriptionQuota = Omit<
|
||||||
|
RouteResponseType<GetRoutes['/api/tenants/:tenantId/subscription/quota']>,
|
||||||
|
'auditLogsRetentionDays'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the response of the `GET /api/tenants/:tenantId/subscription/usage` endpoint.
|
||||||
|
* It is the same as the response type of `GET /api/tenants/my/subscription/usage` endpoint.
|
||||||
|
*/
|
||||||
|
export type SubscriptionUsage = RouteResponseType<
|
||||||
|
GetRoutes['/api/tenants/:tenantId/subscription/usage']
|
||||||
|
>;
|
||||||
|
|
Loading…
Reference in a new issue