refactor: remove Simple CORS implementation and related components

- Deleted SimpleCorsInterceptor, associated decorator, and helper functions to streamline CORS handling.
- Updated CorsMiddleware to use a simplified CORS configuration.
- Refactored StaticAssetController and StaticAssetService to remove references to the removed CORS functionality.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-19 15:36:19 +08:00
parent da429585da
commit 32dc0e661f
9 changed files with 11 additions and 268 deletions

View File

@@ -10,7 +10,6 @@ import { PgPoolProvider } from './database/database.provider'
import { AllExceptionsFilter } from './filters/all-exceptions.filter'
import { LoggingInterceptor } from './interceptors/logging.interceptor'
import { ResponseTransformInterceptor } from './interceptors/response-transform.interceptor'
import { SimpleCorsInterceptor } from './interceptors/simple-cors.interceptor'
import { AppModules } from './modules/index.module'
import { registerOpenApiRoutes } from './openapi'
import { RedisProvider } from './redis/redis.provider'
@@ -46,7 +45,6 @@ export async function createConfiguredApp(options: BootstrapOptions = {}): Promi
app.useGlobalFilters(new AllExceptionsFilter())
app.useGlobalInterceptors(new LoggingInterceptor())
app.useGlobalInterceptors(new SimpleCorsInterceptor())
app.useGlobalInterceptors(new ResponseTransformInterceptor())
app.useGlobalPipes(new GlobalValidationPipe())

View File

@@ -1,31 +0,0 @@
const SIMPLE_CORS_METADATA = Symbol.for('core.cors.simple')
type DecoratorTarget = object | Function
function setSimpleCorsMetadata(target: DecoratorTarget): void {
Reflect.defineMetadata(SIMPLE_CORS_METADATA, true, target)
}
export function AllowSimpleCors(): ClassDecorator & MethodDecorator {
return ((target: DecoratorTarget, _propertyKey?: string | symbol, descriptor?: PropertyDescriptor) => {
if (descriptor?.value && typeof descriptor.value === 'function') {
setSimpleCorsMetadata(descriptor.value)
return descriptor
}
setSimpleCorsMetadata(target)
return descriptor
}) as unknown as ClassDecorator & MethodDecorator
}
export function shouldAllowSimpleCors(target: DecoratorTarget | undefined): boolean {
if (!target) {
return false
}
try {
return (Reflect.getMetadata(SIMPLE_CORS_METADATA, target) ?? false) === true
} catch {
return false
}
}

View File

@@ -1,6 +0,0 @@
export function applySimpleCorsHeaders(response: Response): Response {
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type')
return response
}

View File

@@ -1,19 +0,0 @@
import type { CallHandler, ExecutionContext, FrameworkResponse, Interceptor } from '@afilmory/framework'
import { shouldAllowSimpleCors } from 'core/decorators/simple-cors.decorator'
import { applySimpleCorsHeaders } from 'core/helpers/cors.helper'
import { injectable } from 'tsyringe'
@injectable()
export class SimpleCorsInterceptor implements Interceptor {
async intercept(context: ExecutionContext, next: CallHandler): Promise<FrameworkResponse> {
const handler = context.getHandler()
const clazz = context.getClass()
if (!shouldAllowSimpleCors(handler) && !shouldAllowSimpleCors(clazz)) {
return await next.handle()
}
const response = await next.handle()
return applySimpleCorsHeaders(response)
}
}

View File

