refactor(oauth): remove redirect URI settings from system configuration

- Removed oauthGoogleRedirectUri and oauthGithubRedirectUri from SYSTEM_SETTING_DEFINITIONS and related service logic.
- Updated SystemSettingService to eliminate handling of redirect URIs for Google and GitHub.
- Adjusted UI schema and types to reflect the removal of redirect URI fields.
- Cleaned up associated validation and parsing logic to streamline OAuth configuration.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-12 17:56:22 +08:00
parent 66c57218e4
commit 715dbf7077
9 changed files with 43 additions and 135 deletions

View File

@@ -58,12 +58,6 @@ export const SYSTEM_SETTING_DEFINITIONS = {
defaultValue: null as string | null,
isSensitive: true,
},
oauthGoogleRedirectUri: {
key: 'system.auth.oauth.google.redirectUri',
schema: nullableUrl,
defaultValue: null as string | null,
isSensitive: false,
},
oauthGithubClientId: {
key: 'system.auth.oauth.github.clientId',
schema: nullableNonEmptyString,
@@ -76,12 +70,6 @@ export const SYSTEM_SETTING_DEFINITIONS = {
defaultValue: null as string | null,
isSensitive: true,
},
oauthGithubRedirectUri: {
key: 'system.auth.oauth.github.redirectUri',
schema: nullableUrl,
defaultValue: null as string | null,
isSensitive: false,
},
} as const
export type SystemSettingField = keyof typeof SYSTEM_SETTING_DEFINITIONS

View File

