refactor(server): config system (#11081)

This commit is contained in:
forehalo
2025-03-27 12:32:28 +00:00
parent 7091111f85
commit 0ea38680fa
274 changed files with 7583 additions and 5841 deletions
@@ -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';