@@ -1,204 +1,22 @@
import type { HttpMiddleware, OnModuleDestroy, OnModuleInit } from '@afilmory/framework'
import { EventEmitterService, Middleware } from '@afilmory/framework'
import { logger } from 'core/helpers/logger.helper'
import { AppStateService } from 'core/modules/app/app-state/app-state.service'
import { getTenantContext } from 'core/modules/platform/tenant/tenant.context'
import { TenantContextResolver } from 'core/modules/platform/tenant/tenant-context-resolver.service'
import type { Context } from 'hono'
import type { HttpMiddleware } from '@afilmory/framework'
import { Middleware } from '@afilmory/framework'
import type { Context, Next } from 'hono'
import { cors } from 'hono/cors'
import { injectable } from 'tsyringe'
type AllowedOrigins = '*' | string[]
function normalizeOriginValue(value: string): string {
const trimmed = value.trim()
if (trimmed === '' || trimmed === '*') {
return trimmed
}
try {
const url = new URL(trimmed)
return `${url.protocol}//${url.host}`
} catch {
return trimmed.replace(/\/+$/, '')
}
}
function parseAllowedOrigins(raw: string | null): AllowedOrigins {
if (!raw) {
return '*'
}
const entries = raw
.split(/[\n,]/)
.map((value) => normalizeOriginValue(value))
.filter((value) => value.length > 0)
if (entries.length === 0 || entries.includes('*')) {
return '*'
}
return Array.from(new Set(entries))
}
@Middleware()
@injectable()
export class CorsMiddleware implements HttpMiddleware, OnModuleInit, OnModuleDestroy {
private readonly allowedOrigins = new Map<string, AllowedOrigins>()
private readonly logger = logger.extend('CorsMiddleware')
constructor(
private readonly eventEmitter: EventEmitterService,
private readonly tenantContextResolver: TenantContextResolver,
private readonly appState: AppStateService,
) {}
export class CorsMiddleware implements HttpMiddleware {
private readonly corsMiddleware = cors({
origin: (origin) => this.resolveOrigin(origin),
origin: (origin) => {
return origin || '*'
},
credentials: true,
allowMethods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
})
private readonly handleSettingUpdated = ({ tenantId, key }: { tenantId: string; key: string }) => {
if (key !== 'http.cors.allowedOrigins') {
return
}
void this.reloadAllowedOrigins(tenantId)
}
private readonly handleSettingDeleted = ({ tenantId, key }: { tenantId: string; key: string }) => {
if (key !== 'http.cors.allowedOrigins') {
return
}
this.allowedOrigins.delete(tenantId)
}
async onModuleInit(): Promise<void> {
this.eventEmitter.on('setting.updated', this.handleSettingUpdated)
this.eventEmitter.on('setting.deleted', this.handleSettingDeleted)
}
async onModuleDestroy(): Promise<void> {
this.eventEmitter.off('setting.updated', this.handleSettingUpdated)
this.eventEmitter.off('setting.deleted', this.handleSettingDeleted)
}
private addAllCorsHeaders(context: Context): void {
context.res.headers.set('Access-Control-Allow-Origin', context.req.header('Origin') ?? '*')
context.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
context.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
context.res.headers.set('Access-Control-Allow-Credentials', 'true')
}
private handleOptionsPreflight(context: Context): Response {
const origin = context.req.header('Origin')
if (origin) {
context.res.headers.append('Vary', 'Origin')
}
// Align with typical CORS preflight behavior
const requestHeaders = context.req.header('Access-Control-Request-Headers')
if (requestHeaders) {
context.res.headers.set('Access-Control-Allow-Headers', requestHeaders)
context.res.headers.append('Vary', 'Access-Control-Request-Headers')
}
context.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
context.res.headers.set('Access-Control-Allow-Credentials', 'true')
context.res.headers.set('Access-Control-Allow-Origin', origin ?? '*')
// Ensure no body-related headers on 204
context.res.headers.delete('Content-Length')
context.res.headers.delete('Content-Type')
return new Response(null, {
headers: context.res.headers,
status: 204,
statusText: 'No Content',
})
}
['use']: HttpMiddleware['use'] = async (context, next) => {
const initialized = await this.appState.isInitialized()
if (!initialized) {
this.logger.info(`Application not initialized yet, skip CORS middleware for ${context.req.path}`)
if (context.req.method === 'OPTIONS') {
return this.handleOptionsPreflight(context)
}
this.addAllCorsHeaders(context)
return await next()
}
const tenantContext = await this.tenantContextResolver.resolve(context, {
skipInitializationCheck: true,
})
const tenantId = tenantContext?.tenant.id
if (tenantId) {
await this.ensureTenantOriginsLoaded(tenantId)
} else {
this.logger.warn(`Tenant context missing for request ${context.req.method} ${context.req.path}`)
}
async use(context: Context, next: Next) {
return await this.corsMiddleware(context, next)
}
private async ensureTenantOriginsLoaded(tenantId: string): Promise<void> {
if (this.allowedOrigins.has(tenantId)) {
return
}
await this.reloadAllowedOrigins(tenantId)
}
private async reloadAllowedOrigins(tenantId: string): Promise<void> {
// let raw: string | null = null
// try {
// raw = await this.settingService.get('http.cors.allowedOrigins', { tenantId })
// } catch (error) {
// this.logger.warn('Failed to load CORS configuration from settings for tenant', tenantId, error)
// }
this.updateAllowedOrigins(tenantId, null)
}
private updateAllowedOrigins(tenantId: string, next: string | null): void {
const parsed = parseAllowedOrigins(next)
this.allowedOrigins.set(tenantId, parsed)
this.logger.info('Updated CORS allowed origins for tenant', tenantId, parsed === '*' ? '*' : JSON.stringify(parsed))
}
private resolveOrigin(origin: string | undefined): string | null {
if (!origin) {
return null
}
const normalized = normalizeOriginValue(origin)
if (!normalized) {
return null
}
const tenantContext = getTenantContext()
const tenantId = tenantContext?.tenant.id
if (!tenantId) {
return null
}
const allowed = this.allowedOrigins.get(tenantId)
if (!allowed) {
return null
}
if (allowed === '*') {
return normalized
}
return allowed.includes(normalized) ? normalized : null
}
}

View File

@@ -4,7 +4,6 @@ import { SkipTenantGuard } from 'core/decorators/skip-tenant.decorator'
import type { Context } from 'hono'
import { StaticBaseController } from './static-base.controller'
import { StaticControllerUtils } from './static-controller.utils'
import { StaticDashboardService } from './static-dashboard.service'
import { StaticWebService } from './static-web.service'
@@ -18,7 +17,6 @@ export class StaticAssetController extends StaticBaseController {
@AllowPlaceholderTenant()
@Get('/*')
async getAsset(@ContextParam() context: Context) {
const response = await this.handleAssetRequest(context, false)
return StaticControllerUtils.applyStaticAssetCors(response)
return await this.handleAssetRequest(context, false)
}
}

View File

@@ -380,7 +380,6 @@ export abstract class StaticAssetService {
headers.set('last-modified', file.stats.mtime.toUTCString())
this.applyCacheHeaders(headers, file.relativePath)
this.applyCorsHeaders(headers)
if (headOnly) {
return new Response(null, { headers, status: 200 })
@@ -403,7 +402,6 @@ export abstract class StaticAssetService {
headers.set('content-length', `${Buffer.byteLength(transformed, 'utf-8')}`)
headers.set('last-modified', file.stats.mtime.toUTCString())
this.applyCacheHeaders(headers, file.relativePath)
this.applyCorsHeaders(headers)
if (headOnly) {
return new Response(null, { headers, status: 200 })
@@ -475,12 +473,6 @@ export abstract class StaticAssetService {
headers.set('surrogate-control', policy.cdn)
}
private applyCorsHeaders(headers: Headers): void {
headers.set('access-control-allow-origin', '*')
headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS')
headers.set('access-control-allow-headers', 'content-type')
}
private resolveCachePolicy(relativePath: string): { browser: string; cdn: string } {
if (this.isHtml(relativePath)) {
return {

View File

@@ -1,6 +1,5 @@
import { isTenantSlugReserved } from '@afilmory/utils'
import { BizException, ErrorCode } from 'core/errors'
import { applySimpleCorsHeaders } from 'core/helpers/cors.helper'
import { ROOT_TENANT_SLUG } from 'core/modules/platform/tenant/tenant.constants'
import { getTenantContext, isPlaceholderTenantContext } from 'core/modules/platform/tenant/tenant.context'
import type { Context } from 'hono'
@@ -93,8 +92,4 @@ export const StaticControllerUtils = {
message: 'Workspace access restricted',
})
},
applyStaticAssetCors(response: Response): Response {
return applySimpleCorsHeaders(response)
},
}

View File

@@ -1,7 +1,6 @@
import { Body, Controller, createZodSchemaDto, Post } from '@afilmory/framework'
import { isTenantSlugReserved } from '@afilmory/utils'
import { AllowPlaceholderTenant } from 'core/decorators/allow-placeholder.decorator'
import { AllowSimpleCors } from 'core/decorators/simple-cors.decorator'
import { SkipTenantGuard } from 'core/decorators/skip-tenant.decorator'
import { BizException, ErrorCode } from 'core/errors'
import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service'
@@ -49,7 +48,6 @@ export class TenantController {
private readonly systemSettings: SystemSettingService,
) {}
@AllowSimpleCors()
@AllowPlaceholderTenant()
@SkipTenantGuard()
@Post('/check-slug')