mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
fix(server): disable Apple oauth on client version < 0.22.0 (#12984)
close AF-2705 #### PR Dependency Tree * **PR #12984** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * The Apple OAuth provider is now available only for clients version 0.22.0 or higher. * Client version detection has been improved by extracting version information from request headers. * **Bug Fixes** * Ensured that the Apple OAuth provider is hidden for clients below version 0.22.0. * **Tests** * Added comprehensive end-to-end and utility tests for OAuth provider selection and client version extraction. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { gqlFetcherFactory } from '@affine/graphql';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { INestApplication, ModuleMetadata } from '@nestjs/common';
|
||||
import { NestApplication } from '@nestjs/core';
|
||||
import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
@@ -35,6 +35,7 @@ import { parseCookies, TEST_LOG_LEVEL } from '../utils';
|
||||
interface TestingAppMetadata {
|
||||
tapModule?(m: TestingModuleBuilder): void;
|
||||
tapApp?(app: INestApplication): void;
|
||||
imports?: ModuleMetadata['imports'];
|
||||
}
|
||||
|
||||
export class TestingApp extends NestApplication {
|
||||
@@ -203,7 +204,7 @@ export async function createApp(
|
||||
const { tapModule, tapApp } = metadata;
|
||||
|
||||
const builder = Test.createTestingModule({
|
||||
imports: [buildAppModule(globalThis.env)],
|
||||
imports: [buildAppModule(globalThis.env), ...(metadata.imports ?? [])],
|
||||
});
|
||||
|
||||
builder.overrideProvider(Mailer).useValue(new MockMailer());
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Snapshot report for `src/__tests__/e2e/oauth/resolver.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `resolver.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should return apple oauth provider in version >= 0.22.0
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
serverConfig: {
|
||||
oauthProviders: [
|
||||
'Google',
|
||||
'Apple',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
> Snapshot 2
|
||||
|
||||
{
|
||||
serverConfig: {
|
||||
oauthProviders: [
|
||||
'Google',
|
||||
'Apple',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
> Snapshot 3
|
||||
|
||||
{
|
||||
serverConfig: {
|
||||
oauthProviders: [
|
||||
'Google',
|
||||
'Apple',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
## should not return apple oauth provider when client version is not specified
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
serverConfig: {
|
||||
oauthProviders: [
|
||||
'Google',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
## should not return apple oauth provider in version < 0.22.0
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
serverConfig: {
|
||||
oauthProviders: [
|
||||
'Google',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
## should not return apple oauth provider when client version format is not correct
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
serverConfig: {
|
||||
oauthProviders: [
|
||||
'Google',
|
||||
],
|
||||
},
|
||||
}
|
||||
Binary file not shown.
111
packages/backend/server/src/__tests__/e2e/oauth/resolver.spec.ts
Normal file
111
packages/backend/server/src/__tests__/e2e/oauth/resolver.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { oauthProvidersQuery } from '@affine/graphql';
|
||||
|
||||
import { ConfigModule } from '../../../base/config';
|
||||
import { createApp, e2e, TestingApp } from '../test';
|
||||
|
||||
let app: TestingApp;
|
||||
|
||||
e2e.before(async () => {
|
||||
app = await createApp({
|
||||
imports: [
|
||||
ConfigModule.override({
|
||||
oauth: {
|
||||
providers: {
|
||||
apple: {
|
||||
clientId: 'test',
|
||||
clientSecret: 'test',
|
||||
args: {
|
||||
redirectUri: 'test',
|
||||
},
|
||||
},
|
||||
google: {
|
||||
clientId: 'test',
|
||||
clientSecret: 'test',
|
||||
args: {
|
||||
redirectUri: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
e2e.after.always(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
e2e('should return apple oauth provider in version >= 0.22.0', async t => {
|
||||
const res = await app.gql({
|
||||
query: oauthProvidersQuery,
|
||||
context: {
|
||||
headers: {
|
||||
'x-affine-version': '0.22.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
t.snapshot(res);
|
||||
|
||||
const res2 = await app.gql({
|
||||
query: oauthProvidersQuery,
|
||||
context: {
|
||||
headers: {
|
||||
'x-affine-version': '0.23.0-beta.1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.snapshot(res2);
|
||||
|
||||
const res3 = await app.gql({
|
||||
query: oauthProvidersQuery,
|
||||
context: {
|
||||
headers: {
|
||||
'x-affine-version': '2025.6.29-canary.93',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.snapshot(res3);
|
||||
});
|
||||
|
||||
e2e(
|
||||
'should not return apple oauth provider when client version is not specified',
|
||||
async t => {
|
||||
const res = await app.gql({
|
||||
query: oauthProvidersQuery,
|
||||
});
|
||||
|
||||
t.snapshot(res);
|
||||
}
|
||||
);
|
||||
|
||||
e2e('should not return apple oauth provider in version < 0.22.0', async t => {
|
||||
const res = await app.gql({
|
||||
query: oauthProvidersQuery,
|
||||
context: {
|
||||
headers: {
|
||||
'x-affine-version': '0.21.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.snapshot(res);
|
||||
});
|
||||
|
||||
e2e(
|
||||
'should not return apple oauth provider when client version format is not correct',
|
||||
async t => {
|
||||
const res = await app.gql({
|
||||
query: oauthProvidersQuery,
|
||||
context: {
|
||||
headers: {
|
||||
'x-affine-version': 'mock-invalid-version',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.snapshot(res);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'ava';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { getClientVersionFromRequest } from '../request';
|
||||
|
||||
test('should get client version from x-affine-version header', t => {
|
||||
const req = {
|
||||
headers: {
|
||||
'x-affine-version': '0.22.2',
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
t.is(getClientVersionFromRequest(req), '0.22.2');
|
||||
|
||||
const req2 = {
|
||||
headers: {
|
||||
'x-affine-version': ['0.22.2', '0.23.0-beta.2'],
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
t.is(getClientVersionFromRequest(req2), '0.22.2');
|
||||
});
|
||||
|
||||
test('should not get client version from x-affine-version header', t => {
|
||||
const req = {
|
||||
headers: {
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.205 Safari/537.36',
|
||||
},
|
||||
} as unknown as Request;
|
||||
|
||||
t.is(getClientVersionFromRequest(req), undefined);
|
||||
});
|
||||
@@ -118,3 +118,11 @@ export function getRequestIdFromHost(host: ArgumentsHost) {
|
||||
const req = getRequestFromHost(host);
|
||||
return getRequestIdFromRequest(req, type);
|
||||
}
|
||||
|
||||
export function getClientVersionFromRequest(req: Request) {
|
||||
let version = req.headers['x-affine-version'];
|
||||
if (Array.isArray(version)) {
|
||||
version = version[0];
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
Context,
|
||||
registerEnumType,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { Request } from 'express';
|
||||
import semver from 'semver';
|
||||
|
||||
import { getClientVersionFromRequest } from '../../base';
|
||||
import { ServerConfigType } from '../../core/config/types';
|
||||
import { OAuthProviderName } from './config';
|
||||
import { OAuthProviderFactory } from './factory';
|
||||
|
||||
registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' });
|
||||
|
||||
const APPLE_OAUTH_PROVIDER_MIN_VERSION = new semver.Range('>=0.22.0', {
|
||||
includePrerelease: true,
|
||||
});
|
||||
|
||||
@Resolver(() => ServerConfigType)
|
||||
export class OAuthResolver {
|
||||
constructor(private readonly factory: OAuthProviderFactory) {}
|
||||
|
||||
@ResolveField(() => [OAuthProviderName])
|
||||
oauthProviders() {
|
||||
return this.factory.providers;
|
||||
oauthProviders(@Context() ctx: { req: Request }) {
|
||||
// Apple oauth provider is not supported in client version < 0.22.0
|
||||
const providers = this.factory.providers;
|
||||
if (providers.includes(OAuthProviderName.Apple)) {
|
||||
const version = getClientVersionFromRequest(ctx.req);
|
||||
if (!version || !APPLE_OAUTH_PROVIDER_MIN_VERSION.test(version)) {
|
||||
return providers.filter(p => p !== OAuthProviderName.Apple);
|
||||
}
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user