From fa86f718535cde6cfb11b3a673f711c18c0953c0 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:50:22 +0800 Subject: [PATCH] feat(server): client version check (#9205) Co-authored-by: forehalo --- packages/backend/server/package.json | 2 + .../server/src/__tests__/version.spec.ts | 148 ++++++++++++++++++ packages/backend/server/src/app.module.ts | 2 + packages/backend/server/src/base/error/def.ts | 11 ++ .../server/src/base/error/errors.gen.ts | 16 +- .../backend/server/src/base/guard/guard.ts | 15 +- .../server/src/core/auth/controller.ts | 4 +- .../backend/server/src/core/version/config.ts | 31 ++++ .../backend/server/src/core/version/guard.ts | 40 +++++ .../backend/server/src/core/version/index.ts | 13 ++ .../server/src/core/version/service.ts | 59 +++++++ .../server/src/plugins/oauth/controller.ts | 3 + packages/backend/server/src/schema.gql | 8 +- packages/frontend/graphql/src/schema.ts | 8 + packages/frontend/i18n/src/i18n.gen.ts | 7 + packages/frontend/i18n/src/resources/en.json | 3 +- yarn.lock | 9 ++ 17 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 packages/backend/server/src/__tests__/version.spec.ts create mode 100644 packages/backend/server/src/core/version/config.ts create mode 100644 packages/backend/server/src/core/version/guard.ts create mode 100644 packages/backend/server/src/core/version/index.ts create mode 100644 packages/backend/server/src/core/version/service.ts diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index f45e2adcd350c..deeb85494457e 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -92,6 +92,7 @@ "react-dom": "19.0.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "semver": "^7.6.3", "ses": "^1.10.0", "socket.io": "^4.8.1", "stripe": "^17.4.0", @@ -119,6 +120,7 @@ "@types/on-headers": "^1.0.3", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", + "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", "ava": "^6.2.0", diff --git a/packages/backend/server/src/__tests__/version.spec.ts b/packages/backend/server/src/__tests__/version.spec.ts new file mode 100644 index 0000000000000..e3dc0cf475c0a --- /dev/null +++ b/packages/backend/server/src/__tests__/version.spec.ts @@ -0,0 +1,148 @@ +import { Controller, Get } from '@nestjs/common'; +import test from 'ava'; +import Sinon from 'sinon'; + +import { AppModule } from '../app.module'; +import { Runtime, UseNamedGuard } from '../base'; +import { Public } from '../core/auth/guard'; +import { VersionService } from '../core/version/service'; +import { createTestingApp, TestingApp } from './utils'; + +@Public() +@Controller('/guarded') +class GuardedController { + @UseNamedGuard('version') + @Get('/test') + test() { + return 'test'; + } +} + +let app: TestingApp; +let runtime: Sinon.SinonStubbedInstance; +let version: VersionService; + +function checkVersion(enabled = true) { + runtime.fetch.withArgs('client/versionControl.enabled').resolves(enabled); + + runtime.fetch + .withArgs('client/versionControl.requiredVersion') + .resolves('>=0.20.0'); +} + +test.before(async () => { + app = await createTestingApp({ + imports: [AppModule], + controllers: [GuardedController], + tapModule: m => { + m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime)); + }, + }); + + runtime = app.get(Runtime); + version = app.get(VersionService, { strict: false }); +}); + +test.beforeEach(async () => { + Sinon.reset(); + + checkVersion(true); +}); + +test.after.always(async () => { + await app.close(); +}); + +test('should passthrough if version check is not enabled', async t => { + checkVersion(false); + + const spy = Sinon.spy(version, 'checkVersion'); + + let res = await app.GET('/guarded/test'); + + t.is(res.status, 200); + + res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); + + t.is(res.status, 200); + + res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); + + t.is(res.status, 200); + t.true(spy.notCalled); + spy.restore(); +}); + +test('should passthrough is version range is invalid', async t => { + runtime.fetch + .withArgs('client/versionControl.requiredVersion') + .resolves('invalid'); + + let res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); + + t.is(res.status, 200); +}); + +test('should pass if client version is allowed', async t => { + let res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); + + t.is(res.status, 200); + + res = await app.GET('/guarded/test').set('x-affine-version', '0.21.0'); + + t.is(res.status, 200); + + runtime.fetch + .withArgs('client/versionControl.requiredVersion') + .resolves('>=0.19.0'); + + res = await app.GET('/guarded/test').set('x-affine-version', '0.19.0'); + + t.is(res.status, 200); +}); + +test('should fail if client version is not set or invalid', async t => { + let res = await app.GET('/guarded/test'); + + t.is(res.status, 403); + t.is( + res.body.message, + 'Unsupported client with version [unset_or_invalid], required version is [>=0.20.0].' + ); + + res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); + + t.is(res.status, 403); + t.is( + res.body.message, + 'Unsupported client with version [invalid], required version is [>=0.20.0].' + ); +}); + +test('should tell upgrade if client version is lower than allowed', async t => { + runtime.fetch + .withArgs('client/versionControl.requiredVersion') + .resolves('>=0.21.0 <=0.22.0'); + + let res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); + + t.is(res.status, 403); + t.is( + res.body.message, + 'Unsupported client with version [0.20.0], required version is [>=0.21.0 <=0.22.0].' + ); +}); + +test('should tell downgrade if client version is higher than allowed', async t => { + runtime.fetch + .withArgs('client/versionControl.requiredVersion') + .resolves('>=0.20.0 <=0.22.0'); + + let res = await app.GET('/guarded/test').set('x-affine-version', '0.23.0'); + + t.is(res.status, 403); + t.is( + res.body.message, + 'Unsupported client with version [0.23.0], required version is [>=0.20.0 <=0.22.0].' + ); +}); diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index a3c2dc2e1c1bc..d46168e690493 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -49,6 +49,7 @@ import { SelfhostModule } from './core/selfhost'; import { StorageModule } from './core/storage'; import { SyncModule } from './core/sync'; import { UserModule } from './core/user'; +import { VersionModule } from './core/version'; import { WorkspaceModule } from './core/workspaces'; import { ModelsModule } from './models'; import { REGISTERED_PLUGINS } from './plugins'; @@ -225,6 +226,7 @@ export function buildAppModule() { // graphql server only .useIf( config => config.flavor.graphql, + VersionModule, GqlModule, StorageModule, ServerConfigModule, diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 765fe24d42e48..c91d8e27e9a4c 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -724,4 +724,15 @@ export const USER_FRIENDLY_ERRORS = { message: ({ limit }) => `You cannot downgrade the workspace from team workspace because there are more than ${limit} members that are currently active.`, }, + + // version errors + unsupported_client_version: { + type: 'action_forbidden', + args: { + clientVersion: 'string', + requiredVersion: 'string', + }, + message: ({ clientVersion, requiredVersion }) => + `Unsupported client with version [${clientVersion}], required version is [${requiredVersion}].`, + }, } satisfies Record; diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index 25ac6f26b22bc..b8cb1dff8166e 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -794,6 +794,17 @@ export class WorkspaceMembersExceedLimitToDowngrade extends UserFriendlyError { super('bad_request', 'workspace_members_exceed_limit_to_downgrade', message, args); } } +@ObjectType() +class UnsupportedClientVersionDataType { + @Field() clientVersion!: string + @Field() requiredVersion!: string +} + +export class UnsupportedClientVersion extends UserFriendlyError { + constructor(args: UnsupportedClientVersionDataType, message?: string | ((args: UnsupportedClientVersionDataType) => string)) { + super('action_forbidden', 'unsupported_client_version', message, args); + } +} export enum ErrorNames { INTERNAL_SERVER_ERROR, TOO_MANY_REQUEST, @@ -895,7 +906,8 @@ export enum ErrorNames { LICENSE_NOT_FOUND, INVALID_LICENSE_TO_ACTIVATE, INVALID_LICENSE_UPDATE_PARAMS, - WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE + WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE, + UNSUPPORTED_CLIENT_VERSION } registerEnumType(ErrorNames, { name: 'ErrorNames' @@ -904,5 +916,5 @@ registerEnumType(ErrorNames, { export const ErrorDataUnionType = createUnionType({ name: 'ErrorDataUnion', types: () => - [GraphqlBadRequestDataType, QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType] as const, + [GraphqlBadRequestDataType, QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType, UnsupportedClientVersionDataType] as const, }); diff --git a/packages/backend/server/src/base/guard/guard.ts b/packages/backend/server/src/base/guard/guard.ts index e747ee6f0aa44..28f7794d65d6b 100644 --- a/packages/backend/server/src/base/guard/guard.ts +++ b/packages/backend/server/src/base/guard/guard.ts @@ -18,14 +18,19 @@ export class BasicGuard implements CanActivate { async canActivate(context: ExecutionContext) { // get registered guard name - const providerName = this.reflector.get( + const providerName = this.reflector.get( BasicGuardSymbol, context.getHandler() ); - const provider = GUARD_PROVIDER[providerName as NamedGuards]; - if (provider) { - return await provider.canActivate(context); + if (Array.isArray(providerName) && providerName.length > 0) { + for (const name of providerName) { + const provider = GUARD_PROVIDER[name as NamedGuards]; + if (provider) { + const ret = await provider.canActivate(context); + if (!ret) return false; + } + } } return true; @@ -46,5 +51,5 @@ export class BasicGuard implements CanActivate { * } * ``` */ -export const UseNamedGuard = (name: NamedGuards) => +export const UseNamedGuard = (...name: NamedGuards[]) => applyDecorators(UseGuards(BasicGuard), SetMetadata(BasicGuardSymbol, name)); diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index a05ad640bc895..3f483274dc918 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -79,6 +79,7 @@ export class AuthController { } @Public() + @UseNamedGuard('version') @Post('/preflight') async preflight( @Body() params?: { email: string } @@ -108,7 +109,7 @@ export class AuthController { } @Public() - @UseNamedGuard('captcha') + @UseNamedGuard('version', 'captcha') @Post('/sign-in') @Header('content-type', 'application/json') async signIn( @@ -260,6 +261,7 @@ export class AuthController { } @Public() + @UseNamedGuard('version') @Post('/magic-link') async magicLinkSignIn( @Req() req: Request, diff --git a/packages/backend/server/src/core/version/config.ts b/packages/backend/server/src/core/version/config.ts new file mode 100644 index 0000000000000..4fb9516b04144 --- /dev/null +++ b/packages/backend/server/src/core/version/config.ts @@ -0,0 +1,31 @@ +import { defineRuntimeConfig, ModuleConfig } from '../../base/config'; + +export interface VersionConfig { + versionControl: { + enabled: boolean; + requiredVersion: string; + }; +} + +declare module '../../base/config' { + interface AppConfig { + client: ModuleConfig; + } +} + +declare module '../../base/guard' { + interface RegisterGuardName { + version: 'version'; + } +} + +defineRuntimeConfig('client', { + 'versionControl.enabled': { + desc: 'Whether check version of client before accessing the server.', + default: false, + }, + 'versionControl.requiredVersion': { + desc: "Allowed version range of the app that allowed to access the server. Requires 'client/versionControl.enabled' to be true to take effect.", + default: '>=0.20.0', + }, +}); diff --git a/packages/backend/server/src/core/version/guard.ts b/packages/backend/server/src/core/version/guard.ts new file mode 100644 index 0000000000000..ce83dd99afb19 --- /dev/null +++ b/packages/backend/server/src/core/version/guard.ts @@ -0,0 +1,40 @@ +import type { + CanActivate, + ExecutionContext, + OnModuleInit, +} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; + +import { + getRequestResponseFromContext, + GuardProvider, + Runtime, +} from '../../base'; +import { VersionService } from './service'; + +@Injectable() +export class VersionGuardProvider + extends GuardProvider + implements CanActivate, OnModuleInit +{ + name = 'version' as const; + + constructor( + private readonly runtime: Runtime, + private readonly version: VersionService + ) { + super(); + } + + async canActivate(context: ExecutionContext) { + if (!(await this.runtime.fetch('client/versionControl.enabled'))) { + return true; + } + + const { req } = getRequestResponseFromContext(context); + + const version = req.headers['x-affine-version'] as string | undefined; + + return this.version.checkVersion(version); + } +} diff --git a/packages/backend/server/src/core/version/index.ts b/packages/backend/server/src/core/version/index.ts new file mode 100644 index 0000000000000..0ef83c14beebc --- /dev/null +++ b/packages/backend/server/src/core/version/index.ts @@ -0,0 +1,13 @@ +import './config'; + +import { Module } from '@nestjs/common'; + +import { VersionGuardProvider } from './guard'; +import { VersionService } from './service'; + +@Module({ + providers: [VersionService, VersionGuardProvider], +}) +export class VersionModule {} + +export type { VersionConfig } from './config'; diff --git a/packages/backend/server/src/core/version/service.ts b/packages/backend/server/src/core/version/service.ts new file mode 100644 index 0000000000000..385b596503e0b --- /dev/null +++ b/packages/backend/server/src/core/version/service.ts @@ -0,0 +1,59 @@ +import { Injectable, Logger } from '@nestjs/common'; +import semver from 'semver'; + +import { Runtime, UnsupportedClientVersion } from '../../base'; + +@Injectable() +export class VersionService { + private readonly logger = new Logger(VersionService.name); + + constructor(private readonly runtime: Runtime) {} + + async checkVersion(clientVersion?: string) { + const requiredVersion = await this.runtime.fetch( + 'client/versionControl.requiredVersion' + ); + + const range = await this.getVersionRange(requiredVersion); + if (!range) { + // ignore invalid allowed version config + return true; + } + + if (!clientVersion || !semver.satisfies(clientVersion, range)) { + throw new UnsupportedClientVersion({ + clientVersion: clientVersion ?? 'unset_or_invalid', + requiredVersion, + }); + } + + return true; + } + + private readonly cachedVersionRange = new Map< + string, + semver.Range | undefined + >(); + private async getVersionRange(versionRange: string) { + if (this.cachedVersionRange.has(versionRange)) { + return this.cachedVersionRange.get(versionRange); + } + + let range: semver.Range | undefined; + try { + range = new semver.Range(versionRange, { loose: false }); + if (!semver.validRange(range)) { + range = undefined; + } + } catch { + range = undefined; + } + + if (!range) { + this.logger.error(`invalid version range: ${versionRange}`); + } + + this.cachedVersionRange.set(versionRange, range); + return range; + } +} diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts index 2524e9c2cd151..a9033b9c18bd4 100644 --- a/packages/backend/server/src/plugins/oauth/controller.ts +++ b/packages/backend/server/src/plugins/oauth/controller.ts @@ -16,6 +16,7 @@ import { OauthAccountAlreadyConnected, OauthStateExpired, UnknownOauthProvider, + UseNamedGuard, } from '../../base'; import { AuthService, Public } from '../../core/auth'; import { Models } from '../../models'; @@ -34,6 +35,7 @@ export class OAuthController { ) {} @Public() + @UseNamedGuard('version') @Post('/preflight') @HttpCode(HttpStatus.OK) async preflight( @@ -63,6 +65,7 @@ export class OAuthController { } @Public() + @UseNamedGuard('version') @Post('/callback') @HttpCode(HttpStatus.OK) async callback( diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 4fb89a016a570..8e614e0b1b6db 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -316,7 +316,7 @@ type EditorType { name: String! } -union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType +union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToMatchContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType enum ErrorNames { ACCESS_DENIED @@ -409,6 +409,7 @@ enum ErrorNames { TOO_MANY_REQUEST UNKNOWN_OAUTH_PROVIDER UNSPLASH_IS_NOT_CONFIGURED + UNSUPPORTED_CLIENT_VERSION UNSUPPORTED_SUBSCRIPTION_PLAN USER_AVATAR_NOT_FOUND USER_NOT_FOUND @@ -1101,6 +1102,11 @@ type UnknownOauthProviderDataType { name: String! } +type UnsupportedClientVersionDataType { + clientVersion: String! + requiredVersion: String! +} + type UnsupportedSubscriptionPlanDataType { plan: String! } diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 697a488c3be7d..b8052e050c874 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -426,6 +426,7 @@ export type ErrorDataUnion = | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType + | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType @@ -523,6 +524,7 @@ export enum ErrorNames { TOO_MANY_REQUEST = 'TOO_MANY_REQUEST', UNKNOWN_OAUTH_PROVIDER = 'UNKNOWN_OAUTH_PROVIDER', UNSPLASH_IS_NOT_CONFIGURED = 'UNSPLASH_IS_NOT_CONFIGURED', + UNSUPPORTED_CLIENT_VERSION = 'UNSUPPORTED_CLIENT_VERSION', UNSUPPORTED_SUBSCRIPTION_PLAN = 'UNSUPPORTED_SUBSCRIPTION_PLAN', USER_AVATAR_NOT_FOUND = 'USER_AVATAR_NOT_FOUND', USER_NOT_FOUND = 'USER_NOT_FOUND', @@ -1534,6 +1536,12 @@ export interface UnknownOauthProviderDataType { name: Scalars['String']['output']; } +export interface UnsupportedClientVersionDataType { + __typename?: 'UnsupportedClientVersionDataType'; + clientVersion: Scalars['String']['output']; + requiredVersion: Scalars['String']['output']; +} + export interface UnsupportedSubscriptionPlanDataType { __typename?: 'UnsupportedSubscriptionPlanDataType'; plan: Scalars['String']['output']; diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 1b019ee075ec6..12b67db7dca5c 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7503,6 +7503,13 @@ export function useAFFiNEI18N(): { ["error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE"](options: { readonly limit: string; }): string; + /** + * `Unsupported client with version [{{clientVersion}}], required version is [{{requiredVersion}}].` + */ + ["error.UNSUPPORTED_CLIENT_VERSION"](options: Readonly<{ + clientVersion: string; + requiredVersion: string; + }>): string; } { const { t } = useTranslation(); return useMemo(() => createProxy((key) => t.bind(null, key)), [t]); } function createComponent(i18nKey: string) { return (props) => createElement(Trans, { i18nKey, shouldUnescape: true, ...props }); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 83c10cc3e9834..2038bc928ee06 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1843,5 +1843,6 @@ "error.LICENSE_NOT_FOUND": "License not found.", "error.INVALID_LICENSE_TO_ACTIVATE": "Invalid license to activate.", "error.INVALID_LICENSE_UPDATE_PARAMS": "Invalid license update params. {{reason}}", - "error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE": "You cannot downgrade the workspace from team workspace because there are more than {{limit}} members that are currently active." + "error.WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE": "You cannot downgrade the workspace from team workspace because there are more than {{limit}} members that are currently active.", + "error.UNSUPPORTED_CLIENT_VERSION": "Unsupported client with version [{{clientVersion}}], required version is [{{requiredVersion}}]." } diff --git a/yarn.lock b/yarn.lock index 62c12fc0e7147..5924c88a0a49e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -831,6 +831,7 @@ __metadata: "@types/on-headers": "npm:^1.0.3" "@types/react": "npm:^19.0.1" "@types/react-dom": "npm:^19.0.2" + "@types/semver": "npm:^7.5.8" "@types/sinon": "npm:^17.0.3" "@types/supertest": "npm:^6.0.2" ava: "npm:^6.2.0" @@ -870,6 +871,7 @@ __metadata: react-email: "npm:3.0.7" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.8.1" + semver: "npm:^7.6.3" ses: "npm:^1.10.0" sinon: "npm:^19.0.2" socket.io: "npm:^4.8.1" @@ -15527,6 +15529,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.5.8": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 + languageName: node + linkType: hard + "@types/send@npm:*": version: 0.17.4 resolution: "@types/send@npm:0.17.4"