feat: enhance static asset handling and improve request processing

- Refactored StaticAssetService to remove unnecessary request options, simplifying the handleRequest method.
- Introduced resolveRequestHost method to streamline host resolution logic.
- Updated StaticBaseController and StaticShareController to align with the new request handling approach.
- Added I18nProvider to tenant missing and restricted entry points for improved localization support.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-23 00:09:28 +08:00
parent 9095bb08c8
commit f598add893
7 changed files with 53 additions and 59 deletions

View File

@@ -217,3 +217,7 @@ This project contains multiple web applications with distinct design systems. Fo
- **`apps/web`**: Contains the "Glassmorphic Depth Design System" for the main user-facing photo gallery. See `apps/web/AGENTS.md` for details.
- **`be/apps/dashboard`**: Contains guidelines for the functional, data-driven UI of the administration panel. See `be/apps/dashboard/AGENTS.md` for details.
## IMPORTANT
Avoid feature gates/flags and any backwards compability changes - since our app is still unreleased" is really helpful.

View File

@@ -5,7 +5,7 @@ import { extname, isAbsolute, join, normalize, relative, resolve } from 'node:pa
import { Readable } from 'node:stream'
import type { PrettyLogger } from '@afilmory/framework'
import { createLogger } from '@afilmory/framework'
import { createLogger, HttpContext } from '@afilmory/framework'
import { DOMParser } from 'linkedom'
import { lookup as lookupMimeType } from 'mime-types'
@@ -34,10 +34,6 @@ export interface StaticAssetServiceOptions {
staticAssetHostResolver?: (requestHost?: string | null) => Promise<string | null>
}
export interface StaticAssetRequestOptions {
requestHost?: string | null
}
export interface ResolvedStaticAsset {
absolutePath: string
relativePath: string
@@ -61,11 +57,7 @@ export abstract class StaticAssetService {
this.staticAssetHostResolver = options.staticAssetHostResolver
}
async handleRequest(
fullPath: string,
headOnly: boolean,
options?: StaticAssetRequestOptions,
): Promise<Response | null> {
async handleRequest(fullPath: string, headOnly: boolean): Promise<Response | null> {
const staticRoot = await this.resolveStaticRoot()
if (!staticRoot) {
return null
@@ -78,7 +70,7 @@ export abstract class StaticAssetService {
return null
}
return await this.createResponse(target, headOnly, options)
return await this.createResponse(target, headOnly)
}
protected get routeSegment(): string {
@@ -364,13 +356,9 @@ export abstract class StaticAssetService {
return relativePath !== '' && !relativePath.startsWith('..') && !isAbsolute(relativePath)
}
private async createResponse(
file: ResolvedStaticAsset,
headOnly: boolean,
options?: StaticAssetRequestOptions,
): Promise<Response> {
private async createResponse(file: ResolvedStaticAsset, headOnly: boolean): Promise<Response> {
if (this.isHtml(file.relativePath)) {
return await this.createHtmlResponse(file, headOnly, options)
return await this.createHtmlResponse(file, headOnly)
}
const mimeType = lookupMimeType(file.absolutePath) || 'application/octet-stream'
@@ -390,13 +378,9 @@ export abstract class StaticAssetService {
return new Response(body, { headers, status: 200 })
}
private async createHtmlResponse(
file: ResolvedStaticAsset,
headOnly: boolean,
options?: StaticAssetRequestOptions,
): Promise<Response> {
private async createHtmlResponse(file: ResolvedStaticAsset, headOnly: boolean): Promise<Response> {
const html = await readFile(file.absolutePath, 'utf-8')
const transformed = await this.transformIndexHtml(html, file, options)
const transformed = await this.transformIndexHtml(html, file)
const headers = new Headers()
headers.set('content-type', 'text/html; charset=utf-8')
headers.set('content-length', `${Buffer.byteLength(transformed, 'utf-8')}`)
@@ -410,16 +394,12 @@ export abstract class StaticAssetService {
return new Response(transformed, { headers, status: 200 })
}
private async transformIndexHtml(
html: string,
file: ResolvedStaticAsset,
options?: StaticAssetRequestOptions,
): Promise<string> {
private async transformIndexHtml(html: string, file: ResolvedStaticAsset): Promise<string> {
try {
const document = DOM_PARSER.parseFromString(html, 'text/html') as unknown as StaticAssetDocument
await this.decorateDocument(document, file)
if (this.shouldRewriteAssetReferences(file)) {
const staticAssetHost = await this.getStaticAssetHost(options?.requestHost)
const staticAssetHost = await this.getStaticAssetHost(this.resolveRequestHost())
this.rewriteStaticAssetReferences(document, staticAssetHost)
}
return document.documentElement.outerHTML
@@ -458,6 +438,29 @@ export abstract class StaticAssetService {
return requestHost.trim().toLowerCase()
}
private resolveRequestHost(): string | null {
const context = HttpContext.getValue('hono')
if (!context) {
return null
}
const forwardedHost = context.req.header('x-forwarded-host')?.trim()
if (forwardedHost) {
return forwardedHost
}
const host = context.req.header('host')?.trim()
if (host) {
return host
}
try {
const url = new URL(context.req.url)
return url.hostname
} catch {
return null
}
}
private shouldTreatAsImmutable(relativePath: string): boolean {
if (this.isHtml(relativePath)) {
return false

View File

@@ -1,7 +1,6 @@
import type { Context } from 'hono'
import type { StaticAssetService } from './static-asset.service'
import { StaticControllerUtils } from './static-controller.utils'
import type { StaticDashboardService } from './static-dashboard.service'
import { STATIC_DASHBOARD_BASENAME } from './static-dashboard.service'
import type { StaticWebService } from './static-web.service'
@@ -20,9 +19,7 @@ export abstract class StaticBaseController {
protected async serve(context: Context, service: StaticAssetService, headOnly: boolean): Promise<Response> {
const pathname = context.req.path
const normalizedPath = this.normalizeRequestPath(pathname, service)
const response = await service.handleRequest(normalizedPath, headOnly, {
requestHost: StaticControllerUtils.resolveRequestHost(context),
})
const response = await service.handleRequest(normalizedPath, headOnly)
if (response) {
return response

View File

@@ -2,7 +2,6 @@ import { isTenantSlugReserved } from '@afilmory/utils'
import { BizException, ErrorCode } from 'core/errors'
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'
import type { StaticDashboardService } from './static-dashboard.service'
import { STATIC_DASHBOARD_BASENAME } from './static-dashboard.service'
@@ -11,25 +10,6 @@ const TENANT_MISSING_ENTRY_PATH = `${STATIC_DASHBOARD_BASENAME}/tenant-missing.h
const TENANT_RESTRICTED_ENTRY_PATH = `${STATIC_DASHBOARD_BASENAME}/tenant-restricted.html`
export const StaticControllerUtils = {
resolveRequestHost(context: Context): string | null {
const forwardedHost = context.req.header('x-forwarded-host')?.trim()
if (forwardedHost) {
return forwardedHost
}
const host = context.req.header('host')?.trim()
if (host) {
return host
}
try {
const url = new URL(context.req.url)
return url.host
} catch {
return null
}
},
cloneResponseWithStatus(response: Response, status: number): Response {
const headers = new Headers(response.headers)
return new Response(response.body, {

View File

@@ -5,7 +5,7 @@ import { z } from 'zod'
import { StaticControllerUtils } from './static-controller.utils'
import { StaticDashboardService } from './static-dashboard.service'
import { STATIC_SHARE_ENTRY_PATH,StaticShareService } from './static-share.service'
import { STATIC_SHARE_ENTRY_PATH, StaticShareService } from './static-share.service'
const shareQuerySchema = z.object({
id: z.string().min(1, 'Photo ID(s) required'),
@@ -28,9 +28,7 @@ export class StaticShareController {
return await StaticControllerUtils.renderTenantMissingPage(this.staticDashboardService)
}
const response = await this.staticShareService.handleRequest(STATIC_SHARE_ENTRY_PATH, false, {
requestHost: StaticControllerUtils.resolveRequestHost(context),
})
const response = await this.staticShareService.handleRequest(STATIC_SHARE_ENTRY_PATH, false)
if (!response || response.status === 404) {
throw new BizException(ErrorCode.COMMON_NOT_FOUND, {

View File

@@ -2,6 +2,8 @@ import '../styles/index.css'
import { createRoot } from 'react-dom/client'
import { I18nProvider } from '~/providers/i18n-provider'
import { TenantMissingStandalone } from '../modules/welcome/components/TenantMissingStandalone'
const root = document.querySelector('#root')
@@ -10,4 +12,8 @@ if (!root) {
throw new Error('Root element not found for tenant missing entry.')
}
createRoot(root).render(<TenantMissingStandalone />)
createRoot(root).render(
<I18nProvider>
<TenantMissingStandalone />
</I18nProvider>,
)

View File

@@ -2,6 +2,8 @@ import '../styles/index.css'
import { createRoot } from 'react-dom/client'
import { I18nProvider } from '~/providers/i18n-provider'
import { TenantRestrictedStandalone } from '../modules/welcome/components/TenantRestrictedStandalone'
const root = document.querySelector('#root')
@@ -10,4 +12,8 @@ if (!root) {
throw new Error('Root element not found for tenant restricted entry.')
}
createRoot(root).render(<TenantRestrictedStandalone />)
createRoot(root).render(
<I18nProvider>
<TenantRestrictedStandalone />
</I18nProvider>,
)