mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(server): client version check (#9205)
Co-authored-by: forehalo <forehalo@gmail.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user