Skip to content

Commit

Permalink
refactor: update code according to CR
Browse files Browse the repository at this point in the history
  • Loading branch information
darcyYe committed Jul 22, 2024
1 parent d8a40d1 commit 9bc2bc0
Show file tree
Hide file tree
Showing 14 changed files with 70 additions and 32 deletions.
33 changes: 22 additions & 11 deletions packages/core/src/libraries/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ export const createQuotaLibrary = (
}
};

// `SubscriptionQuota` and `SubscriptionUsage` are sharing keys.
const newGuardKey = async (key: keyof SubscriptionQuota) => {
const guardTenantUsageByKey = async (key: keyof SubscriptionQuota) => {
const { isCloud, isIntegrationTest } = EnvSet.values;

// Cloud only feature, skip in non-cloud environments
Expand All @@ -160,6 +159,8 @@ export const createQuotaLibrary = (
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;

Expand All @@ -178,7 +179,10 @@ export const createQuotaLibrary = (
},
})
);
} else if (typeof limit === 'number') {
return;
}

if (typeof limit === 'number') {
// See the definition of `SubscriptionQuota` and `SubscriptionUsage` in `types.ts`, this should never happen.
assertThat(
typeof usage === 'number',
Expand All @@ -197,12 +201,14 @@ export const createQuotaLibrary = (
},
})
);
} else {
throw new TypeError('Unsupported subscription quota type');

return;
}

throw new TypeError('Unsupported subscription quota type');

Check warning on line 208 in packages/core/src/libraries/quota.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/libraries/quota.ts#L154-L208

Added lines #L154 - L208 were not covered by tests
};

const scopesGuardKey = async (entityName: 'resources' | 'roles', entityId: string) => {
const guardEntityScopesUsage = async (entityName: 'resources' | 'roles', entityId: string) => {
const { isCloud, isIntegrationTest } = EnvSet.values;

// Cloud only feature, skip in non-cloud environments
Expand All @@ -215,10 +221,15 @@ export const createQuotaLibrary = (
return;
}

const {
quota: { scopesPerResourceLimit, scopesPerRoleLimit },
} = await getTenantSubscriptionQuotaAndUsage(cloudConnection);
const scopeUsages = await getTenantSubscriptionScopeUsage(cloudConnection, entityName);
const [
{
quota: { scopesPerResourceLimit, scopesPerRoleLimit },
},
scopeUsages,
] = await Promise.all([
getTenantSubscriptionQuotaAndUsage(cloudConnection),
getTenantSubscriptionScopeUsage(cloudConnection, entityName),
]);

Check warning on line 232 in packages/core/src/libraries/quota.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/libraries/quota.ts#L219-L232

Added lines #L219 - L232 were not covered by tests
const usage = scopeUsages[entityId] ?? 0;

if (entityName === 'resources') {
Expand Down Expand Up @@ -251,5 +262,5 @@ export const createQuotaLibrary = (
);
};

return { guardKey, newGuardKey, scopesGuardKey };
return { guardKey, guardTenantUsageByKey, guardEntityScopesUsage };
};
10 changes: 7 additions & 3 deletions packages/core/src/libraries/sign-in-experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
SignInExperience,
SsoConnectorMetadata,
} from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { ConnectorType, ReservedPlanId } from '@logto/schemas';
import { deduplicate, pick, trySafe } from '@silverhand/essentials';
import deepmerge from 'deepmerge';

Expand All @@ -19,7 +19,7 @@ import type { SsoConnectorLibrary } from '#src/libraries/sso-connector.js';
import { ssoConnectorFactories } from '#src/sso/index.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 { getTenantSubscription, getTenantSubscriptionPlan } from '#src/utils/subscription/index.js';
import { isKeyOfI18nPhrases } from '#src/utils/translation.js';

import { type CloudConnectionLibrary } from '../cloud-connection.js';
Expand Down Expand Up @@ -113,8 +113,12 @@ export const createSignInExperienceLibrary = (
return false;
}

const plan = await getTenantSubscriptionPlan(cloudConnection);
if (EnvSet.values.isDevFeaturesEnabled) {
const subscription = await getTenantSubscription(cloudConnection);
return subscription.planId === ReservedPlanId.Development;
}

Check warning on line 119 in packages/core/src/libraries/sign-in-experience/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/libraries/sign-in-experience/index.ts#L116-L119

Added lines #L116 - L119 were not covered by tests

const plan = await getTenantSubscriptionPlan(cloudConnection);

Check warning on line 121 in packages/core/src/libraries/sign-in-experience/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/libraries/sign-in-experience/index.ts#L121

Added line #L121 was not covered by tests
return plan.id === developmentTenantPlanId;
}, ['is-development-tenant']);

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/middleware/koa-quota-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function newKoaQuotaGuard<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.newGuardKey(key);
await quota.guardTenantUsageByKey(key);
}
return next();
};
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/routes/applications/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,19 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
// When creating a m2m app, should check both m2m limit and application limit.
if (rest.type === ApplicationType.MachineToMachine) {
await (EnvSet.values.isDevFeaturesEnabled
? quota.newGuardKey('machineToMachineLimit')
? quota.guardTenantUsageByKey('machineToMachineLimit')
: quota.guardKey('machineToMachineLimit'));

Check warning on line 175 in packages/core/src/routes/applications/application.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/applications/application.ts#L173-L175

Added lines #L173 - L175 were not covered by tests
}

