mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08: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 assert from 'node:assert';
|
||||||
|
|
||||||
import { gqlFetcherFactory } from '@affine/graphql';
|
import { gqlFetcherFactory } from '@affine/graphql';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication, ModuleMetadata } from '@nestjs/common';
|
||||||
import { NestApplication } from '@nestjs/core';
|
import { NestApplication } from '@nestjs/core';
|
||||||
import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
import { Test, TestingModuleBuilder } from '@nestjs/testing';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
@@ -35,6 +35,7 @@ import { parseCookies, TEST_LOG_LEVEL } from '../utils';
|
|||||||
interface TestingAppMetadata {
|
interface TestingAppMetadata {
|
||||||
tapModule?(m: TestingModuleBuilder): void;
|
tapModule?(m: TestingModuleBuilder): void;
|
||||||
tapApp?(app: INestApplication): void;
|
tapApp?(app: INestApplication): void;
|
||||||
|
imports?: ModuleMetadata['imports'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TestingApp extends NestApplication {
|
export class TestingApp extends NestApplication {
|
||||||
@@ -203,7 +204,7 @@ export async function createApp(
|
|||||||
const { tapModule, tapApp } = metadata;
|
const { tapModule, tapApp } = metadata;
|
||||||
|
|
||||||
const builder = Test.createTestingModule({
|
const builder = Test.createTestingModule({
|
||||||
imports: [buildAppModule(globalThis.env)],
|
imports: [buildAppModule(globalThis.env), ...(metadata.imports ?? [])],
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.overrideProvider(Mailer).useValue(new MockMailer());
|
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);
|
const req = getRequestFromHost(host);
|
||||||
return getRequestIdFromRequest(req, type);
|
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 { ServerConfigType } from '../../core/config/types';
|
||||||
import { OAuthProviderName } from './config';
|
import { OAuthProviderName } from './config';
|
||||||
import { OAuthProviderFactory } from './factory';
|
import { OAuthProviderFactory } from './factory';
|
||||||
|
|
||||||
registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' });
|
registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' });
|
||||||
|
|
||||||
|
const APPLE_OAUTH_PROVIDER_MIN_VERSION = new semver.Range('>=0.22.0', {
|
||||||
|
includePrerelease: true,
|
||||||
|
});
|
||||||
|
|
||||||
@Resolver(() => ServerConfigType)
|
@Resolver(() => ServerConfigType)
|
||||||
export class OAuthResolver {
|
export class OAuthResolver {
|
||||||
constructor(private readonly factory: OAuthProviderFactory) {}
|
constructor(private readonly factory: OAuthProviderFactory) {}
|
||||||
|
|
||||||
@ResolveField(() => [OAuthProviderName])
|
@ResolveField(() => [OAuthProviderName])
|
||||||
oauthProviders() {
|
oauthProviders(@Context() ctx: { req: Request }) {
|
||||||
return this.factory.providers;
|
// 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