@@ -71,14 +71,6 @@ export class SystemSettingService {
SYSTEM_SETTING_DEFINITIONS.oauthGoogleClientSecret.schema,
SYSTEM_SETTING_DEFINITIONS.oauthGoogleClientSecret.defaultValue,
)
const oauthGoogleRedirectUri = this.normalizeRedirectPath(
this.parseSetting(
rawValues[SYSTEM_SETTING_DEFINITIONS.oauthGoogleRedirectUri.key],
SYSTEM_SETTING_DEFINITIONS.oauthGoogleRedirectUri.schema,
SYSTEM_SETTING_DEFINITIONS.oauthGoogleRedirectUri.defaultValue,
),
)
const oauthGithubClientId = this.parseSetting(
rawValues[SYSTEM_SETTING_DEFINITIONS.oauthGithubClientId.key],
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientId.schema,
@@ -89,14 +81,6 @@ export class SystemSettingService {
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientSecret.schema,
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientSecret.defaultValue,
)
const oauthGithubRedirectUri = this.normalizeRedirectPath(
this.parseSetting(
rawValues[SYSTEM_SETTING_DEFINITIONS.oauthGithubRedirectUri.key],
SYSTEM_SETTING_DEFINITIONS.oauthGithubRedirectUri.schema,
SYSTEM_SETTING_DEFINITIONS.oauthGithubRedirectUri.defaultValue,
),
)
return {
allowRegistration,
maxRegistrableUsers,
@@ -105,10 +89,8 @@ export class SystemSettingService {
oauthGatewayUrl,
oauthGoogleClientId,
oauthGoogleClientSecret,
oauthGoogleRedirectUri,
oauthGithubClientId,
oauthGithubClientSecret,
oauthGithubRedirectUri,
}
}
@@ -195,13 +177,6 @@ export class SystemSettingService {
}
}
if (patch.oauthGoogleRedirectUri !== undefined) {
const sanitized = this.normalizeRedirectPath(patch.oauthGoogleRedirectUri)
if (sanitized !== current.oauthGoogleRedirectUri) {
enqueueUpdate('oauthGoogleRedirectUri', sanitized)
}
}
if (patch.oauthGithubClientId !== undefined) {
const sanitized = this.normalizeNullableString(patch.oauthGithubClientId)
if (sanitized !== current.oauthGithubClientId) {
@@ -216,13 +191,6 @@ export class SystemSettingService {
}
}
if (patch.oauthGithubRedirectUri !== undefined) {
const sanitized = this.normalizeRedirectPath(patch.oauthGithubRedirectUri)
if (sanitized !== current.oauthGithubRedirectUri) {
enqueueUpdate('oauthGithubRedirectUri', sanitized)
}
}
if (updates.length === 0) {
return current
}
@@ -291,7 +259,6 @@ export class SystemSettingService {
providers.google = {
clientId: settings.oauthGoogleClientId,
clientSecret: settings.oauthGoogleClientSecret,
redirectPath: settings.oauthGoogleRedirectUri,
}
}
@@ -299,7 +266,6 @@ export class SystemSettingService {
providers.github = {
clientId: settings.oauthGithubClientId,
clientSecret: settings.oauthGithubClientSecret,
redirectPath: settings.oauthGithubRedirectUri,
}
}
@@ -337,36 +303,6 @@ export class SystemSettingService {
}
}
private normalizeRedirectPath(value: string | null | undefined): string | null {
if (value === undefined || value === null) {
return null
}
const trimmed = value.trim()
if (trimmed.length === 0) {
return null
}
const ensureLeadingSlash = (input: string): string | null => {
if (!input.startsWith('/')) {
return null
}
return input
}
try {
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
const url = new URL(trimmed)
const pathWithQuery = `${url.pathname}${url.search ?? ''}`
return ensureLeadingSlash(pathWithQuery) ?? null
}
} catch {
// fall through to path handling
}
return ensureLeadingSlash(trimmed)
}
private buildStats(settings: SystemSettings, totalUsers: number): SystemSettingStats {
const remaining =
settings.maxRegistrableUsers === null ? null : Math.max(settings.maxRegistrableUsers - totalUsers, 0)

View File

@@ -10,10 +10,8 @@ export interface SystemSettings {
oauthGatewayUrl: string | null
oauthGoogleClientId: string | null
oauthGoogleClientSecret: string | null
oauthGoogleRedirectUri: string | null
oauthGithubClientId: string | null
oauthGithubClientSecret: string | null
oauthGithubRedirectUri: string | null
}
export type SystemSettingValueMap = {

View File

@@ -2,7 +2,7 @@ import type { UiNode, UiSchema } from 'core/modules/ui/ui-schema/ui-schema.type'
import type { SystemSettingField } from './system-setting.constants'
export const SYSTEM_SETTING_UI_SCHEMA_VERSION = '1.2.0'
export const SYSTEM_SETTING_UI_SCHEMA_VERSION = '1.3.0'
export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
version: SYSTEM_SETTING_UI_SCHEMA_VERSION,
@@ -74,7 +74,7 @@ export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
type: 'field',
id: 'oauth-gateway-url',
title: 'OAuth 网关地址',
description: '统一的 OAuth 回调入口例如 https://auth.afilmory.art。留空则直接回调到租户域名。',
description: '所有第三方登录统一走该回调入口例如 https://auth.afilmory.art。留空则回退到租户域名。',
helperText: '必须包含 http/https 协议,结尾无需斜杠。',
key: 'oauthGatewayUrl',
component: {
@@ -113,17 +113,6 @@ export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
autoComplete: 'off',
},
},
{
type: 'field',
id: 'oauth-google-redirect-uri',
title: 'Redirect URI',
description: 'OAuth 回调路径,域名会自动使用当前租户如 slug.主域名。',
key: 'oauthGoogleRedirectUri',
component: {
type: 'text',
placeholder: '/api/auth/callback/google',
},
},
],
},
{
@@ -157,17 +146,6 @@ export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
autoComplete: 'off',
},
},
{
type: 'field',
id: 'oauth-github-redirect-uri',
title: 'Redirect URI',
description: 'GitHub 回调路径,域名会自动使用租户的 subdomain。',
key: 'oauthGithubRedirectUri',
component: {
type: 'text',
placeholder: '/api/auth/callback/github',
},
},
],
},
],

