mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(server): client version check (#9205)
Co-authored-by: forehalo <forehalo@gmail.com>
This commit is contained in:
148
packages/backend/server/src/__tests__/version.spec.ts
Normal file
148
packages/backend/server/src/__tests__/version.spec.ts
Normal file
@@ -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<Runtime>;
|
||||
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].'
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, UserFriendlyErrorOptions>;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -18,14 +18,19 @@ export class BasicGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
// get registered guard name
|
||||
const providerName = this.reflector.get<string>(
|
||||
const providerName = this.reflector.get<string[]>(
|
||||
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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
31
packages/backend/server/src/core/version/config.ts
Normal file
31
packages/backend/server/src/core/version/config.ts
Normal file
@@ -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<never, VersionConfig>;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
40
packages/backend/server/src/core/version/guard.ts
Normal file
40
packages/backend/server/src/core/version/guard.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
13
packages/backend/server/src/core/version/index.ts
Normal file
13
packages/backend/server/src/core/version/index.ts
Normal file
@@ -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';
|
||||
59
packages/backend/server/src/core/version/service.ts
Normal file
59
packages/backend/server/src/core/version/service.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user