// Guard third party application limit
if (rest.isThirdParty) {
await (EnvSet.values.isDevFeaturesEnabled
? quota.newGuardKey('thirdPartyApplicationsLimit')
? quota.guardTenantUsageByKey('thirdPartyApplicationsLimit')
: quota.guardKey('thirdPartyApplicationsLimit'));

Check warning on line 182 in packages/core/src/routes/applications/application.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/applications/application.ts#L180-L182

Added lines #L180 - L182 were not covered by tests
}

await (EnvSet.values.isDevFeaturesEnabled
? quota.newGuardKey('applicationsLimit')
? quota.guardTenantUsageByKey('applicationsLimit')
: quota.guardKey('applicationsLimit'));

assertThat(
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/routes/connector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const guardConnectorsQuota = async (
) => {
if (factory.type === ConnectorType.Social) {
await (EnvSet.values.isDevFeaturesEnabled
? quota.newGuardKey('socialConnectorsLimit')
? quota.guardTenantUsageByKey('socialConnectorsLimit')
: quota.guardKey('socialConnectorsLimit'));
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/routes/resource.scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default function resourceScopeRoutes<T extends ManagementApiRouter>(
} = ctx.guard;

await (EnvSet.values.isDevFeaturesEnabled
? quota.scopesGuardKey('resources', resourceId)
? quota.guardEntityScopesUsage('resources', resourceId)
: quota.guardKey('scopesPerResourceLimit', resourceId));

assertThat(!/\s/.test(body.name), 'scope.name_with_space');
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/routes/role.scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function roleScopeRoutes<T extends ManagementApiRouter>(
} = ctx.guard;

await (EnvSet.values.isDevFeaturesEnabled
? quota.scopesGuardKey('roles', id)
? quota.guardEntityScopesUsage('roles', id)
: quota.guardKey('scopesPerRoleLimit', id));

await validateRoleScopeAssignment(scopeIds, id);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/routes/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export default function roleRoutes<T extends ManagementApiRouter>(
// 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 (EnvSet.values.isDevFeaturesEnabled
? quota.newGuardKey(
? quota.guardTenantUsageByKey(
roleBody.type === RoleType.MachineToMachine
? 'machineToMachineRolesLimit'
: // In new pricing model, we rename `rolesLimit` to `userRolesLimit`, which is easier to be distinguished from `machineToMachineRolesLimit`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { object, z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.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 assertThat from '#src/utils/assert-that.js';
import { getConsoleLogFromContext } from '#src/utils/console.js';
Expand All @@ -35,7 +35,11 @@ export default function customUiAssetsRoutes<T extends ManagementApiRouter>(

router.post(
'/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({
files: object({
file: uploadFileGuard.array().min(1).max(1),
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/routes/sign-in-experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
const { deleteConnectorById } = queries.connectors;
const {
signInExperiences: { validateLanguageInfo },
quota: { guardKey, newGuardKey },
quota: { guardKey, guardTenantUsageByKey },
} = libraries;
const { getLogtoConnectors } = connectors;

Expand Down Expand Up @@ -92,7 +92,7 @@ export default function signInExperiencesRoutes<T extends ManagementApiRouter>(
if (mfa) {
if (mfa.factors.length > 0) {
await (EnvSet.values.isDevFeaturesEnabled
? newGuardKey('mfaEnabled')
? guardTenantUsageByKey('mfaEnabled')
: guardKey('mfaEnabled'));

Check warning on line 96 in packages/core/src/routes/sign-in-experience/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/sign-in-experience/index.ts#L94-L96

Added lines #L94 - L96 were not covered by tests
}
validateMfa(mfa);
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/routes/subject-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { addSeconds } from 'date-fns';
import { object, string } from 'zod';

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 { newKoaQuotaGuard } 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';

Expand All @@ -25,7 +26,9 @@ export default function subjectTokenRoutes<T extends ManagementApiRouter>(

router.post(
'/subject-tokens',
newKoaQuotaGuard({ key: 'subjectTokenEnabled', quota }),
EnvSet.values.isDevFeaturesEnabled
? newKoaQuotaGuard({ key: 'subjectTokenEnabled', quota })
: koaQuotaGuard({ key: 'subjectTokenEnabled', quota }),
koaGuard({
body: object({
userId: string(),
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/test-utils/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { jest } = import.meta;
export const createMockQuotaLibrary = (): QuotaLibrary => {
return {
guardKey: jest.fn(),
newGuardKey: jest.fn(),
scopesGuardKey: jest.fn(),
guardTenantUsageByKey: jest.fn(),
guardEntityScopesUsage: jest.fn(),
};
};
18 changes: 16 additions & 2 deletions packages/core/src/utils/subscription/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@ import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'

import assertThat from '../assert-that.js';

import { type SubscriptionQuota, type SubscriptionUsage, 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;
};

Check warning on line 19 in packages/core/src/utils/subscription/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/utils/subscription/index.ts#L13-L19

Added lines #L13 - L19 were not covered by tests

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'),
getTenantSubscription(cloudConnection),

Check warning on line 26 in packages/core/src/utils/subscription/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/utils/subscription/index.ts#L26

Added line #L26 was not covered by tests
client.get('/api/subscription-plans'),
]);
const plan = plans.find(({ id }) => id === subscription.planId);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/utils/subscription/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type RouteResponseType<T extends { search?: unknown; body?: unknown; response?:

export type SubscriptionPlan = RouteResponseType<GetRoutes['/api/subscription-plans']>[number];

export type Subscription = RouteResponseType<GetRoutes['/api/tenants/:tenantId/subscription']>;

Check warning on line 13 in packages/core/src/utils/subscription/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/utils/subscription/types.ts#L12-L13

Added lines #L12 - L13 were not covered by tests
// 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<
Expand Down

0 comments on commit 9bc2bc0

Please sign in to comment.