created proxy for parser backend

This commit is contained in:
Timerix 2025-05-22 23:31:17 +05:00
parent 9faf3f7618
commit 8e4ce66cc4
14 changed files with 146 additions and 15 deletions

16
package-lock.json generated
View File

@ -15,7 +15,7 @@
"react-dom": "^19" "react-dom": "^19"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"prettier": "^3", "prettier": "^3",
@ -589,13 +589,13 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.50", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
@ -1091,9 +1091,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.19.8", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
} }

View File

@ -17,7 +17,7 @@
"react-dom": "^19" "react-dom": "^19"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"prettier": "^3", "prettier": "^3",

View File

@ -0,0 +1,3 @@
export const PROXY_PATHS = {
saveParserBackend: '/api/proxy/saveParserBackend',
}

View File

@ -0,0 +1,55 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
/// Resend request to backend.
/// Body is being sent as stream.
export async function redirectRequest(
req: NextRequest,
proxyUrl: string,
backendBaseUrl: string
): Promise<NextResponse> {
const reqUrl = req.url // full URL e.g. http://localhost:3000/api/proxy?id=123&foo=bar
// Find position of proxyPath in reqUrl
const proxyIndex = reqUrl.indexOf(proxyUrl)
if (proxyIndex === -1) {
return new NextResponse('Invalid proxy path', { status: 400 })
}
// Replace '/api/proxy/parserBackend' with backend URL, preserve everything after
// e.g. '/api/proxy/extra/path?id=123' -> 'http://your-backend-server/extra/path?id=123'
const backendUrl = backendBaseUrl + reqUrl.substring(proxyIndex + proxyUrl.length)
console.log('redirecting', req.url, 'to', backendUrl)
const backendRes = await fetch(backendUrl, {
method: req.method,
headers: req.headers,
body: req.body,
// @ts-ignore
duplex: 'half', // some property required by nodejs but not defined in @types/node
})
return new NextResponse(backendRes.body, {
status: backendRes.status,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': backendRes.headers.get('content-type') || 'application/octet-stream',
},
})
}
/// Browsers send OPTIONS request sometimes to check webserver allowed headers and methods.
/// My backend doesn't support such requests so use proxy to answer to it that all headers and methods are allowed
export async function responseToOPTIONS(req: NextRequest): Promise<NextResponse> {
console.log('proxy responding to CORS OPTIONS', req.url)
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD',
'Access-Control-Allow-Headers':
req.headers.get('access-control-request-headers') || 'Content-Type, Authorization',
},
})
}

View File

@ -0,0 +1,17 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { redirectRequest, responseToOPTIONS as respondToOPTIONS } from '@/app/api/proxy/proxyFunctions'
import { appConfig } from '@/app/lib/configLoader'
import { PROXY_PATHS } from '../../paths'
async function redirect(req: NextRequest): Promise<NextResponse> {
return await redirectRequest(req, PROXY_PATHS.saveParserBackend, appConfig.services.saveParserBackend.url)
}
export const GET = redirect
export const POST = redirect
export const PUT = redirect
export const PATCH = redirect
export const DELETE = redirect
export const HEAD = redirect
export const OPTIONS = respondToOPTIONS

View File

@ -1,5 +1,6 @@
import 'bootstrap/dist/css/bootstrap.min.css' import 'bootstrap/dist/css/bootstrap.min.css'
import './globals.css' import './globals.css'
import '@/app/lib/customConsoleLog'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community' import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
import Navbar from '@/components/Navbar' import Navbar from '@/components/Navbar'

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { font_Hack } from '@/utils/myFonts' import { font_Hack } from '@/app/lib/myFonts'
export default function Navbar() { export default function Navbar() {
return ( return (

1
src/config/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/runtimeConfig.json

View File

@ -0,0 +1,11 @@
'server'
export const defaultConfig = {
services: {
saveParserBackend: {
url: 'http://127.0.0.1:5226',
},
auth: {},
},
}
export type AppConfig = typeof defaultConfig

23
src/lib/configLoader.ts Normal file
View File

@ -0,0 +1,23 @@
'server'
import fs from 'fs'
import path from 'path'
import { AppConfig, defaultConfig } from '@/config/defaultConfig'
const configFilePath = path.join(process.cwd(), 'config', 'runtimeConfig.json')
function loadConfig(): AppConfig {
let config = defaultConfig
if (fs.existsSync(configFilePath)) {
const raw = fs.readFileSync(configFilePath, 'utf-8')
const parsed = JSON.parse(raw)
config = { ...defaultConfig, ...parsed }
} else {
fs.mkdirSync(path.dirname(configFilePath), { recursive: true })
}
fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 4))
return config
}
export const appConfig = loadConfig()

View File

@ -0,0 +1,8 @@
'server'
const originalLog = console.log
console.log = (message?: any, ...optionalParams: any[]) => {
const now = new Date()
const timestamp = now.toTimeString().split(' ')[0] // hh:mm:ss
originalLog(`[${timestamp}] ${message}`, ...optionalParams)
}

View File

@ -10,20 +10,28 @@ export interface SaveStatusResponse {
} }
export class SaveParserBackendService { export class SaveParserBackendService {
private API_BASE = 'http://localhost:5226' private API_BASE = '/api/proxy/parserBackend'
private async tryGetResponseJson(res: Response): Promise<string> {
try {
const body = await res.json()
return JSON.stringify(body)
} catch {}
return ''
}
async uploadSave(game: string, file: File): Promise<UploadSaveResponse> { async uploadSave(game: string, file: File): Promise<UploadSaveResponse> {
const url = `${this.API_BASE}/uploadSave?game=${encodeURIComponent(game)}` const url = `${this.API_BASE}/uploadSave?game=${encodeURIComponent(game)}`
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'application/octet-stream',
}, },
body: file, body: file,
}) })
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to upload save: ${response.statusText} \n${await response.text()}`) throw new Error(`Failed to upload save: ${response.statusText} ${await this.tryGetResponseJson(response)}`)
} }
return await response.json() return await response.json()
@ -34,7 +42,9 @@ export class SaveParserBackendService {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to get save status: ${response.statusText} \n${await response.text()}`) throw new Error(
`Failed to get save status: ${response.statusText} ${await this.tryGetResponseJson(response)}`
)
} }
return await response.json() return await response.json()
@ -45,7 +55,9 @@ export class SaveParserBackendService {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to get parsed save: ${response.statusText} \n${await response.text()}`) throw new Error(
`Failed to get parsed save: ${response.statusText} ${await this.tryGetResponseJson(response)}`
)
} }
return await response.json() return await response.json()