mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-13 16:16:46 +08:00
refactor(server): config system (#11081)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { defineStartupConfig, ModuleConfig } from '../../base/config';
|
||||
import { defineModuleConfig, JSONSchema } from '../../base';
|
||||
|
||||
export interface OAuthProviderConfig {
|
||||
clientId: string;
|
||||
@@ -23,23 +23,53 @@ export enum OAuthProviderName {
|
||||
GitHub = 'github',
|
||||
OIDC = 'oidc',
|
||||
}
|
||||
|
||||
type OAuthProviderConfigMapping = {
|
||||
[OAuthProviderName.Google]: OAuthProviderConfig;
|
||||
[OAuthProviderName.GitHub]: OAuthProviderConfig;
|
||||
[OAuthProviderName.OIDC]: OAuthOIDCProviderConfig;
|
||||
};
|
||||
|
||||
export interface OAuthConfig {
|
||||
providers: Partial<OAuthProviderConfigMapping>;
|
||||
}
|
||||
|
||||
declare module '../config' {
|
||||
interface PluginsConfig {
|
||||
oauth: ModuleConfig<OAuthConfig>;
|
||||
declare global {
|
||||
interface AppConfigSchema {
|
||||
oauth: {
|
||||
providers: {
|
||||
[OAuthProviderName.Google]: ConfigItem<OAuthProviderConfig>;
|
||||
[OAuthProviderName.GitHub]: ConfigItem<OAuthProviderConfig>;
|
||||
[OAuthProviderName.OIDC]: ConfigItem<OAuthOIDCProviderConfig>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
defineStartupConfig('plugins.oauth', {
|
||||
providers: {},
|
||||
const schema: JSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clientId: { type: 'string' },
|
||||
clientSecret: { type: 'string' },
|
||||
args: { type: 'object' },
|
||||
},
|
||||
};
|
||||
|
||||
defineModuleConfig('oauth', {
|
||||
'providers.google': {
|
||||
desc: 'Google OAuth provider config',
|
||||
default: {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
},
|
||||
schema,
|
||||
link: 'https://developers.google.com/identity/protocols/oauth2/web-server',
|
||||
},
|
||||
'providers.github': {
|
||||
desc: 'GitHub OAuth provider config',
|
||||
default: {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
},
|
||||
schema,
|
||||
link: 'https://docs.github.com/en/apps/oauth-apps',
|
||||
},
|
||||
'providers.oidc': {
|
||||
desc: 'OIDC OAuth provider config',
|
||||
default: {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
issuer: '',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
import { AuthService, Public } from '../../core/auth';
|
||||
import { Models } from '../../models';
|
||||
import { OAuthProviderName } from './config';
|
||||
import { OAuthProviderFactory } from './factory';
|
||||
import { OAuthAccount, Tokens } from './providers/def';
|
||||
import { OAuthProviderFactory } from './register';
|
||||
import { OAuthService } from './service';
|
||||
|
||||
@Controller('/api/oauth')
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ServerFeature, ServerService } from '../../core';
|
||||
import { OAuthProviderName } from './config';
|
||||
import type { OAuthProvider } from './providers/def';
|
||||
|
||||
@Injectable()
|
||||
export class OAuthProviderFactory {
|
||||
constructor(private readonly server: ServerService) {}
|
||||
|
||||
private readonly logger = new Logger(OAuthProviderFactory.name);
|
||||
readonly #providers = new Map<OAuthProviderName, OAuthProvider>();
|
||||
|
||||
get providers() {
|
||||
return Array.from(this.#providers.keys());
|
||||
}
|
||||
|
||||
get(name: OAuthProviderName): OAuthProvider | undefined {
|
||||
return this.#providers.get(name);
|
||||
}
|
||||
|
||||
register(provider: OAuthProvider) {
|
||||
this.#providers.set(provider.provider, provider);
|
||||
this.logger.log(`OAuth provider [${provider.provider}] registered.`);
|
||||
this.server.enableFeature(ServerFeature.OAuth);
|
||||
}
|
||||
|
||||
unregister(provider: OAuthProvider) {
|
||||
this.#providers.delete(provider.provider);
|
||||
this.logger.log(`OAuth provider [${provider.provider}] unregistered.`);
|
||||
if (this.#providers.size === 0) {
|
||||
this.server.disableFeature(ServerFeature.OAuth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import './config';
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ServerConfigModule } from '../../core';
|
||||
import { AuthModule } from '../../core/auth';
|
||||
import { ServerFeature } from '../../core/config';
|
||||
import { UserModule } from '../../core/user';
|
||||
import { Plugin } from '../registry';
|
||||
import { OAuthController } from './controller';
|
||||
import { OAuthProviderFactory } from './factory';
|
||||
import { OAuthProviders } from './providers';
|
||||
import { OAuthProviderFactory } from './register';
|
||||
import { OAuthResolver } from './resolver';
|
||||
import { OAuthService } from './service';
|
||||
|
||||
@Plugin({
|
||||
name: 'oauth',
|
||||
imports: [AuthModule, UserModule],
|
||||
@Module({
|
||||
imports: [AuthModule, UserModule, ServerConfigModule],
|
||||
providers: [
|
||||
OAuthProviderFactory,
|
||||
OAuthService,
|
||||
@@ -20,7 +20,5 @@ import { OAuthService } from './service';
|
||||
...OAuthProviders,
|
||||
],
|
||||
controllers: [OAuthController],
|
||||
contributesTo: ServerFeature.OAuth,
|
||||
if: config => config.flavor.graphql && !!config.plugins.oauth,
|
||||
})
|
||||
export class OAuthModule {}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { Config, OnEvent } from '../../../base';
|
||||
import { OAuthProviderName } from '../config';
|
||||
import { OAuthProviderFactory } from '../factory';
|
||||
|
||||
export interface OAuthAccount {
|
||||
id: string;
|
||||
@@ -13,9 +17,42 @@ export interface Tokens {
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export abstract class OAuthProvider {
|
||||
abstract provider: OAuthProviderName;
|
||||
abstract getAuthUrl(state: string): string;
|
||||
abstract getToken(code: string): Promise<Tokens>;
|
||||
abstract getUser(token: string): Promise<OAuthAccount>;
|
||||
|
||||
protected readonly logger = new Logger(this.constructor.name);
|
||||
@Inject() private readonly factory!: OAuthProviderFactory;
|
||||
@Inject() private readonly AFFiNEConfig!: Config;
|
||||
|
||||
get config() {
|
||||
return this.AFFiNEConfig.oauth.providers[this.provider];
|
||||
}
|
||||
|
||||
get configured() {
|
||||
return this.config && this.config.clientId && this.config.clientSecret;
|
||||
}
|
||||
|
||||
@OnEvent('config.init')
|
||||
onConfigInit() {
|
||||
this.setup();
|
||||
}
|
||||
|
||||
@OnEvent('config.changed')
|
||||
onConfigUpdated(event: Events['config.changed']) {
|
||||
if ('oauth' in event.updates) {
|
||||
this.setup();
|
||||
}
|
||||
}
|
||||
|
||||
protected setup() {
|
||||
if (this.configured) {
|
||||
this.factory.register(this);
|
||||
} else {
|
||||
this.factory.unregister(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config, InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
||||
import { InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
||||
import { OAuthProviderName } from '../config';
|
||||
import { AutoRegisteredOAuthProvider } from '../register';
|
||||
import { OAuthProvider } from './def';
|
||||
|
||||
interface AuthTokenResponse {
|
||||
access_token: string;
|
||||
@@ -18,13 +18,10 @@ export interface UserInfo {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GithubOAuthProvider extends AutoRegisteredOAuthProvider {
|
||||
export class GithubOAuthProvider extends OAuthProvider {
|
||||
provider = OAuthProviderName.GitHub;
|
||||
|
||||
constructor(
|
||||
protected readonly AFFiNEConfig: Config,
|
||||
private readonly url: URLHelper
|
||||
) {
|
||||
constructor(private readonly url: URLHelper) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config, InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
||||
import { InvalidOauthCallbackCode, URLHelper } from '../../../base';
|
||||
import { OAuthProviderName } from '../config';
|
||||
import { AutoRegisteredOAuthProvider } from '../register';
|
||||
import { OAuthProvider } from './def';
|
||||
|
||||
interface GoogleOAuthTokenResponse {
|
||||
access_token: string;
|
||||
@@ -20,13 +20,10 @@ export interface UserInfo {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GoogleOAuthProvider extends AutoRegisteredOAuthProvider {
|
||||
export class GoogleOAuthProvider extends OAuthProvider {
|
||||
override provider = OAuthProviderName.Google;
|
||||
|
||||
constructor(
|
||||
protected readonly AFFiNEConfig: Config,
|
||||
private readonly url: URLHelper
|
||||
) {
|
||||
constructor(private readonly url: URLHelper) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Config, URLHelper } from '../../../base';
|
||||
import { URLHelper } from '../../../base';
|
||||
import {
|
||||
OAuthOIDCProviderConfig,
|
||||
OAuthProviderName,
|
||||
OIDCArgs,
|
||||
} from '../config';
|
||||
import { AutoRegisteredOAuthProvider } from '../register';
|
||||
import { OAuthAccount, Tokens } from './def';
|
||||
import { OAuthAccount, OAuthProvider, Tokens } from './def';
|
||||
|
||||
const OIDCTokenSchema = z.object({
|
||||
access_token: z.string(),
|
||||
@@ -163,26 +162,26 @@ class OIDCClient {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OIDCProvider
|
||||
extends AutoRegisteredOAuthProvider
|
||||
implements OnModuleInit
|
||||
{
|
||||
export class OIDCProvider extends OAuthProvider {
|
||||
override provider = OAuthProviderName.OIDC;
|
||||
private client: OIDCClient | null = null;
|
||||
|
||||
constructor(
|
||||
protected readonly AFFiNEConfig: Config,
|
||||
private readonly url: URLHelper
|
||||
) {
|
||||
constructor(private readonly url: URLHelper) {
|
||||
super();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
override async onModuleInit() {
|
||||
const config = this.optionalConfig as OAuthOIDCProviderConfig;
|
||||
if (config && config.issuer && config.clientId && config.clientSecret) {
|
||||
this.client = await OIDCClient.create(config, this.url);
|
||||
super.onModuleInit();
|
||||
protected override setup() {
|
||||
super.setup();
|
||||
if (this.configured) {
|
||||
OIDCClient.create(this.config as OAuthOIDCProviderConfig, this.url)
|
||||
.then(client => {
|
||||
this.client = client;
|
||||
})
|
||||
.catch(e => {
|
||||
this.logger.error('Failed to create OIDC client', e);
|
||||
});
|
||||
} else {
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../base';
|
||||
import { OAuthProviderName } from './config';
|
||||
import { OAuthProvider } from './providers/def';
|
||||
|
||||
const PROVIDERS: Map<OAuthProviderName, OAuthProvider> = new Map();
|
||||
|
||||
export function registerOAuthProvider(
|
||||
name: OAuthProviderName,
|
||||
provider: OAuthProvider
|
||||
) {
|
||||
PROVIDERS.set(name, provider);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OAuthProviderFactory {
|
||||
get providers() {
|
||||
return Array.from(PROVIDERS.keys());
|
||||
}
|
||||
|
||||
get(name: OAuthProviderName): OAuthProvider | undefined {
|
||||
return PROVIDERS.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AutoRegisteredOAuthProvider
|
||||
extends OAuthProvider
|
||||
implements OnModuleInit
|
||||
{
|
||||
protected abstract AFFiNEConfig: Config;
|
||||
|
||||
get optionalConfig() {
|
||||
return this.AFFiNEConfig.plugins.oauth?.providers?.[this.provider];
|
||||
}
|
||||
|
||||
get config() {
|
||||
const config = this.optionalConfig;
|
||||
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
`OAuthProvider Config should not be used before registered`
|
||||
);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
const config = this.optionalConfig;
|
||||
if (config && config.clientId && config.clientSecret) {
|
||||
registerOAuthProvider(this.provider, this);
|
||||
new Logger(`OAuthProvider:${this.provider}`).log(
|
||||
'OAuth provider registered.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { registerEnumType, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { ServerConfigType } from '../../core/config/types';
|
||||
import { OAuthProviderName } from './config';
|
||||
import { OAuthProviderFactory } from './register';
|
||||
import { OAuthProviderFactory } from './factory';
|
||||
|
||||
registerEnumType(OAuthProviderName, { name: 'OAuthProviderType' });
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SessionCache } from '../../base';
|
||||
import { OAuthProviderName } from './config';
|
||||
import { OAuthProviderFactory } from './register';
|
||||
import { OAuthProviderFactory } from './factory';
|
||||
|
||||
const OAUTH_STATE_KEY = 'OAUTH_STATE';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user