Skip to content

Commit

Permalink
feat(core): add customJwt paywall guard to core API (#5708)
Browse files Browse the repository at this point in the history
add customJwt paywall guard to core API
  • Loading branch information
simeng-li authored Apr 16, 2024
1 parent 49b60af commit 43430af
Show file tree
Hide file tree
Showing 54 changed files with 332 additions and 38 deletions.
2 changes: 1 addition & 1 deletion packages/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0",
"@logto/app-insights": "workspace:^1.4.0",
"@logto/cloud": "0.2.5-ab8a489",
"@logto/cloud": "0.2.5-94f7bcc",
"@logto/connector-kit": "workspace:^3.0.0",
"@logto/core-kit": "workspace:^2.4.0",
"@logto/language-kit": "workspace:^1.1.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/console/src/consts/quota-item-phrases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const quotaItemPhrasesMap: Record<
organizationsEnabled: 'organizations_enabled.name',
ssoEnabled: 'sso_enabled.name',
tenantMembersLimit: 'tenant_members_limit.name',
customJwtEnabled: 'custom_jwt_enabled.name',
};

export const quotaItemUnlimitedPhrasesMap: Record<
Expand Down Expand Up @@ -54,6 +55,7 @@ export const quotaItemUnlimitedPhrasesMap: Record<
organizationsEnabled: 'organizations_enabled.unlimited',
ssoEnabled: 'sso_enabled.unlimited',
tenantMembersLimit: 'tenant_members_limit.unlimited',
customJwtEnabled: 'custom_jwt_enabled.unlimited',
};

export const quotaItemLimitedPhrasesMap: Record<
Expand Down Expand Up @@ -81,6 +83,7 @@ export const quotaItemLimitedPhrasesMap: Record<
organizationsEnabled: 'organizations_enabled.limited',
ssoEnabled: 'sso_enabled.limited',
tenantMembersLimit: 'tenant_members_limit.limited',
customJwtEnabled: 'custom_jwt_enabled.limited',
};

export const quotaItemNotEligiblePhrasesMap: Record<
Expand Down Expand Up @@ -108,4 +111,5 @@ export const quotaItemNotEligiblePhrasesMap: Record<
organizationsEnabled: 'organizations_enabled.not_eligible',
ssoEnabled: 'sso_enabled.not_eligible',
tenantMembersLimit: 'tenant_members_limit.not_eligible',
customJwtEnabled: 'custom_jwt_enabled.not_eligible',
};
3 changes: 2 additions & 1 deletion packages/console/src/consts/tenants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TenantTag, ReservedPlanId, defaultManagementApi } from '@logto/schemas';
import { ReservedPlanId, TenantTag, defaultManagementApi } from '@logto/schemas';
import dayjs from 'dayjs';

import { type TenantResponse } from '@/cloud/types/router';
Expand Down Expand Up @@ -67,6 +67,7 @@ export const defaultSubscriptionPlan: SubscriptionPlan = {
ticketSupportResponseTime: 48,
thirdPartyApplicationsLimit: null,
tenantMembersLimit: null,
customJwtEnabled: true,
},
};

