From a7c42779ef332a464cb6a428618125fb9c79febe Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 19 Dec 2024 11:40:27 +0800 Subject: [PATCH 01/11] feat: init version check --- packages/backend/server/package.json | 2 + packages/backend/server/src/base/error/def.ts | 7 +++ .../server/src/base/error/errors.gen.ts | 15 ++++++- .../backend/server/src/base/guard/guard.ts | 15 ++++--- .../server/src/core/auth/controller.ts | 2 +- .../backend/server/src/core/version/config.ts | 29 ++++++++++++ .../backend/server/src/core/version/guard.ts | 40 +++++++++++++++++ .../backend/server/src/core/version/index.ts | 13 ++++++ .../server/src/core/version/service.ts | 45 +++++++++++++++++++ packages/backend/server/src/schema.gql | 7 ++- yarn.lock | 9 ++++ 11 files changed, 175 insertions(+), 9 deletions(-) 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 53d8055427f15..40176aef4cc77 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -82,6 +82,7 @@ "prisma": "^5.22.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", @@ -104,6 +105,7 @@ "@types/node": "^20.17.10", "@types/nodemailer": "^6.4.17", "@types/on-headers": "^1.0.3", + "@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/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 79345ef300d0e..40592960cd9ab 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -593,4 +593,11 @@ export const USER_FRIENDLY_ERRORS = { type: 'bad_request', message: 'Captcha verification failed.', }, + // version errors + unsupported_client_version: { + type: 'action_forbidden', + args: { minVersion: 'string' }, + message: ({ minVersion }) => + `Unsupported client version. Please upgrade to ${minVersion}.`, + }, } 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 a17feb5b84ac5..53da139eb56b3 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -591,6 +591,16 @@ export class CaptchaVerificationFailed extends UserFriendlyError { super('bad_request', 'captcha_verification_failed', message); } } +@ObjectType() +class UnsupportedClientVersionDataType { + @Field() minVersion!: 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, @@ -669,7 +679,8 @@ export enum ErrorNames { MAILER_SERVICE_IS_NOT_CONFIGURED, CANNOT_DELETE_ALL_ADMIN_ACCOUNT, CANNOT_DELETE_OWN_ACCOUNT, - CAPTCHA_VERIFICATION_FAILED + CAPTCHA_VERIFICATION_FAILED, + UNSUPPORTED_CLIENT_VERSION } registerEnumType(ErrorNames, { name: 'ErrorNames' @@ -678,5 +689,5 @@ registerEnumType(ErrorNames, { export const ErrorDataUnionType = createUnionType({ name: 'ErrorDataUnion', types: () => - [WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const, + [WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, 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 dd954eeb9c812..013bb61144919 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -103,7 +103,7 @@ export class AuthController { } @Public() - @UseNamedGuard('captcha') + @UseNamedGuard('version', 'captcha') @Post('/sign-in') @Header('content-type', 'application/json') async signIn( 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..57fec65602775 --- /dev/null +++ b/packages/backend/server/src/core/version/config.ts @@ -0,0 +1,29 @@ +import { defineRuntimeConfig, ModuleConfig } from '../../base/config'; + +export interface VersionConfig { + enable: boolean; + minVersion: string; +} + +declare module '../../base/config' { + interface AppConfig { + version: ModuleConfig; + } +} + +declare module '../../base/guard' { + interface RegisterGuardName { + version: 'version'; + } +} + +defineRuntimeConfig('version', { + enable: { + desc: 'Check version of the app', + default: false, + }, + minVersion: { + desc: 'Minimum version of the app that can access the server', + default: '0.0.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..c8205b0a9a741 --- /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('version/enable'))) { + return true; + } + + const { req } = getRequestResponseFromContext(context); + + const version = req.headers['x-affine-version']; + + return await 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..c367496ca4223 --- /dev/null +++ b/packages/backend/server/src/core/version/service.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert'; + +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?: any) { + const minVersion = await this.runtime.fetch('version/minVersion'); + const readableMinVersion = semver.valid(semver.coerce(minVersion)); + if (!minVersion || !readableMinVersion) { + // ignore invalid min version config + return true; + } + + assert( + typeof clientVersion === 'string' && clientVersion.length > 0, + new UnsupportedClientVersion({ + minVersion: readableMinVersion, + }) + ); + + if (semver.valid(clientVersion)) { + if (!semver.satisfies(clientVersion, minVersion)) { + throw new UnsupportedClientVersion({ + minVersion: readableMinVersion, + }); + } + return true; + } else { + if (clientVersion) { + this.logger.warn(`Invalid client version: ${clientVersion}`); + } + throw new UnsupportedClientVersion({ + minVersion: readableMinVersion, + }); + } + } +} diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 3ec9dee256c99..51c2fda591007 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -209,7 +209,7 @@ type EditorType { name: String! } -union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType +union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType enum ErrorNames { ACCESS_DENIED @@ -282,6 +282,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 @@ -861,6 +862,10 @@ type UnknownOauthProviderDataType { name: String! } +type UnsupportedClientVersionDataType { + minVersion: String! +} + type UnsupportedSubscriptionPlanDataType { plan: String! } diff --git a/yarn.lock b/yarn.lock index 08b98ec18ebed..034623b486eed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -779,6 +779,7 @@ __metadata: "@types/node": "npm:^20.17.10" "@types/nodemailer": "npm:^6.4.17" "@types/on-headers": "npm:^1.0.3" + "@types/semver": "npm:^7.5.8" "@types/sinon": "npm:^17.0.3" "@types/supertest": "npm:^6.0.2" ava: "npm:^6.2.0" @@ -809,6 +810,7 @@ __metadata: prisma: "npm:^5.22.0" 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" @@ -14904,6 +14906,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" From 6b83ce6f07e1ab09b4ffc7361256873cf9f392c7 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 19 Dec 2024 12:42:47 +0800 Subject: [PATCH 02/11] feat: add test --- packages/backend/server/tests/version.spec.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/backend/server/tests/version.spec.ts diff --git a/packages/backend/server/tests/version.spec.ts b/packages/backend/server/tests/version.spec.ts new file mode 100644 index 0000000000000..15df86f064228 --- /dev/null +++ b/packages/backend/server/tests/version.spec.ts @@ -0,0 +1,90 @@ +import '../src/core/version/config'; + +import { Controller, Get, HttpStatus, INestApplication } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import ava, { TestFn } from 'ava'; +import request from 'supertest'; + +import { AppModule } from '../src/app.module'; +import { Runtime, UseNamedGuard } from '../src/base'; +import { createTestingApp, initTestingDB } from './utils'; + +const test = ava as TestFn<{ + runtime: Runtime; + cookie: string; + app: INestApplication; +}>; + +@UseNamedGuard() +@Controller('/guarded') +class GuardedController { + @Get('/test') + test() { + return 'test'; + } +} + +test.before(async t => { + const { app } = await createTestingApp({ + imports: [AppModule], + controllers: [GuardedController], + }); + + t.context.runtime = app.get(Runtime); + t.context.app = app; +}); + +test.beforeEach(async t => { + await initTestingDB(t.context.app.get(PrismaClient)); +}); + +test.after.always(async t => { + await t.context.app.close(); +}); + +async function fetchWithVersion( + server: any, + version: string | undefined, + status: number +) { + let req = request(server).get('/guarded/test'); + if (version) { + req = req.set({ 'x-affine-version': version }); + } + return req.expect(status); +} + +test('should be able to prevent requests if version outdated', async t => { + const { app, runtime } = t.context; + + { + await runtime.set('version/enable', false); + t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK), + 'should not check version if disabled' + ); + } + + { + await runtime.set('version/enable', true); + t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK), + 'should not check version if disabled' + ); + } + + { + await runtime.set('version/minVersion', 'unknownVersion'); + t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.OK), + 'should not check version if invalid minVersion provided' + ); + + await runtime.set('version/minVersion', '0.0.1'); + t.throwsAsync( + fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.FORBIDDEN), + { message: 'Version outdated' }, + 'should check version if valid minVersion provided' + ); + } +}); From c07dec8fbed71258c2e95a219e397eee9ae0c312 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 19 Dec 2024 17:11:37 +0800 Subject: [PATCH 03/11] feat: more detailed test case --- packages/backend/server/src/app.module.ts | 2 + .../server/src/core/version/service.ts | 22 +++++++-- packages/backend/server/tests/version.spec.ts | 45 +++++++++++++++---- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index d6764ab9c203d..6ad38902504c7 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -35,6 +35,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 { REGISTERED_PLUGINS } from './plugins'; import { ENABLED_PLUGINS } from './plugins/registry'; @@ -167,6 +168,7 @@ export function buildAppModule() { .useIf( config => config.flavor.graphql, ScheduleModule.forRoot(), + VersionModule, GqlModule, StorageModule, ServerConfigModule, diff --git a/packages/backend/server/src/core/version/service.ts b/packages/backend/server/src/core/version/service.ts index c367496ca4223..5b93e59522e1e 100644 --- a/packages/backend/server/src/core/version/service.ts +++ b/packages/backend/server/src/core/version/service.ts @@ -11,10 +11,24 @@ export class VersionService { constructor(private readonly runtime: Runtime) {} - async checkVersion(clientVersion?: any) { + private async getRecommendedVersion() { const minVersion = await this.runtime.fetch('version/minVersion'); - const readableMinVersion = semver.valid(semver.coerce(minVersion)); - if (!minVersion || !readableMinVersion) { + + try { + const range = new semver.Range(minVersion); + 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)); + } + } + + async checkVersion(clientVersion?: any) { + const readableMinVersion = await this.getRecommendedVersion(); + if (!readableMinVersion) { // ignore invalid min version config return true; } @@ -27,7 +41,7 @@ export class VersionService { ); if (semver.valid(clientVersion)) { - if (!semver.satisfies(clientVersion, minVersion)) { + if (!semver.satisfies(clientVersion, readableMinVersion)) { throw new UnsupportedClientVersion({ minVersion: readableMinVersion, }); diff --git a/packages/backend/server/tests/version.spec.ts b/packages/backend/server/tests/version.spec.ts index 15df86f064228..f15e948c017e4 100644 --- a/packages/backend/server/tests/version.spec.ts +++ b/packages/backend/server/tests/version.spec.ts @@ -7,6 +7,7 @@ import request from 'supertest'; import { AppModule } from '../src/app.module'; import { Runtime, UseNamedGuard } from '../src/base'; +import { Public } from '../src/core/auth/guard'; import { createTestingApp, initTestingDB } from './utils'; const test = ava as TestFn<{ @@ -15,9 +16,10 @@ const test = ava as TestFn<{ app: INestApplication; }>; -@UseNamedGuard() +@Public() @Controller('/guarded') class GuardedController { + @UseNamedGuard('version') @Get('/test') test() { return 'test'; @@ -36,6 +38,11 @@ test.before(async t => { 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.set('version/enable', false); + await t.context.runtime.set('version/minVersion', '0.0.0'); }); test.after.always(async t => { @@ -51,7 +58,11 @@ async function fetchWithVersion( if (version) { req = req.set({ 'x-affine-version': version }); } - return req.expect(status); + const res = await req.expect(status); + if (res.body.message) { + throw new Error(res.body.message); + } + return res; } test('should be able to prevent requests if version outdated', async t => { @@ -59,7 +70,7 @@ test('should be able to prevent requests if version outdated', async t => { { await runtime.set('version/enable', false); - t.notThrowsAsync( + await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK), 'should not check version if disabled' ); @@ -67,23 +78,39 @@ test('should be able to prevent requests if version outdated', async t => { { await runtime.set('version/enable', true); - t.notThrowsAsync( - fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK), - 'should not check version if disabled' + await t.throwsAsync( + fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.FORBIDDEN), + { message: 'Unsupported client version. Please upgrade to 0.0.0.' }, + 'should check version exists' + ); + await t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.OK), + 'should check version exists' ); } { await runtime.set('version/minVersion', 'unknownVersion'); - t.notThrowsAsync( + 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), 'should not check version if invalid minVersion provided' ); await runtime.set('version/minVersion', '0.0.1'); - t.throwsAsync( + await t.throwsAsync( fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.FORBIDDEN), - { message: 'Version outdated' }, + { message: 'Unsupported client version. Please upgrade to 0.0.1.' }, + 'should check version if valid minVersion provided' + ); + + await runtime.set('version/minVersion', '0.0.5 || >=0.0.7'); + await t.throwsAsync( + fetchWithVersion(app.getHttpServer(), '0.0.6', HttpStatus.FORBIDDEN), + { message: 'Unsupported client version. Please upgrade to 0.0.7.' }, 'should check version if valid minVersion provided' ); } From b026c3eb186d8e1848eaff2e8af3615bfcd22799 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 19 Dec 2024 17:34:19 +0800 Subject: [PATCH 04/11] chore: adjust comment --- packages/backend/server/tests/version.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/backend/server/tests/version.spec.ts b/packages/backend/server/tests/version.spec.ts index f15e948c017e4..ce367ba8fea1e 100644 --- a/packages/backend/server/tests/version.spec.ts +++ b/packages/backend/server/tests/version.spec.ts @@ -104,14 +104,22 @@ test('should be able to prevent requests if version outdated', async t => { await t.throwsAsync( fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.FORBIDDEN), { message: 'Unsupported client version. Please upgrade to 0.0.1.' }, - 'should check version if valid minVersion provided' + 'should reject version if valid minVersion provided' ); await runtime.set('version/minVersion', '0.0.5 || >=0.0.7'); + await t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), '0.0.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.' }, - 'should check version if valid minVersion provided' + 'should reject version if valid minVersion provided' + ); + await t.notThrowsAsync( + fetchWithVersion(app.getHttpServer(), '0.1.0', HttpStatus.OK), + 'should pass version if version satisfies minVersion' ); } }); From 6a55ec4fe074965daf5cc3b014831ac63f1cb410 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 19 Dec 2024 17:47:16 +0800 Subject: [PATCH 05/11] feat: check with min version --- packages/backend/server/src/core/version/service.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/backend/server/src/core/version/service.ts b/packages/backend/server/src/core/version/service.ts index 5b93e59522e1e..fcda54787a299 100644 --- a/packages/backend/server/src/core/version/service.ts +++ b/packages/backend/server/src/core/version/service.ts @@ -11,9 +11,7 @@ export class VersionService { constructor(private readonly runtime: Runtime) {} - private async getRecommendedVersion() { - const minVersion = await this.runtime.fetch('version/minVersion'); - + private async getRecommendedVersion(minVersion: string) { try { const range = new semver.Range(minVersion); const versions = range.set @@ -27,8 +25,9 @@ export class VersionService { } async checkVersion(clientVersion?: any) { - const readableMinVersion = await this.getRecommendedVersion(); - if (!readableMinVersion) { + const minVersion = await this.runtime.fetch('version/minVersion'); + const readableMinVersion = await this.getRecommendedVersion(minVersion); + if (!minVersion || !readableMinVersion) { // ignore invalid min version config return true; } @@ -41,7 +40,7 @@ export class VersionService { ); if (semver.valid(clientVersion)) { - if (!semver.satisfies(clientVersion, readableMinVersion)) { + if (!semver.satisfies(clientVersion, minVersion)) { throw new UnsupportedClientVersion({ minVersion: readableMinVersion, }); From 77660085047210acac82a4f48a9b0ea9802913a5 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 19 Dec 2024 19:11:03 +0800 Subject: [PATCH 06/11] feat: add real world version test --- packages/backend/server/src/base/error/def.ts | 10 +- .../server/src/base/error/errors.gen.ts | 4 +- .../backend/server/src/core/version/config.ts | 8 +- .../server/src/core/version/service.ts | 34 +++-- packages/backend/server/src/schema.gql | 4 +- packages/backend/server/tests/version.spec.ts | 118 ++++++++++++++++-- 6 files changed, 144 insertions(+), 34 deletions(-) diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 40592960cd9ab..93b5db6689023 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -596,8 +596,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 53da139eb56b3..426371d18bea6 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -593,7 +593,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 51c2fda591007..26611bae5fa0c 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -863,7 +863,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' + ); + } }); From f95fbe9ba0179022da734b1d48c4df530210a6d6 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Fri, 20 Dec 2024 15:33:21 +0800 Subject: [PATCH 07/11] chore: cleanup codes --- .../server/src/core/auth/controller.ts | 2 ++ .../backend/server/src/core/version/guard.ts | 2 +- .../server/src/plugins/oauth/controller.ts | 3 ++ packages/backend/server/tests/version.spec.ts | 28 +++++++++---------- packages/frontend/graphql/src/schema.ts | 9 ++++++ 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index 013bb61144919..9b95f0b3201bf 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -72,6 +72,7 @@ export class AuthController { } @Public() + @UseNamedGuard('version') @Post('/preflight') async preflight( @Body() params?: { email: string } @@ -236,6 +237,7 @@ export class AuthController { } @Public() + @UseNamedGuard('version') @Post('/magic-link') async magicLinkSignIn( @Req() req: Request, diff --git a/packages/backend/server/src/core/version/guard.ts b/packages/backend/server/src/core/version/guard.ts index c8205b0a9a741..916256a5973b4 100644 --- a/packages/backend/server/src/core/version/guard.ts +++ b/packages/backend/server/src/core/version/guard.ts @@ -35,6 +35,6 @@ export class VersionGuardProvider const version = req.headers['x-affine-version']; - return await this.version.checkVersion(version); + return this.version.checkVersion(version); } } diff --git a/packages/backend/server/src/plugins/oauth/controller.ts b/packages/backend/server/src/plugins/oauth/controller.ts index f38eaa2eb0992..fd9c59fe5a962 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 { UserService } from '../../core/user'; @@ -35,6 +36,7 @@ export class OAuthController { ) {} @Public() + @UseNamedGuard('version') @Post('/preflight') @HttpCode(HttpStatus.OK) async preflight( @@ -64,6 +66,7 @@ export class OAuthController { } @Public() + @UseNamedGuard('version') @Post('/callback') @HttpCode(HttpStatus.OK) async callback( diff --git a/packages/backend/server/tests/version.spec.ts b/packages/backend/server/tests/version.spec.ts index bb67c1397668e..0c5cf48ab10d9 100644 --- a/packages/backend/server/tests/version.spec.ts +++ b/packages/backend/server/tests/version.spec.ts @@ -108,11 +108,11 @@ test('should be able to prevent requests if version outdated', async t => { await runtime.set('version/allowedVersion', 'unknownVersion'); await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK), - 'should not check version if invalid minVersion provided' + 'should not check version if invalid allowedVersion provided' ); await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK), - 'should not check version if invalid minVersion provided' + 'should not check version if invalid allowedVersion provided' ); await runtime.set('version/allowedVersion', '0.0.1'); @@ -121,7 +121,7 @@ test('should be able to prevent requests if version outdated', async t => { { message: 'Unsupported client version: 0.0.0, please upgrade to 0.0.1.', }, - 'should reject version if valid minVersion provided' + 'should reject version if valid allowedVersion provided' ); await runtime.set( @@ -130,7 +130,7 @@ test('should be able to prevent requests if version outdated', async t => { ); await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), '0.17.5', HttpStatus.OK), - 'should pass version if version satisfies minVersion' + 'should pass version if version satisfies allowedVersion' ); await t.throwsAsync( fetchWithVersion(app.getHttpServer(), '0.17.4', HttpStatus.FORBIDDEN), @@ -138,7 +138,7 @@ test('should be able to prevent requests if version outdated', async t => { message: 'Unsupported client version: 0.17.4, please upgrade to 0.18.0.', }, - 'should reject version if valid minVersion provided' + 'should reject version if valid allowedVersion provided' ); await t.throwsAsync( fetchWithVersion( @@ -150,7 +150,7 @@ test('should be able to prevent requests if version outdated', async t => { message: 'Unsupported client version: 0.17.6-nightly-f0d99f4, please upgrade to 0.18.0.', }, - 'should reject version if valid minVersion provided' + 'should reject version if valid allowedVersion provided' ); await t.notThrowsAsync( fetchWithVersion( @@ -158,11 +158,11 @@ test('should be able to prevent requests if version outdated', async t => { '0.18.0-nightly-cc9b38c', HttpStatus.OK ), - 'should pass version if version satisfies minVersion' + 'should pass version if version satisfies allowedVersion' ); await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), '0.18.1', HttpStatus.OK), - 'should pass version if version satisfies minVersion' + 'should pass version if version satisfies allowedVersion' ); } @@ -174,18 +174,18 @@ test('should be able to prevent requests if version outdated', async t => { await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK), - 'should pass version if version satisfies minVersion' + 'should pass version if version satisfies allowedVersion' ); await t.notThrowsAsync( fetchWithVersion(app.getHttpServer(), '0.1.2', HttpStatus.OK), - 'should pass version if version satisfies maxVersion' + 'should pass version if version satisfies allowedVersion' ); 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' + 'should reject version if valid allowedVersion provided' ); await t.notThrowsAsync( @@ -194,7 +194,7 @@ test('should be able to prevent requests if version outdated', async t => { '0.2.0-nightly-cc9b38c', HttpStatus.OK ), - 'should pass version if version satisfies maxVersion' + 'should pass version if version satisfies allowedVersion' ); await t.throwsAsync( @@ -202,7 +202,7 @@ test('should be able to prevent requests if version outdated', async t => { { message: 'Unsupported client version: 0.2.0, please upgrade to 0.3.0.', }, - 'should reject version if valid maxVersion provided' + 'should reject version if valid allowedVersion provided' ); await t.throwsAsync( @@ -211,7 +211,7 @@ test('should be able to prevent requests if version outdated', async t => { message: 'Unsupported client version: 0.3.1, please downgrade to 0.3.0.', }, - 'should reject version if valid maxVersion provided' + 'should reject version if valid allowedVersion provided' ); } }); diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index cc2b381f7ddee..8d2ad27fc7a4f 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -285,6 +285,7 @@ export type ErrorDataUnion = | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType + | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType; @@ -360,6 +361,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', @@ -1207,6 +1209,13 @@ export interface UnknownOauthProviderDataType { name: Scalars['String']['output']; } +export interface UnsupportedClientVersionDataType { + __typename?: 'UnsupportedClientVersionDataType'; + action: Scalars['String']['output']; + clientVersion: Scalars['String']['output']; + recommendedVersion: Scalars['String']['output']; +} + export interface UnsupportedSubscriptionPlanDataType { __typename?: 'UnsupportedSubscriptionPlanDataType'; plan: Scalars['String']['output']; From ccd01cb08af2d0ae4877afd9a3166329c0efadd2 Mon Sep 17 00:00:00 2001 From: Jimmfly Date: Fri, 20 Dec 2024 12:27:10 +0800 Subject: [PATCH 08/11] feat(core): add version check to sign in --- .../sign-in/sign-in-with-password.tsx | 22 +++++++++++++++- .../src/desktop/pages/auth/magic-link.tsx | 19 +++++++++++--- .../src/desktop/pages/auth/oauth-callback.tsx | 20 +++++++++++--- .../core/src/desktop/pages/auth/sign-in.tsx | 26 ++++++++++++++++++- .../i18n/src/i18n-completenesses.json | 6 ++--- packages/frontend/i18n/src/resources/en.json | 7 ++++- 6 files changed, 88 insertions(+), 12 deletions(-) diff --git a/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx b/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx index 30b00e1c5859f..33dc21c43b6b4 100644 --- a/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx @@ -9,14 +9,20 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo import { AuthService, CaptchaService, + isBackendError, ServerService, } from '@affine/core/modules/cloud'; import { Unreachable } from '@affine/env/constant'; -import { ServerDeploymentType } from '@affine/graphql'; +import { + ErrorNames, + ServerDeploymentType, + UserFriendlyError, +} from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import type { Dispatch, SetStateAction } from 'react'; import { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import type { SignInState } from '.'; import { Captcha } from './captcha'; @@ -59,6 +65,7 @@ export const SignInWithPasswordStep = ({ const [isLoading, setIsLoading] = useState(false); const loginStatus = useLiveData(authService.session.status$); + const nav = useNavigate(); useEffect(() => { if (loginStatus === 'authenticated') { @@ -84,6 +91,18 @@ export const SignInWithPasswordStep = ({ }); } catch (err) { console.error(err); + if ( + err instanceof Error && + isBackendError(err) && + UserFriendlyError.fromAnyError(err).name === + ErrorNames.UNSUPPORTED_CLIENT_VERSION + ) { + const { action } = UserFriendlyError.fromAnyError(err).args; + nav( + `/sign-in?error=${encodeURIComponent(err.message)}&action=${encodeURIComponent(action as string)}` + ); + return; + } setPasswordError(true); } finally { setIsLoading(false); @@ -97,6 +116,7 @@ export const SignInWithPasswordStep = ({ email, password, challenge, + nav, ]); const sendMagicLink = useCallback(() => { diff --git a/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx b/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx index ce8ba88e1cb98..1587b03c7b547 100644 --- a/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx @@ -1,3 +1,4 @@ +import { ErrorNames, UserFriendlyError } from '@affine/graphql'; import { useService } from '@toeverything/infra'; import { useEffect, useRef } from 'react'; import { @@ -8,7 +9,7 @@ import { useNavigate, } from 'react-router-dom'; -import { AuthService } from '../../../modules/cloud'; +import { AuthService, isBackendError } from '../../../modules/cloud'; import { supportedClient } from './common'; interface LoaderData { @@ -78,8 +79,20 @@ export const Component = () => { } }); }) - .catch(e => { - nav(`/sign-in?error=${encodeURIComponent(e.message)}`); + .catch(err => { + if ( + err instanceof Error && + isBackendError(err) && + UserFriendlyError.fromAnyError(err).name === + ErrorNames.UNSUPPORTED_CLIENT_VERSION + ) { + const { action } = UserFriendlyError.fromAnyError(err).args; + nav( + `/sign-in?error=${encodeURIComponent(err.message)}&action=${encodeURIComponent(action as string)}` + ); + return; + } + nav(`/sign-in?error=${encodeURIComponent(err.message)}`); }); }, [auth, data, data.email, data.redirectUri, data.token, nav]); diff --git a/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx b/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx index 453f4a6e326cf..b2df33351ac01 100644 --- a/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx @@ -1,3 +1,4 @@ +import { ErrorNames, UserFriendlyError } from '@affine/graphql'; import { useService } from '@toeverything/infra'; import { useEffect, useRef } from 'react'; import { @@ -8,7 +9,7 @@ import { useNavigate, } from 'react-router-dom'; -import { AuthService } from '../../../modules/cloud'; +import { AuthService, isBackendError } from '../../../modules/cloud'; import { supportedClient } from './common'; interface LoaderData { @@ -80,8 +81,21 @@ export const Component = () => { // TODO(@forehalo): need a good way to go back to previous tab and close current one nav(redirectUri ?? '/'); }) - .catch(e => { - nav(`/sign-in?error=${encodeURIComponent(e.message)}`); + .catch(err => { + if ( + err instanceof Error && + isBackendError(err) && + UserFriendlyError.fromAnyError(err).name === + ErrorNames.UNSUPPORTED_CLIENT_VERSION + ) { + const { action } = UserFriendlyError.fromAnyError(err).args; + nav( + `/sign-in?error=${encodeURIComponent(err.message)}&action=${encodeURIComponent(action as string)}` + ); + return; + } + nav(`/sign-in?error=${encodeURIComponent(err.message)}`); + return; }); }, [data, auth, nav]); diff --git a/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx b/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx index f58c25e2f6e92..64675076d86db 100644 --- a/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/sign-in.tsx @@ -1,4 +1,4 @@ -import { notify } from '@affine/component'; +import { notify, useConfirmModal } from '@affine/component'; import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout'; import { SignInPageContainer } from '@affine/component/auth-components'; import { SignInPanel } from '@affine/core/components/sign-in'; @@ -24,8 +24,10 @@ export const SignIn = ({ const navigate = useNavigate(); const { jumpToIndex } = useNavigateHelper(); const [searchParams] = useSearchParams(); + const { openConfirmModal } = useConfirmModal(); const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri'); const error = searchParams.get('error'); + const action = searchParams.get('action'); useEffect(() => { if (error) { @@ -36,6 +38,28 @@ export const SignIn = ({ } }, [error, t]); + useEffect(() => { + if (action === 'upgrade' || action === 'downgrade') { + openConfirmModal({ + title: t['com.affine.minimum-client.title'](), + description: + t[ + `com.affine.minimum-client.${action === 'upgrade' ? 'outdated' : 'advanced'}.message` + ](), + confirmText: + t[ + `com.affine.minimum-client.${action === 'upgrade' ? 'outdated' : 'advanced'}.button` + ](), + onConfirm: () => + window.open( + BUILD_CONFIG.downloadUrl, + '_blank', + 'noreferrer noopener' + ), + }); + } + }, [action, jumpToIndex, openConfirmModal, searchParams, t]); + const handleClose = () => { if (session.status$.value === 'authenticated' && redirectUrl) { navigate(redirectUrl, { diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 1c81c85c44084..87281f22cbd75 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,5 +1,5 @@ { - "ar": 68, + "ar": 67, "ca": 5, "da": 5, "de": 26, @@ -15,10 +15,10 @@ "ja": 90, "ko": 72, "pl": 0, - "pt-BR": 78, + "pt-BR": 77, "ru": 66, "sv-SE": 4, "ur": 2, - "zh-Hans": 91, + "zh-Hans": 90, "zh-Hant": 90 } \ No newline at end of file diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index f112ce394a980..e3b03ab0e252f 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1614,5 +1614,10 @@ "com.affine.payment.sync-paused.member.both.description": "This workspace has exceeded both storage and member limits, causing synchronization to pause. Please contact your workspace owner to address these limits and resume syncing.", "com.affine.payment.sync-paused.member.storage.description": "This workspace has exceeded its storage limit and synchronization has been paused. Please contact your workspace owner to either reduce storage usage or upgrade the plan to resume syncing.", "com.affine.payment.sync-paused.member.member.description": "This workspace has reached its maximum member capacity and synchronization has been paused. Please contact your workspace owner to either adjust team membership or upgrade the plan to resume syncing.", - "com.affine.payment.sync-paused.member.member.confirm": "Got It" + "com.affine.payment.sync-paused.member.member.confirm": "Got It", + "com.affine.minimum-client.title": "Client is outdated", + "com.affine.minimum-client.outdated.message": "This client is outdated, for the security of your data, please update the client, before accessing the data, or visit our web app.", + "com.affine.minimum-client.advanced.message": "This client app is not supported by the server yet, for the security of your data, please download the matching client, before accessing the data, or visit our web app.", + "com.affine.minimum-client.outdated.button": "Update client", + "com.affine.minimum-client.advanced.button": "Download client" } From b457486b852d49826b7501410b77ff57eac371bb Mon Sep 17 00:00:00 2001 From: Jimmfly Date: Fri, 20 Dec 2024 15:15:44 +0800 Subject: [PATCH 09/11] chore: adjust code --- .../components/sign-in/sign-in-with-password.tsx | 13 ++++--------- .../core/src/desktop/pages/auth/magic-link.tsx | 14 +++++--------- .../core/src/desktop/pages/auth/oauth-callback.tsx | 14 +++++--------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx b/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx index 33dc21c43b6b4..2d04e1c68a858 100644 --- a/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in-with-password.tsx @@ -9,7 +9,6 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo import { AuthService, CaptchaService, - isBackendError, ServerService, } from '@affine/core/modules/cloud'; import { Unreachable } from '@affine/env/constant'; @@ -91,15 +90,11 @@ export const SignInWithPasswordStep = ({ }); } catch (err) { console.error(err); - if ( - err instanceof Error && - isBackendError(err) && - UserFriendlyError.fromAnyError(err).name === - ErrorNames.UNSUPPORTED_CLIENT_VERSION - ) { - const { action } = UserFriendlyError.fromAnyError(err).args; + const userFriendlyError = UserFriendlyError.fromAnyError(err); + if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { + const { action } = userFriendlyError.args; nav( - `/sign-in?error=${encodeURIComponent(err.message)}&action=${encodeURIComponent(action as string)}` + `/sign-in?error=${encodeURIComponent(userFriendlyError.message)}&action=${encodeURIComponent(action as string)}` ); return; } diff --git a/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx b/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx index 1587b03c7b547..3e4c007826116 100644 --- a/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx @@ -9,7 +9,7 @@ import { useNavigate, } from 'react-router-dom'; -import { AuthService, isBackendError } from '../../../modules/cloud'; +import { AuthService } from '../../../modules/cloud'; import { supportedClient } from './common'; interface LoaderData { @@ -80,15 +80,11 @@ export const Component = () => { }); }) .catch(err => { - if ( - err instanceof Error && - isBackendError(err) && - UserFriendlyError.fromAnyError(err).name === - ErrorNames.UNSUPPORTED_CLIENT_VERSION - ) { - const { action } = UserFriendlyError.fromAnyError(err).args; + const userFriendlyError = UserFriendlyError.fromAnyError(err); + if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { + const { action } = userFriendlyError.args; nav( - `/sign-in?error=${encodeURIComponent(err.message)}&action=${encodeURIComponent(action as string)}` + `/sign-in?error=${encodeURIComponent(userFriendlyError.message)}&action=${encodeURIComponent(action as string)}` ); return; } diff --git a/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx b/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx index b2df33351ac01..31e734246fce9 100644 --- a/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx @@ -9,7 +9,7 @@ import { useNavigate, } from 'react-router-dom'; -import { AuthService, isBackendError } from '../../../modules/cloud'; +import { AuthService } from '../../../modules/cloud'; import { supportedClient } from './common'; interface LoaderData { @@ -82,15 +82,11 @@ export const Component = () => { nav(redirectUri ?? '/'); }) .catch(err => { - if ( - err instanceof Error && - isBackendError(err) && - UserFriendlyError.fromAnyError(err).name === - ErrorNames.UNSUPPORTED_CLIENT_VERSION - ) { - const { action } = UserFriendlyError.fromAnyError(err).args; + const userFriendlyError = UserFriendlyError.fromAnyError(err); + if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { + const { action } = userFriendlyError.args; nav( - `/sign-in?error=${encodeURIComponent(err.message)}&action=${encodeURIComponent(action as string)}` + `/sign-in?error=${encodeURIComponent(userFriendlyError.message)}&action=${encodeURIComponent(action as string)}` ); return; } From 16496bc0698624815e9ea38f1b556b7ace8c3e26 Mon Sep 17 00:00:00 2001 From: Jimmfly Date: Mon, 23 Dec 2024 16:31:27 +0800 Subject: [PATCH 10/11] feat(core): catch unsupported client version error --- .../core/src/components/sign-in/sign-in.tsx | 43 ++++++++++++++++--- .../src/desktop/pages/auth/magic-link.tsx | 13 +----- .../src/desktop/pages/auth/oauth-callback.tsx | 14 +----- .../core/src/modules/cloud/stores/auth.ts | 9 ++++ .../desktop-api/service/desktop-api.ts | 36 ++++++++++++++-- packages/frontend/graphql/src/error.ts | 4 +- 6 files changed, 84 insertions(+), 35 deletions(-) diff --git a/packages/frontend/core/src/components/sign-in/sign-in.tsx b/packages/frontend/core/src/components/sign-in/sign-in.tsx index 57d13924aa1cb..5dc5828eba901 100644 --- a/packages/frontend/core/src/components/sign-in/sign-in.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in.tsx @@ -4,7 +4,11 @@ import { OAuth } from '@affine/core/components/affine/auth/oauth'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { AuthService, ServerService } from '@affine/core/modules/cloud'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; -import { ServerDeploymentType } from '@affine/graphql'; +import { + ErrorNames, + ServerDeploymentType, + UserFriendlyError, +} from '@affine/graphql'; import { Trans, useI18n } from '@affine/i18n'; import { ArrowRightBigIcon, PublishIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; @@ -120,14 +124,41 @@ export const SignInStep = ({ } catch (err) { console.error(err); - // TODO(@eyhn): better error handling - notify.error({ - title: 'Failed to send email. Please try again.', - }); + const userFriendlyError = UserFriendlyError.fromAnyError(err); + if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { + const { action } = userFriendlyError.args; + notify.error({ + title: t['com.affine.minimum-client.title'](), + message: + t[ + `com.affine.minimum-client.${action === 'upgrade' ? 'outdated' : 'advanced'}.message` + ](), + action: { + label: + t[ + `com.affine.minimum-client.${action === 'upgrade' ? 'outdated' : 'advanced'}.button` + ](), + onClick: () => + window.open( + BUILD_CONFIG.downloadUrl, + '_blank', + 'noreferrer noopener' + ), + buttonProps: { + variant: 'primary', + }, + }, + }); + } else { + // TODO(@eyhn): better error handling + notify.error({ + title: 'Failed to send email. Please try again.', + }); + } } setIsMutating(false); - }, [authService, changeState, email]); + }, [authService, changeState, email, t]); const onAddSelfhosted = useCallback(() => { changeState(prev => ({ diff --git a/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx b/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx index 3e4c007826116..ce8ba88e1cb98 100644 --- a/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/magic-link.tsx @@ -1,4 +1,3 @@ -import { ErrorNames, UserFriendlyError } from '@affine/graphql'; import { useService } from '@toeverything/infra'; import { useEffect, useRef } from 'react'; import { @@ -79,16 +78,8 @@ export const Component = () => { } }); }) - .catch(err => { - const userFriendlyError = UserFriendlyError.fromAnyError(err); - if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { - const { action } = userFriendlyError.args; - nav( - `/sign-in?error=${encodeURIComponent(userFriendlyError.message)}&action=${encodeURIComponent(action as string)}` - ); - return; - } - nav(`/sign-in?error=${encodeURIComponent(err.message)}`); + .catch(e => { + nav(`/sign-in?error=${encodeURIComponent(e.message)}`); }); }, [auth, data, data.email, data.redirectUri, data.token, nav]); diff --git a/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx b/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx index 31e734246fce9..453f4a6e326cf 100644 --- a/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/oauth-callback.tsx @@ -1,4 +1,3 @@ -import { ErrorNames, UserFriendlyError } from '@affine/graphql'; import { useService } from '@toeverything/infra'; import { useEffect, useRef } from 'react'; import { @@ -81,17 +80,8 @@ export const Component = () => { // TODO(@forehalo): need a good way to go back to previous tab and close current one nav(redirectUri ?? '/'); }) - .catch(err => { - const userFriendlyError = UserFriendlyError.fromAnyError(err); - if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { - const { action } = userFriendlyError.args; - nav( - `/sign-in?error=${encodeURIComponent(userFriendlyError.message)}&action=${encodeURIComponent(action as string)}` - ); - return; - } - nav(`/sign-in?error=${encodeURIComponent(err.message)}`); - return; + .catch(e => { + nav(`/sign-in?error=${encodeURIComponent(e.message)}`); }); }, [data, auth, nav]); diff --git a/packages/frontend/core/src/modules/cloud/stores/auth.ts b/packages/frontend/core/src/modules/cloud/stores/auth.ts index b909e3885c0fe..89d0450dddf81 100644 --- a/packages/frontend/core/src/modules/cloud/stores/auth.ts +++ b/packages/frontend/core/src/modules/cloud/stores/auth.ts @@ -1,12 +1,15 @@ import { + ErrorNames, removeAvatarMutation, updateUserProfileMutation, uploadAvatarMutation, + UserFriendlyError, } from '@affine/graphql'; import { Store } from '@toeverything/infra'; import type { GlobalState } from '../../storage'; import type { AuthSessionInfo } from '../entities/session'; +import { BackendError } from '../error'; import type { FetchService } from '../services/fetch'; import type { GraphQLService } from '../services/graphql'; import type { ServerService } from '../services/server'; @@ -99,6 +102,12 @@ export class AuthStore extends Store { }); if (!res.ok) { + const error = await res.json(); + + const userFriendlyError = UserFriendlyError.fromAnyError(error); + if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { + throw new BackendError(userFriendlyError, res.status); + } throw new Error(`Failed to check user by email: ${email}`); } diff --git a/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts b/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts index 9f2beb7783193..1d00ca2273436 100644 --- a/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts +++ b/packages/frontend/core/src/modules/desktop-api/service/desktop-api.ts @@ -1,4 +1,5 @@ import { notify } from '@affine/component'; +import { ErrorNames, UserFriendlyError } from '@affine/graphql'; import { I18n } from '@affine/i18n'; import { init, @@ -173,10 +174,37 @@ export class DesktopApiService extends Service { } } })().catch(e => { - notify.error({ - title: I18n['com.affine.auth.toast.title.failed'](), - message: (e as any).message, - }); + const userFriendlyError = UserFriendlyError.fromAnyError(e); + if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { + const { action } = userFriendlyError.args; + notify.error({ + title: I18n['com.affine.minimum-client.title'](), + message: + I18n[ + `com.affine.minimum-client.${action === 'upgrade' ? 'outdated' : 'advanced'}.message` + ](), + action: { + label: + I18n[ + `com.affine.minimum-client.${action === 'upgrade' ? 'outdated' : 'advanced'}.button` + ](), + onClick: () => + window.open( + BUILD_CONFIG.downloadUrl, + '_blank', + 'noreferrer noopener' + ), + buttonProps: { + variant: 'primary', + }, + }, + }); + } else { + notify.error({ + title: I18n['com.affine.auth.toast.title.failed'](), + message: (e as any).message, + }); + } }); }); } diff --git a/packages/frontend/graphql/src/error.ts b/packages/frontend/graphql/src/error.ts index c10623454abd5..6ecde21b71318 100644 --- a/packages/frontend/graphql/src/error.ts +++ b/packages/frontend/graphql/src/error.ts @@ -8,7 +8,7 @@ export interface UserFriendlyErrorResponse { type: string; name: ErrorNames; message: string; - args?: any; + data?: any; stacktrace?: string; } @@ -21,7 +21,7 @@ export class UserFriendlyError readonly type = this.response.type; override readonly name = this.response.name; override readonly message = this.response.message; - readonly args = this.response.args; + readonly args = this.response.data; readonly stacktrace = this.response.stacktrace; static fromAnyError(response: any) { From fc666ec7237c684d38b970e49f6864d4f7932eb0 Mon Sep 17 00:00:00 2001 From: Jimmfly Date: Mon, 23 Dec 2024 17:01:57 +0800 Subject: [PATCH 11/11] chore: remove redundant conditionals --- .../frontend/core/src/modules/cloud/stores/auth.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/frontend/core/src/modules/cloud/stores/auth.ts b/packages/frontend/core/src/modules/cloud/stores/auth.ts index 89d0450dddf81..fd65b81c9abef 100644 --- a/packages/frontend/core/src/modules/cloud/stores/auth.ts +++ b/packages/frontend/core/src/modules/cloud/stores/auth.ts @@ -1,15 +1,12 @@ import { - ErrorNames, removeAvatarMutation, updateUserProfileMutation, uploadAvatarMutation, - UserFriendlyError, } from '@affine/graphql'; import { Store } from '@toeverything/infra'; import type { GlobalState } from '../../storage'; import type { AuthSessionInfo } from '../entities/session'; -import { BackendError } from '../error'; import type { FetchService } from '../services/fetch'; import type { GraphQLService } from '../services/graphql'; import type { ServerService } from '../services/server'; @@ -101,16 +98,6 @@ export class AuthStore extends Store { }, }); - if (!res.ok) { - const error = await res.json(); - - const userFriendlyError = UserFriendlyError.fromAnyError(error); - if (userFriendlyError.name === ErrorNames.UNSUPPORTED_CLIENT_VERSION) { - throw new BackendError(userFriendlyError, res.status); - } - throw new Error(`Failed to check user by email: ${email}`); - } - const data = (await res.json()) as { registered: boolean; hasPassword: boolean;