Rocky_Mountain_Vending/app/api/manuals/[...path]/route.ts
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

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'
// 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 = join(process.cwd(), '..', 'manuals-data', 'manuals')
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 })
}
}