Expand Down
12 changes: 3 additions & 9 deletions packages/console/src/contexts/AppConfirmModalProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Nullable } from '@silverhand/essentials';
import { noop } from '@silverhand/essentials';
import { useState, useRef, useMemo, createContext, useCallback, useEffect } from 'react';
import { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';

import ConfirmModal from '@/ds-components/ConfirmModal';
import type { ConfirmModalProps } from '@/ds-components/ConfirmModal';
import ConfirmModal from '@/ds-components/ConfirmModal';

type ModalContentRenderProps = {
confirm: (data?: unknown) => void;
Expand Down Expand Up @@ -108,13 +108,7 @@ function AppConfirmModalProvider({ children }: Props) {
{children}
<ConfirmModal
{...restProps}
onConfirm={
type === 'confirm'
? () => {
handleConfirm();
}
: undefined
}
onConfirm={type === 'confirm' ? handleConfirm : undefined}
onCancel={() => {
handleCancel();
}}
Expand Down
44 changes: 41 additions & 3 deletions packages/console/src/pages/CustomizeJwt/CreateButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { type LogtoJwtTokenKeyType } from '@logto/schemas';
import { useCallback, useContext } from 'react';
import { Trans, useTranslation } from 'react-i18next';

import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import Button from '@/ds-components/Button';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import { getPagePath } from '@/pages/CustomizeJwt/utils/path';

Expand All @@ -11,14 +16,47 @@ type Props = {
function CreateButton({ tokenType }: Props) {
const link = getPagePath(tokenType, 'create');
const { navigate } = useTenantPathname();
const { show } = useConfirmModal();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

const { currentPlan } = useContext(SubscriptionDataContext);
const {
quota: { customJwtEnabled },
} = currentPlan;

const onCreateButtonClick = useCallback(async () => {
if (customJwtEnabled) {
navigate(link);
return;
}

const [confirm] = await show({
title: 'upsell.paywall.custom_jwt.title',
ModalContent: () => (
<Trans
components={{
a: <ContactUsPhraseLink />,
}}
>
{t('upsell.paywall.custom_jwt.description')}
</Trans>
),
confirmButtonText: 'upsell.upgrade_plan',
confirmButtonType: 'primary',
isCancelButtonVisible: false,
});

if (confirm) {
// Navigate to subscription page by default
navigate('/tenant-settings/subscription');
}
}, [customJwtEnabled, link, navigate, show, t]);

return (
<Button
type="primary"
title="jwt_claims.custom_jwt_create_button"
onClick={() => {
navigate(link);
}}
onClick={onCreateButtonClick}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@logto/cloud": "0.2.5-749cae5",
"@logto/cloud": "0.2.5-94f7bcc",
"@silverhand/eslint-config": "5.0.0",
"@silverhand/ts-config": "5.0.0",
"@types/debug": "^4.1.7",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/libraries/quota.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConnectorType, DemoConnector } from '@logto/connector-kit';
import { RoleType, ReservedPlanId } from '@logto/schemas';
import { ReservedPlanId, RoleType } from '@logto/schemas';

import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
Expand Down Expand Up @@ -72,6 +72,7 @@ export const createQuotaLibrary = (
ssoEnabled: notNumber,
omniSignInEnabled: notNumber, // No limit for now
builtInEmailConnectorEnabled: notNumber, // No limit for now
customJwtEnabled: notNumber, // No limit for now
};

const getTenantUsage = async (key: keyof FeatureQuota, queryKey?: string): Promise<number> => {
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/routes/logto-config/jwt-customizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ZodError, z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import RequestError, { formatZodError } from '#src/errors/RequestError/index.js';
import koaGuard, { parse } from '#src/middleware/koa-guard.js';
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';

import type { AuthedRouter, RouterInitArgs } from '../types.js';

Expand All @@ -31,7 +32,10 @@ const getJwtTokenKeyAndBody = (tokenPath: LogtoJwtTokenKeyType, body: unknown) =
};

export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
...[router, { id: tenantId, queries, logtoConfigs, cloudConnection }]: RouterInitArgs<T>
...[
router,
{ id: tenantId, queries, logtoConfigs, cloudConnection, libraries },
]: RouterInitArgs<T>
) {
const { getRowsByKeys, deleteJwtCustomizer } = queries.logtoConfigs;
const {
Expand Down Expand Up @@ -60,6 +64,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
response: accessTokenJwtCustomizerGuard.or(clientCredentialsJwtCustomizerGuard),
status: [200, 201, 400, 403],
}),
koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
async (ctx, next) => {
const { isCloud, isIntegrationTest } = EnvSet.values;
if (tenantId === adminTenantId && isCloud && !isIntegrationTest) {
Expand Down Expand Up @@ -109,6 +114,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
response: accessTokenJwtCustomizerGuard.or(clientCredentialsJwtCustomizerGuard),
status: [200, 400, 404],
}),
koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
async (ctx, next) => {
const { isIntegrationTest } = EnvSet.values;

Expand Down Expand Up @@ -211,6 +217,7 @@ export default function logtoConfigJwtCustomizerRoutes<T extends AuthedRouter>(
response: jsonObjectGuard,
status: [200, 400, 403, 422],
}),
koaQuotaGuard({ key: 'customJwtEnabled', quota: libraries.quota }),
async (ctx, next) => {
const { body } = ctx.guard;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ const quota_item = {
/** UNTRANSLATED */
not_eligible: 'Remove your tenant members',
},
custom_jwt_enabled: {
/** UNTRANSLATED */
name: 'Custom JWT',
/** UNTRANSLATED */
limited: 'Custom JWT',
/** UNTRANSLATED */
unlimited: 'Custom JWT',
/** UNTRANSLATED */
not_eligible: 'Remove your JWT claims customizer',
},
};

export default Object.freeze(quota_item);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const tabs = {
tenant_settings: 'Einstellungen',
mfa: 'Multi-Faktor-Authentifizierung',
/** UNTRANSLATED */
customize_jwt: 'JWT Claims',
customize_jwt: 'Custom JWT',
signing_keys: 'Signierschlüssel',
organization_template: 'Organisationstemplate',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ const paywall = {
/** UNTRANSLATED */
tenant_members_dev_plan:
"You've reached your {{limit}}-member limit. Release a member or revoke a pending invitation to add someone new. Need more seats? Feel free to contact us.",
custom_jwt: {
/** UNTRANSLATED */
title: 'Add custom claims',
/** UNTRANSLATED */
description:
"Upgrade to a paid plan for custom JWT functionality and premium benefits. Don't hesitate to <a>contact us</a> if you have any questions.",
},
};

export default Object.freeze(paywall);
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ const quota_item = {
unlimited: 'Unlimited tenant members',
not_eligible: 'Remove your tenant members',
},
custom_jwt_enabled: {
name: 'Custom JWT',
limited: 'Custom JWT',
unlimited: 'Custom JWT',
not_eligible: 'Remove your JWT claims customizer',
},
};

export default Object.freeze(quota_item);
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const tabs = {
docs: 'Docs',
tenant_settings: 'Settings',
mfa: 'Multi-factor auth',
customize_jwt: 'JWT Claims',
customize_jwt: 'Custom JWT',
signing_keys: 'Signing keys',
organization_template: 'Organization template',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ const paywall = {
'Unlock collaboration feature by upgrading to a paid plan. For any assistance, feel free to <a>contact us</a>.',
tenant_members_dev_plan:
"You've reached your {{limit}}-member limit. Release a member or revoke a pending invitation to add someone new. Need more seats? Feel free to contact us.",
custom_jwt: {
title: 'Add custom claims',
description:
"Upgrade to a paid plan for custom JWT functionality and premium benefits. Don't hesitate to <a>contact us</a> if you have any questions.",
},
};

export default Object.freeze(paywall);
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ const quota_item = {
/** UNTRANSLATED */
not_eligible: 'Remove your tenant members',
},
custom_jwt_enabled: {
/** UNTRANSLATED */
name: 'Custom JWT',
/** UNTRANSLATED */
limited: 'Custom JWT',
/** UNTRANSLATED */
unlimited: 'Custom JWT',
/** UNTRANSLATED */
not_eligible: 'Remove your JWT claims customizer',
},
};

export default Object.freeze(quota_item);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const tabs = {
tenant_settings: 'Configuraciones del inquilino',
mfa: 'Autenticación multifactor',
/** UNTRANSLATED */
customize_jwt: 'JWT Claims',
customize_jwt: 'Custom JWT',
signing_keys: 'Claves de firma',
organization_template: 'Plantilla de organización',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ const paywall = {
/** UNTRANSLATED */
tenant_members_dev_plan:
"You've reached your {{limit}}-member limit. Release a member or revoke a pending invitation to add someone new. Need more seats? Feel free to contact us.",
custom_jwt: {
/** UNTRANSLATED */
title: 'Add custom claims',
/** UNTRANSLATED */
description:
"Upgrade to a paid plan for custom JWT functionality and premium benefits. Don't hesitate to <a>contact us</a> if you have any questions.",
},
};

export default Object.freeze(paywall);
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ const quota_item = {
/** UNTRANSLATED */
not_eligible: 'Remove your tenant members',
},
custom_jwt_enabled: {
/** UNTRANSLATED */
name: 'Custom JWT',
/** UNTRANSLATED */
limited: 'Custom JWT',
/** UNTRANSLATED */
unlimited: 'Custom JWT',
/** UNTRANSLATED */
not_eligible: 'Remove your JWT claims customizer',
},
};

export default Object.freeze(quota_item);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const tabs = {
tenant_settings: 'Paramètres du locataire',
mfa: 'Authentification multi-facteur',
/** UNTRANSLATED */
customize_jwt: 'JWT Claims',
customize_jwt: 'Custom JWT',
signing_keys: 'Clés de signature',
organization_template: "Modèle d'organisation",
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ const paywall = {
/** UNTRANSLATED */
tenant_members_dev_plan:
"You've reached your {{limit}}-member limit. Release a member or revoke a pending invitation to add someone new. Need more seats? Feel free to contact us.",
custom_jwt: {
/** UNTRANSLATED */
title: 'Add custom claims',
/** UNTRANSLATED */
description:
"Upgrade to a paid plan for custom JWT functionality and premium benefits. Don't hesitate to <a>contact us</a> if you have any questions.",
},
};

export default Object.freeze(paywall);
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ const quota_item = {
/** UNTRANSLATED */
not_eligible: 'Remove your tenant members',
},
custom_jwt_enabled: {
/** UNTRANSLATED */
name: 'Custom JWT',
/** UNTRANSLATED */
limited: 'Custom JWT',
/** UNTRANSLATED */
unlimited: 'Custom JWT',
/** UNTRANSLATED */
not_eligible: 'Remove your JWT claims customizer',
},
};

export default Object.freeze(quota_item);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const tabs = {
tenant_settings: 'Impostazioni',
mfa: 'Autenticazione multi-fattore',
/** UNTRANSLATED */
customize_jwt: 'JWT Claims',
customize_jwt: 'Custom JWT',
signing_keys: 'Chiavi di firma',
organization_template: 'Modello di organizzazione',
};
Expand Down
Loading

0 comments on commit 43430af

Please sign in to comment.