diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 69279d3b87c06..e08a94b773ac0 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -600,8 +600,12 @@ export const USER_FRIENDLY_ERRORS = { // version errors unsupported_client_version: { type: 'action_forbidden', - args: { minVersion: 'string' }, - message: ({ minVersion }) => - `Unsupported client version. Please upgrade to ${minVersion}.`, + args: { + clientVersion: 'string', + recommendedVersion: 'string', + action: 'string', + }, + message: ({ clientVersion, recommendedVersion, action }) => + `Unsupported client version: ${clientVersion}, please ${action} to ${recommendedVersion}.`, }, } 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 553be54c345b3..37a56fbe4b5a5 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -1,4 +1,4 @@ -/* eslint-disable */ + // AUTO GENERATED FILE import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql'; @@ -599,7 +599,9 @@ export class CaptchaVerificationFailed extends UserFriendlyError { } @ObjectType() class UnsupportedClientVersionDataType { - @Field() minVersion!: string + @Field() clientVersion!: string + @Field() recommendedVersion!: string + @Field() action!: string } export class UnsupportedClientVersion extends UserFriendlyError { diff --git a/packages/backend/server/src/core/version/config.ts b/packages/backend/server/src/core/version/config.ts index 57fec65602775..3ea427cf5dcc1 100644 --- a/packages/backend/server/src/core/version/config.ts +++ b/packages/backend/server/src/core/version/config.ts @@ -2,7 +2,7 @@ import { defineRuntimeConfig, ModuleConfig } from '../../base/config'; export interface VersionConfig { enable: boolean; - minVersion: string; + allowedVersion: string; } declare module '../../base/config' { @@ -22,8 +22,8 @@ defineRuntimeConfig('version', { desc: 'Check version of the app', default: false, }, - minVersion: { - desc: 'Minimum version of the app that can access the server', - default: '0.0.0', + allowedVersion: { + desc: 'Allowed version range of the app that can access the server', + default: '>=0.0.1', }, }); diff --git a/packages/backend/server/src/core/version/service.ts b/packages/backend/server/src/core/version/service.ts index fcda54787a299..66f3c82f4dcfe 100644 --- a/packages/backend/server/src/core/version/service.ts +++ b/packages/backend/server/src/core/version/service.ts @@ -11,38 +11,46 @@ export class VersionService { constructor(private readonly runtime: Runtime) {} - private async getRecommendedVersion(minVersion: string) { + private async getRecommendedVersion(versionRange: string) { try { - const range = new semver.Range(minVersion); + const range = new semver.Range(versionRange); const versions = range.set .flat() .map(c => c.semver) .toSorted((a, b) => semver.rcompare(a, b)); return versions[0]?.toString(); } catch { - return semver.valid(semver.coerce(minVersion)); + return semver.valid(semver.coerce(versionRange)); } } async checkVersion(clientVersion?: any) { - const minVersion = await this.runtime.fetch('version/minVersion'); - const readableMinVersion = await this.getRecommendedVersion(minVersion); - if (!minVersion || !readableMinVersion) { - // ignore invalid min version config + const allowedVersion = await this.runtime.fetch('version/allowedVersion'); + const recommendedVersion = await this.getRecommendedVersion(allowedVersion); + if (!allowedVersion || !recommendedVersion) { + // ignore invalid allowed version config return true; } + const parsedClientVersion = semver.valid(clientVersion); + const action = semver.lt(parsedClientVersion || '0.0.0', recommendedVersion) + ? 'upgrade' + : 'downgrade'; assert( typeof clientVersion === 'string' && clientVersion.length > 0, new UnsupportedClientVersion({ - minVersion: readableMinVersion, + clientVersion: '[Not Provided]', + recommendedVersion, + action, }) ); - if (semver.valid(clientVersion)) { - if (!semver.satisfies(clientVersion, minVersion)) { + if (parsedClientVersion) { + if (!semver.satisfies(parsedClientVersion, allowedVersion)) { throw new UnsupportedClientVersion({ - minVersion: readableMinVersion, + clientVersion, + recommendedVersion, + action, }); } return true; @@ -51,7 +59,9 @@ export class VersionService { this.logger.warn(`Invalid client version: ${clientVersion}`); } throw new UnsupportedClientVersion({ - minVersion: readableMinVersion, + clientVersion, + recommendedVersion, + action, }); } } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index c8dc16a95c6bb..8ca8867f6397b 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -864,7 +864,9 @@ type UnknownOauthProviderDataType { } type UnsupportedClientVersionDataType { - minVersion: String! + action: String! + clientVersion: String! + recommendedVersion: String! } type UnsupportedSubscriptionPlanDataType { diff --git a/packages/backend/server/tests/version.spec.ts b/packages/backend/server/tests/version.spec.ts index ce367ba8fea1e..bb67c1397668e 100644 --- a/packages/backend/server/tests/version.spec.ts +++ b/packages/backend/server/tests/version.spec.ts @@ -40,9 +40,9 @@ test.beforeEach(async t => { await initTestingDB(t.context.app.get(PrismaClient)); // reset runtime await t.context.runtime.loadDb('version/enable'); - await t.context.runtime.loadDb('version/minVersion'); + await t.context.runtime.loadDb('version/allowedVersion'); await t.context.runtime.set('version/enable', false); - await t.context.runtime.set('version/minVersion', '0.0.0'); + await t.context.runtime.set('version/allowedVersion', '>=0.0.1'); }); test.after.always(async t => { @@ -80,46 +80,138 @@ test('should be able to prevent requests if version outdated', async t => { await runtime.set('version/enable', true); await t.throwsAsync( fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.FORBIDDEN), - { message: 'Unsupported client version. Please upgrade to 0.0.0.' }, + { + message: + 'Unsupported client version: [Not Provided], please upgrade to 0.0.1.', + }, + 'should check version exists' + ); + await t.throwsAsync( + fetchWithVersion( + app.getHttpServer(), + 'not_a_version', + HttpStatus.FORBIDDEN + ), + { + message: + 'Unsupported client version: not_a_version, please upgrade to 0.0.1.', + }, 'should check version exists' ); await t.notThrowsAsync( - fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.OK), + fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK), 'should check version exists' ); } { - await runtime.set('version/minVersion', 'unknownVersion'); + await runtime.set('version/allowedVersion', 'unknownVersion'); await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK), 'should not check version if invalid minVersion provided' ); await t.notThrowsAsync( - fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.OK), + fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK), 'should not check version if invalid minVersion provided' ); - await runtime.set('version/minVersion', '0.0.1'); + await runtime.set('version/allowedVersion', '0.0.1'); await t.throwsAsync( fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.FORBIDDEN), - { message: 'Unsupported client version. Please upgrade to 0.0.1.' }, + { + message: 'Unsupported client version: 0.0.0, please upgrade to 0.0.1.', + }, 'should reject version if valid minVersion provided' ); - await runtime.set('version/minVersion', '0.0.5 || >=0.0.7'); + await runtime.set( + 'version/allowedVersion', + '0.17.5 || >=0.18.0-nightly || >=0.18.0' + ); await t.notThrowsAsync( - fetchWithVersion(app.getHttpServer(), '0.0.5', HttpStatus.OK), + fetchWithVersion(app.getHttpServer(), '0.17.5', HttpStatus.OK), 'should pass version if version satisfies minVersion' ); await t.throwsAsync( - fetchWithVersion(app.getHttpServer(), '0.0.6', HttpStatus.FORBIDDEN), - { message: 'Unsupported client version. Please upgrade to 0.0.7.' }, + fetchWithVersion(app.getHttpServer(), '0.17.4', HttpStatus.FORBIDDEN), + { + message: + 'Unsupported client version: 0.17.4, please upgrade to 0.18.0.', + }, 'should reject version if valid minVersion provided' ); + await t.throwsAsync( + fetchWithVersion( + app.getHttpServer(), + '0.17.6-nightly-f0d99f4', + HttpStatus.FORBIDDEN + ), + { + message: + 'Unsupported client version: 0.17.6-nightly-f0d99f4, please upgrade to 0.18.0.', + }, + 'should reject version if valid minVersion provided' + ); + await t.notThrowsAsync( + fetchWithVersion( + app.getHttpServer(), + '0.18.0-nightly-cc9b38c', + HttpStatus.OK + ), + 'should pass version if version satisfies minVersion' + ); await t.notThrowsAsync( - fetchWithVersion(app.getHttpServer(), '0.1.0', HttpStatus.OK), + fetchWithVersion(app.getHttpServer(), '0.18.1', HttpStatus.OK), 'should pass version if version satisfies minVersion' ); } + + { + await runtime.set( + 'version/allowedVersion', + '>=0.0.1 <=0.1.2 || ^0.2.0-nightly <0.2.0 || 0.3.0' + ); + + await t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK), + 'should pass version if version satisfies minVersion' + ); + await t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), '0.1.2', HttpStatus.OK), + 'should pass version if version satisfies maxVersion' + ); + await t.throwsAsync( + fetchWithVersion(app.getHttpServer(), '0.1.3', HttpStatus.FORBIDDEN), + { + message: 'Unsupported client version: 0.1.3, please upgrade to 0.3.0.', + }, + 'should reject version if valid maxVersion provided' + ); + + await t.notThrowsAsync( + fetchWithVersion( + app.getHttpServer(), + '0.2.0-nightly-cc9b38c', + HttpStatus.OK + ), + 'should pass version if version satisfies maxVersion' + ); + + await t.throwsAsync( + fetchWithVersion(app.getHttpServer(), '0.2.0', HttpStatus.FORBIDDEN), + { + message: 'Unsupported client version: 0.2.0, please upgrade to 0.3.0.', + }, + 'should reject version if valid maxVersion provided' + ); + + await t.throwsAsync( + fetchWithVersion(app.getHttpServer(), '0.3.1', HttpStatus.FORBIDDEN), + { + message: + 'Unsupported client version: 0.3.1, please downgrade to 0.3.0.', + }, + 'should reject version if valid maxVersion provided' + ); + } });