mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
feat: implement simple CORS support and enhance error handling
- Added SimpleCorsInterceptor to manage CORS headers for requests. - Introduced AllowSimpleCors decorator for easy application of CORS settings on controllers and methods. - Created helper function to apply CORS headers consistently across responses. - Updated TenantController to utilize the new CORS functionality. - Added COMMON_FORBIDDEN error code to improve error handling. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ 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'
|
||||
@@ -45,6 +46,7 @@ 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())
|
||||
|
||||
31
be/apps/core/src/decorators/simple-cors.decorator.ts
Normal file
31
be/apps/core/src/decorators/simple-cors.decorator.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export enum ErrorCode {
|
||||
COMMON_CONFLICT = 4,
|
||||
COMMON_RATE_LIMITED = 5,
|
||||
COMMON_INTERNAL_SERVER_ERROR = 6,
|
||||
COMMON_FORBIDDEN = 7,
|
||||
|
||||
// Auth
|
||||
AUTH_UNAUTHORIZED = 10,
|
||||
@@ -58,6 +59,10 @@ export const ERROR_CODE_DESCRIPTORS: Record<ErrorCode, ErrorDescriptor> = {
|
||||
httpStatus: 500,
|
||||
message: 'Internal server error',
|
||||
},
|
||||
[ErrorCode.COMMON_FORBIDDEN]: {
|
||||
httpStatus: 403,
|
||||
message: 'Forbidden',
|
||||
},
|
||||
[ErrorCode.AUTH_UNAUTHORIZED]: {
|
||||
httpStatus: 401,
|
||||
message: 'Unauthorized',
|
||||
|
||||
6
be/apps/core/src/helpers/cors.helper.ts
Normal file
6
be/apps/core/src/helpers/cors.helper.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
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
|
||||
}
|
||||
19
be/apps/core/src/interceptors/simple-cors.interceptor.ts
Normal file
19
be/apps/core/src/interceptors/simple-cors.interceptor.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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,5 +1,6 @@
|
||||
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'
|
||||
@@ -94,9 +95,6 @@ export const StaticControllerUtils = {
|
||||
},
|
||||
|
||||
applyStaticAssetCors(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
|
||||
return applySimpleCorsHeaders(response)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,46 @@
|
||||
import { Body, Controller, Post } from '@afilmory/framework'
|
||||
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'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { TenantService } from './tenant.service'
|
||||
|
||||
const TENANT_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i
|
||||
|
||||
type CheckTenantSlugRequest = {
|
||||
slug?: string | null
|
||||
}
|
||||
const checkTenantSlugSchema = z.object({
|
||||
slug: z
|
||||
.union([z.string(), z.null()])
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) {
|
||||
return null
|
||||
}
|
||||
const normalized = val.trim().toLowerCase()
|
||||
return normalized.length > 0 ? normalized : null
|
||||
})
|
||||
.pipe(
|
||||
z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine((val) => val !== null, { message: '空间名称不能为空' })
|
||||
.transform((val) => val as string)
|
||||
.pipe(
|
||||
z
|
||||
.string()
|
||||
.min(3, { message: '空间名称至少需要 3 个字符' })
|
||||
.max(63, { message: '空间名称长度不能超过 63 个字符' })
|
||||
.regex(TENANT_SLUG_PATTERN, {
|
||||
message: '空间名称只能包含字母、数字或连字符 (-),且不能以连字符开头或结尾。',
|
||||
}),
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
class CheckTenantSlugDto extends createZodSchemaDto(checkTenantSlugSchema) {}
|
||||
|
||||
@Controller('tenant')
|
||||
export class TenantController {
|
||||
@@ -20,18 +49,14 @@ export class TenantController {
|
||||
private readonly systemSettings: SystemSettingService,
|
||||
) {}
|
||||
|
||||
@AllowSimpleCors()
|
||||
@AllowPlaceholderTenant()
|
||||
@SkipTenantGuard()
|
||||
@Post('/check-slug')
|
||||
async checkTenantSlug(@Body() body: CheckTenantSlugRequest) {
|
||||
async checkTenantSlug(@Body() body: CheckTenantSlugDto) {
|
||||
await this.systemSettings.ensureRegistrationAllowed()
|
||||
|
||||
const slug = this.normalizeTenantSlug(body?.slug)
|
||||
if (!slug) {
|
||||
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, { message: '空间名称不能为空' })
|
||||
}
|
||||
|
||||
this.validateTenantSlug(slug)
|
||||
const { slug } = body
|
||||
|
||||
if (isTenantSlugReserved(slug)) {
|
||||
throw new BizException(ErrorCode.TENANT_SLUG_RESERVED, { message: '该空间名称已被系统保留,请尝试其他名称。' })
|
||||
@@ -54,28 +79,6 @@ export class TenantController {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeTenantSlug(slug?: string | null): string | null {
|
||||
if (!slug) {
|
||||
return null
|
||||
}
|
||||
const normalized = slug.trim().toLowerCase()
|
||||
return normalized.length > 0 ? normalized : null
|
||||
}
|
||||
|
||||
private validateTenantSlug(slug: string): void {
|
||||
if (slug.length < 3) {
|
||||
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, { message: '空间名称至少需要 3 个字符' })
|
||||
}
|
||||
if (slug.length > 63) {
|
||||
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, { message: '空间名称长度不能超过 63 个字符' })
|
||||
}
|
||||
if (!TENANT_SLUG_PATTERN.test(slug)) {
|
||||
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, {
|
||||
message: '空间名称只能包含字母、数字或连字符 (-),且不能以连字符开头或结尾。',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private buildTenantWelcomeUrl(slug: string, baseDomain: string): string {
|
||||
const normalizedBase = baseDomain.trim()
|
||||
const host = normalizedBase ? `${slug}.${normalizedBase}` : slug
|
||||
|
||||
Reference in New Issue
Block a user