[Billing for self host] End dummy enterprise key validity (#19560)
<img width="1504" height="755" alt="Screenshot 2026-04-10 at 16 40 07" src="https://github.com/user-attachments/assets/68a12e40-a077-48df-9e18-885493520a32" /> Re-using hasValidEnterpriseKey to avoid breaking changes. This will be entirely removed in the next versions.
This commit is contained in:
+2
-2
@@ -6,7 +6,7 @@ import { InformationBannerBillingSubscriptionPaused } from '@/information-banner
|
||||
import { InformationBannerEndTrialPeriod } from '@/information-banner/components/billing/InformationBannerEndTrialPeriod';
|
||||
import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo';
|
||||
import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription';
|
||||
import { InformationBannerLegacyEnterpriseKey } from '@/information-banner/components/enterprise/InformationBannerLegacyEnterpriseKey';
|
||||
import { InformationBannerInvalidEnterpriseKey } from '@/information-banner/components/enterprise/InformationBannerInvalidEnterpriseKey';
|
||||
import { InformationBannerMaintenance } from '@/information-banner/components/maintenance/InformationBannerMaintenance';
|
||||
import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases';
|
||||
import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions';
|
||||
@@ -55,7 +55,7 @@ export const InformationBannerWrapper = () => {
|
||||
return (
|
||||
<StyledInformationBannerWrapper>
|
||||
<InformationBannerMaintenance />
|
||||
<InformationBannerLegacyEnterpriseKey />
|
||||
<InformationBannerInvalidEnterpriseKey />
|
||||
{isAccountSyncEnabled && (
|
||||
<InformationBannerReconnectAccountInsufficientPermissions />
|
||||
)}
|
||||
|
||||
+7
-6
@@ -9,9 +9,9 @@ import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconKey } from 'twenty-ui/display';
|
||||
|
||||
const COMPONENT_INSTANCE_ID = 'information-banner-legacy-enterprise-key';
|
||||
const COMPONENT_INSTANCE_ID = 'information-banner-invalid-enterprise-key';
|
||||
|
||||
export const InformationBannerLegacyEnterpriseKey = () => {
|
||||
export const InformationBannerInvalidEnterpriseKey = () => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
@@ -21,11 +21,12 @@ export const InformationBannerLegacyEnterpriseKey = () => {
|
||||
COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const hasLegacyKey =
|
||||
const hasInvalidKey =
|
||||
currentWorkspace?.hasValidEnterpriseKey === true &&
|
||||
currentWorkspace?.hasValidSignedEnterpriseKey !== true;
|
||||
currentWorkspace?.hasValidSignedEnterpriseKey !== true &&
|
||||
currentWorkspace?.hasValidEnterpriseValidityToken !== true;
|
||||
|
||||
if (!hasLegacyKey) {
|
||||
if (!hasInvalidKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ export const InformationBannerLegacyEnterpriseKey = () => {
|
||||
<InformationBanner
|
||||
componentInstanceId={COMPONENT_INSTANCE_ID}
|
||||
variant="secondary"
|
||||
message={t`Your enterprise key format is deprecated. Please activate a new key to keep enterprise features.`}
|
||||
message={t`Your enterprise key is no longer valid. Activate a new key to continue using enterprise features.`}
|
||||
buttonTitle={t`Activate`}
|
||||
buttonIcon={IconKey}
|
||||
buttonOnClick={() =>
|
||||
@@ -11,7 +11,8 @@ MUTATION_MAXIMUM_RECORD_AFFECTED=100
|
||||
IS_MULTIWORKSPACE_ENABLED=true
|
||||
FRONTEND_URL=http://localhost:3001
|
||||
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=false
|
||||
ENTERPRISE_KEY=replace_me_with_a_valid_enterprise_key
|
||||
|
||||
ENTERPRISE_VALIDITY_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkZXYtc3Vic2NyaXB0aW9uLWlkIiwic3RhdHVzIjoidmFsaWQiLCJpYXQiOjE3NzMzMDg4MzMsImV4cCI6NDg5NzUxMTIzM30.qhfrW_SV2Y86fWtWXsALlAVhxmMxylUUIefN0fki10Q2NTGGqFVXZrNn2WacJY37yq3m5y4WgwZw34ua6E0ff_YUXsrlY5OHJWHT9DMqKCRn-JujHJnnYp3VHLncy5CvxH5r9mfPFp-5AWe1pYeR1T63sTiejH3sfDrNE357SB7KVti8LCcnsJxEtXB2tRnvyvdun7A-GKoKYEIam-16ZRKKFs6GaWo8ObHdfm8yBt6uK4DZSGPWb644QyWh9FtDxbzJ0ti54DuHSlErLgIp1NNEsMA0MK7zFY7StRaOdt72rxE1ZHwN7e6HhweTU4ORVUPfYkjDFLB2fF7Pa7Kvdg
|
||||
|
||||
AUTH_GOOGLE_ENABLED=false
|
||||
MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
||||
|
||||
+6
-6
@@ -286,6 +286,7 @@ describe('EnterprisePlanService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// hasValidEnterpriseKey now means "has any ENTERPRISE_KEY configured"
|
||||
describe('hasValidEnterpriseKey', () => {
|
||||
it('should return true when signed enterprise key is valid', async () => {
|
||||
await setupValidState();
|
||||
@@ -293,13 +294,12 @@ describe('EnterprisePlanService', () => {
|
||||
expect(service.hasValidEnterpriseKey()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true with legacy unsigned key as fallback', async () => {
|
||||
it('should return true with unsigned legacy key', async () => {
|
||||
setupEnterpriseKey('some-legacy-key');
|
||||
mockCryptoVerify.mockReturnValue(false);
|
||||
appTokenFindOneMock.mockResolvedValue(null);
|
||||
await service.onModuleInit();
|
||||
|
||||
expect(service.hasValidSignedEnterpriseKey()).toBe(false);
|
||||
expect(service.hasValidEnterpriseKey()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -318,13 +318,13 @@ describe('EnterprisePlanService', () => {
|
||||
expect(service.isValid()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true with legacy key as fallback', async () => {
|
||||
it('should return false with unsigned legacy key', async () => {
|
||||
setupEnterpriseKey('some-legacy-key');
|
||||
mockCryptoVerify.mockReturnValue(false);
|
||||
appTokenFindOneMock.mockResolvedValue(null);
|
||||
await service.onModuleInit();
|
||||
|
||||
expect(service.isValid()).toBe(true);
|
||||
expect(service.isValid()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no key or token exists', async () => {
|
||||
@@ -399,7 +399,7 @@ describe('EnterprisePlanService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return legacy license info when only legacy key exists', async () => {
|
||||
it('should return invalid license info when only unsigned legacy key exists', async () => {
|
||||
setupEnterpriseKey('some-legacy-key');
|
||||
mockCryptoVerify.mockReturnValue(false);
|
||||
appTokenFindOneMock.mockResolvedValue(null);
|
||||
@@ -407,7 +407,7 @@ describe('EnterprisePlanService', () => {
|
||||
const licenseInfo = await service.getLicenseInfo();
|
||||
|
||||
expect(licenseInfo).toEqual({
|
||||
isValid: true,
|
||||
isValid: false,
|
||||
licensee: null,
|
||||
expiresAt: null,
|
||||
subscriptionId: null,
|
||||
|
||||
+12
-35
@@ -143,40 +143,22 @@ export class EnterprisePlanService implements OnModuleInit {
|
||||
}
|
||||
|
||||
hasValidEnterpriseKey(): boolean {
|
||||
if (this.hasValidSignedEnterpriseKey()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.checkLegacyKey();
|
||||
return this.hasValidSignedEnterpriseKey() || this.checkLegacyKey();
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
if (this.hasValidEnterpriseValidityToken()) {
|
||||
return true;
|
||||
}
|
||||
return this.hasValidEnterpriseValidityToken();
|
||||
}
|
||||
|
||||
return this.checkLegacyKey(); // temporary
|
||||
private checkLegacyKey(): boolean {
|
||||
// temporary
|
||||
return isDefined(this.twentyConfigService.get('ENTERPRISE_KEY'));
|
||||
}
|
||||
|
||||
isValidEnterpriseKeyFormat(key: string): boolean {
|
||||
return this.verifyJwt<EnterpriseKeyPayload>(key) !== null;
|
||||
}
|
||||
|
||||
private checkLegacyKey(): boolean {
|
||||
const enterpriseKey = this.twentyConfigService.get('ENTERPRISE_KEY');
|
||||
|
||||
if (!isDefined(enterpriseKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
'Unsigned enterprise keys are deprecated and will stop working ' +
|
||||
'in a future version. Please obtain a signed key from twenty.com.',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getLicenseInfo(): Promise<EnterpriseLicenseInfo> {
|
||||
this.refreshKeyPayload();
|
||||
await this.loadValidityToken();
|
||||
@@ -192,15 +174,6 @@ export class EnterprisePlanService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.checkLegacyKey()) {
|
||||
return {
|
||||
isValid: true,
|
||||
licensee: null,
|
||||
expiresAt: null,
|
||||
subscriptionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
licensee: null,
|
||||
@@ -477,11 +450,15 @@ export class EnterprisePlanService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
// In development, try both keys so production keys work when testing locally
|
||||
// In development and Jest integration tests, try both keys so production keys
|
||||
// work locally
|
||||
private getPublicKeysToTry(): string[] {
|
||||
const nodeEnv = this.twentyConfigService.get('NODE_ENV');
|
||||
|
||||
if (nodeEnv === NodeEnvironment.DEVELOPMENT) {
|
||||
if (
|
||||
nodeEnv === NodeEnvironment.DEVELOPMENT ||
|
||||
nodeEnv === NodeEnvironment.TEST
|
||||
) {
|
||||
return [ENTERPRISE_JWT_PUBLIC_KEY, ENTERPRISE_JWT_DEV_PUBLIC_KEY];
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ import { UpsertRowLevelPermissionPredicatesInput } from 'src/engine/metadata-mod
|
||||
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
|
||||
import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto';
|
||||
import { UpsertRowLevelPermissionPredicatesResultDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/upsert-row-level-permission-predicates-result.dto';
|
||||
import { RowLevelPermissionPredicateGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/row-level-permission-predicate/filters/row-level-permission-predicate-graphql-api-exception.filter';
|
||||
import { RowLevelPermissionPredicateGroupService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service';
|
||||
import { RowLevelPermissionPredicateService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
@@ -77,6 +78,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
SettingsPermissionGuard(PermissionFlagType.ROLES),
|
||||
)
|
||||
@UseFilters(
|
||||
RowLevelPermissionPredicateGraphqlApiExceptionFilter,
|
||||
PermissionsGraphqlApiExceptionFilter,
|
||||
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||
)
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Catch, type ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { RowLevelPermissionPredicateException } from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
import { rowLevelPermissionPredicateGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/row-level-permission-predicate/utils/row-level-permission-predicate-graphql-api-exception-handler.util';
|
||||
|
||||
@Catch(RowLevelPermissionPredicateException)
|
||||
export class RowLevelPermissionPredicateGraphqlApiExceptionFilter
|
||||
implements ExceptionFilter
|
||||
{
|
||||
catch(exception: RowLevelPermissionPredicateException) {
|
||||
return rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
RowLevelPermissionPredicateException,
|
||||
RowLevelPermissionPredicateExceptionCode,
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
import { rowLevelPermissionPredicateGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/row-level-permission-predicate/utils/row-level-permission-predicate-graphql-api-exception-handler.util';
|
||||
|
||||
describe('rowLevelPermissionPredicateGraphqlApiExceptionHandler', () => {
|
||||
it('should throw ForbiddenError for ROW_LEVEL_PERMISSION_FEATURE_DISABLED', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Row level permission predicate feature is disabled.',
|
||||
RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(ForbiddenError);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError for ROW_LEVEL_PERMISSION_PREDICATE_NOT_FOUND', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Predicate not found',
|
||||
RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_PREDICATE_NOT_FOUND,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw UserInputError for INVALID_ROW_LEVEL_PERMISSION_PREDICATE_DATA', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Invalid data',
|
||||
RowLevelPermissionPredicateExceptionCode.INVALID_ROW_LEVEL_PERMISSION_PREDICATE_DATA,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(UserInputError);
|
||||
});
|
||||
|
||||
it('should throw InternalServerError for INTERNAL_SERVER_ERROR', () => {
|
||||
const exception = new RowLevelPermissionPredicateException(
|
||||
'Unexpected',
|
||||
RowLevelPermissionPredicateExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
rowLevelPermissionPredicateGraphqlApiExceptionHandler(exception),
|
||||
).toThrow(InternalServerError);
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
type RowLevelPermissionPredicateException,
|
||||
RowLevelPermissionPredicateExceptionCode,
|
||||
} from 'src/engine/metadata-modules/row-level-permission-predicate/exceptions/row-level-permission-predicate.exception';
|
||||
|
||||
export const rowLevelPermissionPredicateGraphqlApiExceptionHandler = (
|
||||
error: RowLevelPermissionPredicateException,
|
||||
) => {
|
||||
switch (error.code) {
|
||||
case RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_FEATURE_DISABLED:
|
||||
case RowLevelPermissionPredicateExceptionCode.UNAUTHORIZED_ROLE_MODIFICATION:
|
||||
case RowLevelPermissionPredicateExceptionCode.UNAUTHORIZED_OBJECT_MODIFICATION:
|
||||
throw new ForbiddenError(error);
|
||||
case RowLevelPermissionPredicateExceptionCode.INVALID_ROW_LEVEL_PERMISSION_PREDICATE_DATA:
|
||||
throw new UserInputError(error);
|
||||
case RowLevelPermissionPredicateExceptionCode.ROW_LEVEL_PERMISSION_PREDICATE_NOT_FOUND:
|
||||
case RowLevelPermissionPredicateExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
case RowLevelPermissionPredicateExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case RowLevelPermissionPredicateExceptionCode.ROLE_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case RowLevelPermissionPredicateExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
throw new InternalServerError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user