View File

@@ -4,7 +4,6 @@ import { injectable } from 'tsyringe'
export interface SocialProviderOptions {
clientId: string
clientSecret: string
redirectPath?: string | null
}
export interface SocialProvidersConfig {

View File

@@ -43,13 +43,13 @@ function resolveSocialProviderMetadata(id: string): { name: string; icon: string
function buildProviderResponse(socialProviders: SocialProvidersConfig) {
return Object.entries(socialProviders)
.filter(([, config]) => Boolean(config))
.map(([id, config]) => {
.map(([id]) => {
const metadata = resolveSocialProviderMetadata(id)
return {
id,
name: metadata.name,
icon: metadata.icon,
callbackPath: config?.redirectPath ?? null,
callbackPath: `/api/auth/callback/${id}`,
}
})
}
@@ -120,7 +120,7 @@ export class AuthController {
}
if (!tenantContext || isPlaceholderTenantContext(tenantContext)) {
const {tenantId} = (authContext.user as { tenantId?: string | null })
const { tenantId } = authContext.user as { tenantId?: string | null }
if (tenantId) {
try {
const aggregate = await this.tenantService.getById(tenantId)

View File

@@ -1,3 +1,5 @@
import { createHash } from 'node:crypto'
import { authAccounts, authSessions, authUsers, authVerifications, generateId } from '@afilmory/db'
import type { OnModuleInit } from '@afilmory/framework'
import { createLogger, HttpContext } from '@afilmory/framework'
@@ -21,7 +23,6 @@ const logger = createLogger('Auth')
@injectable()
export class AuthProvider implements OnModuleInit {
private moduleOptionsPromise?: Promise<AuthModuleOptions>
private instances = new Map<string, Promise<BetterAuthInstance>>()
private placeholderTenantId: string | null = null
@@ -33,7 +34,7 @@ export class AuthProvider implements OnModuleInit {
) {}
async onModuleInit(): Promise<void> {
await this.getAuth()
await this.config.getOptions()
}
private resolveTenantIdFromContext(): string | null {
@@ -71,13 +72,6 @@ export class AuthProvider implements OnModuleInit {
return sanitizedSlug ? `better-auth-${sanitizedSlug}` : 'better-auth'
}
private async getModuleOptions(): Promise<AuthModuleOptions> {
if (!this.moduleOptionsPromise) {
this.moduleOptionsPromise = this.config.getOptions()
}
return this.moduleOptionsPromise
}
private async resolveFallbackTenantId(): Promise<string | null> {
if (this.placeholderTenantId) {
return this.placeholderTenantId
@@ -152,7 +146,7 @@ export class AuthProvider implements OnModuleInit {
return entries.reduce<Record<string, { clientId: string; clientSecret: string; redirectURI?: string }>>(
(acc, [key, value]) => {
const redirectUri = this.buildRedirectUri(tenantSlug, key, value, oauthGatewayUrl)
const redirectUri = this.buildRedirectUri(tenantSlug, key, oauthGatewayUrl)
acc[key] = {
clientId: value.clientId,
clientSecret: value.clientSecret,
@@ -167,13 +161,10 @@ export class AuthProvider implements OnModuleInit {
private buildRedirectUri(
tenantSlug: string | null,
provider: keyof SocialProvidersConfig,
options: SocialProviderOptions,
oauthGatewayUrl: string | null,
): string | null {
const basePath = options.redirectPath ?? `/api/auth/callback/${provider}`
if (!basePath.startsWith('/')) {
return null
}
const basePath = `/api/auth/callback/${provider}`
if (oauthGatewayUrl) {
return this.buildGatewayRedirectUri(oauthGatewayUrl, basePath, tenantSlug)
}
@@ -193,8 +184,10 @@ export class AuthProvider implements OnModuleInit {
return `${normalizedBase}${basePath}${query ? `?${query}` : ''}`
}
private async createAuthForEndpoint(tenantSlug: string | null): Promise<BetterAuthInstance> {
const options = await this.getModuleOptions()
private async createAuthForEndpoint(
tenantSlug: string | null,
options: AuthModuleOptions,
): Promise<BetterAuthInstance> {
const db = this.drizzleProvider.getDb()
const socialProviders = this.buildBetterAuthProvidersForHost(
tenantSlug,
@@ -323,7 +316,7 @@ export class AuthProvider implements OnModuleInit {
}
async getAuth(): Promise<BetterAuthInstance> {
const options = await this.getModuleOptions()
const options = await this.config.getOptions()
const endpoint = this.resolveRequestEndpoint()
const fallbackHost = options.baseDomain.trim().toLowerCase()
const requestedHost = (endpoint.host ?? fallbackHost).trim().toLowerCase()
@@ -331,10 +324,11 @@ export class AuthProvider implements OnModuleInit {
const host = this.applyTenantSlugToHost(requestedHost || fallbackHost, fallbackHost, tenantSlug)
const protocol = this.determineProtocol(host, endpoint.protocol)
const slugKey = tenantSlug ?? 'global'
const cacheKey = `${protocol}://${host}::${slugKey}`
const optionSignature = this.computeOptionsSignature(options)
const cacheKey = `${protocol}://${host}::${slugKey}::${optionSignature}`
if (!this.instances.has(cacheKey)) {
const instancePromise = this.createAuthForEndpoint(tenantSlug).then((instance) => {
const instancePromise = this.createAuthForEndpoint(tenantSlug, options).then((instance) => {
logger.info(`Better Auth initialized for ${cacheKey}`)
return instance
})
@@ -344,6 +338,29 @@ export class AuthProvider implements OnModuleInit {
return await this.instances.get(cacheKey)!
}
private computeOptionsSignature(options: AuthModuleOptions): string {
const hash = createHash('sha256')
hash.update(options.baseDomain)
hash.update('|gateway=')
hash.update(options.oauthGatewayUrl ?? 'null')
const providerEntries = Object.entries(options.socialProviders)
.sort(([a], [b]) => a.localeCompare(b))
.map(([provider, config]) => {
const secretHash = config?.clientSecret
? createHash('sha256').update(config.clientSecret).digest('hex')
: 'null'
return {
provider,
clientId: config?.clientId ?? '',
secretHash,
}
})
hash.update(JSON.stringify(providerEntries))
return hash.digest('hex')
}
async handler(context: Context): Promise<Response> {
const auth = await this.getAuth()
return auth.handler(context.req.raw)

View File

@@ -1,13 +1,6 @@
import { createZodDto } from '@afilmory/framework'
import { z } from 'zod'
const redirectPathInputSchema = z
.string()
.trim()
.refine((value) => value.length === 0 || value.startsWith('/'), {
message: '路径必须以 / 开头',
})
const updateSuperAdminSettingsSchema = z
.object({
allowRegistration: z.boolean().optional(),
@@ -30,10 +23,8 @@ const updateSuperAdminSettingsSchema = z
.optional(),
oauthGoogleClientId: z.string().trim().min(1).nullable().optional(),
oauthGoogleClientSecret: z.string().trim().min(1).nullable().optional(),
oauthGoogleRedirectUri: redirectPathInputSchema.nullable().optional(),
oauthGithubClientId: z.string().trim().min(1).nullable().optional(),
oauthGithubClientSecret: z.string().trim().min(1).nullable().optional(),
oauthGithubRedirectUri: redirectPathInputSchema.nullable().optional(),
})
.refine((value) => Object.values(value).some((entry) => entry !== undefined), {
message: '至少需要更新一项设置',

1
photos Submodule

Submodule photos added at 906be11f8e