mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 14:44:48 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -4,7 +4,6 @@ import { injectable } from 'tsyringe'
|
||||
export interface SocialProviderOptions {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
redirectPath?: string | null
|
||||
}
|
||||
|
||||
export interface SocialProvidersConfig {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
1
photos
Submodule
Submodule photos added at 906be11f8e
Reference in New Issue
Block a user