114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { readFile } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { existsSync } from 'fs'
|
|
import { getManualsFilesRoot } from '@/lib/manuals-paths'
|
|
|
|
// API routes are not supported in static export (GHL hosting)
|
|
// Manuals are now served as static files from /manuals/
|
|
export const dynamic = 'force-static'
|
|
|
|
/**
|
|
* API route to serve PDF manuals
|
|
* This allows serving files from outside the public folder
|
|
*
|
|
* Usage: /api/manuals/BevMax/manual.pdf
|
|
* NOTE: This route is disabled for static export. Use /manuals/ paths instead.
|
|
*/
|
|
|
|
// Required for static export - returns empty array to skip this route
|
|
export async function generateStaticParams(): Promise<Array<{ path: string[] }>> {
|
|
return []
|
|
}
|
|
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ path: string[] }> }
|
|
) {
|
|
try {
|
|
const { path: pathArray } = await params
|
|
|
|
// Decode URL-encoded path segments - handle multiple encodings
|
|
const decodedPath = pathArray.map(segment => {
|
|
try {
|
|
// Try decoding multiple times in case of double encoding
|
|
let decoded = segment
|
|
while (decoded !== decodeURIComponent(decoded)) {
|
|
decoded = decodeURIComponent(decoded)
|
|
}
|
|
return decoded
|
|
} catch {
|
|
// If decoding fails, use original
|
|
return segment
|
|
}
|
|
})
|
|
const filePath = decodedPath.join('/')
|
|
|
|
// Security: Prevent directory traversal
|
|
if (filePath.includes('..') || filePath.startsWith('/')) {
|
|
return new NextResponse('Invalid path', { status: 400 })
|
|
}
|
|
|
|
// Construct full path to manual
|
|
const manualsDir = getManualsFilesRoot()
|
|
const fullPath = join(manualsDir, filePath)
|
|
|
|
// Normalize paths to handle both forward and backward slashes
|
|
const normalizedFullPath = fullPath.replace(/\\/g, '/')
|
|
const normalizedManualsDir = manualsDir.replace(/\\/g, '/')
|
|
|
|
// Verify file exists and is within manuals directory
|
|
let fileToRead = normalizedFullPath
|
|
if (!existsSync(normalizedFullPath)) {
|
|
// Try with original path in case of encoding issues
|
|
if (existsSync(fullPath)) {
|
|
fileToRead = fullPath
|
|
} else {
|
|
// Try with different path separators
|
|
const altPath = fullPath.replace(/\//g, '\\')
|
|
if (existsSync(altPath)) {
|
|
fileToRead = altPath
|
|
} else {
|
|
console.error(`File not found: ${normalizedFullPath}`)
|
|
console.error(`Also tried: ${fullPath}`)
|
|
return new NextResponse('File not found', { status: 404 })
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify the file is within the manuals directory (security check)
|
|
const resolvedPath = fileToRead.replace(/\\/g, '/')
|
|
if (!resolvedPath.startsWith(normalizedManualsDir.replace(/\\/g, '/'))) {
|
|
console.error(`Security violation: Path outside manuals directory: ${resolvedPath}`)
|
|
return new NextResponse('Invalid path', { status: 400 })
|
|
}
|
|
|
|
// Only allow PDF files
|
|
if (!filePath.toLowerCase().endsWith('.pdf')) {
|
|
return new NextResponse('Invalid file type', { status: 400 })
|
|
}
|
|
|
|
// Read and serve the file
|
|
const fileBuffer = await readFile(fileToRead)
|
|
|
|
// Get filename for Content-Disposition header
|
|
const filename = decodedPath[decodedPath.length - 1]
|
|
|
|
return new NextResponse(fileBuffer, {
|
|
headers: {
|
|
'Content-Type': 'application/pdf',
|
|
'Content-Disposition': `inline; filename="${encodeURIComponent(filename)}"`,
|
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
'X-Content-Type-Options': 'nosniff',
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('Error serving manual:', error)
|
|
if (error instanceof Error) {
|
|
console.error('Error details:', error.message, error.stack)
|
|
}
|
|
return new NextResponse('Internal server error', { status: 500 })
|
|
}
|
|
}
|
|
|
|
|