mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
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:
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user