diff --git a/package-lock.json b/package-lock.json index a083412..8fb8eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "react-dom": "^19" }, "devDependencies": { - "@types/node": "^20", + "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", "prettier": "^3", @@ -589,13 +589,13 @@ } }, "node_modules/@types/node": { - "version": "20.17.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", - "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/react": { @@ -1091,9 +1091,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index ef099a4..7d9710a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "react-dom": "^19" }, "devDependencies": { - "@types/node": "^20", + "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", "prettier": "^3", diff --git a/src/app/api/proxy/paths.ts b/src/app/api/proxy/paths.ts new file mode 100644 index 0000000..621c063 --- /dev/null +++ b/src/app/api/proxy/paths.ts @@ -0,0 +1,3 @@ +export const PROXY_PATHS = { + saveParserBackend: '/api/proxy/saveParserBackend', +} diff --git a/src/app/api/proxy/proxyFunctions.ts b/src/app/api/proxy/proxyFunctions.ts new file mode 100644 index 0000000..27fa824 --- /dev/null +++ b/src/app/api/proxy/proxyFunctions.ts @@ -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 { + 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 { + 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', + }, + }) +} diff --git a/src/app/api/proxy/saveParserBackend/[[...path]]/route.ts b/src/app/api/proxy/saveParserBackend/[[...path]]/route.ts new file mode 100644 index 0000000..508a147 --- /dev/null +++ b/src/app/api/proxy/saveParserBackend/[[...path]]/route.ts @@ -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 { + 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 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0f94fe8..eb97a07 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import 'bootstrap/dist/css/bootstrap.min.css' import './globals.css' +import '@/app/lib/customConsoleLog' import type { Metadata } from 'next' import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community' import Navbar from '@/components/Navbar' diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index dd1ec4c..a5b4e0f 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,7 +1,7 @@ 'use client' import Link from 'next/link' -import { font_Hack } from '@/utils/myFonts' +import { font_Hack } from '@/app/lib/myFonts' export default function Navbar() { return ( diff --git a/src/config/.gitignore b/src/config/.gitignore new file mode 100644 index 0000000..caf3ec6 --- /dev/null +++ b/src/config/.gitignore @@ -0,0 +1 @@ +/runtimeConfig.json diff --git a/src/config/defaultConfig.ts b/src/config/defaultConfig.ts new file mode 100644 index 0000000..0896da0 --- /dev/null +++ b/src/config/defaultConfig.ts @@ -0,0 +1,11 @@ +'server' +export const defaultConfig = { + services: { + saveParserBackend: { + url: 'http://127.0.0.1:5226', + }, + auth: {}, + }, +} + +export type AppConfig = typeof defaultConfig diff --git a/src/lib/configLoader.ts b/src/lib/configLoader.ts new file mode 100644 index 0000000..9b7fb8c --- /dev/null +++ b/src/lib/configLoader.ts @@ -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() diff --git a/src/lib/customConsoleLog.ts b/src/lib/customConsoleLog.ts new file mode 100644 index 0000000..a10549b --- /dev/null +++ b/src/lib/customConsoleLog.ts @@ -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) +} diff --git a/src/utils/myFonts.ts b/src/lib/myFonts.ts similarity index 100% rename from src/utils/myFonts.ts rename to src/lib/myFonts.ts diff --git a/src/services/authService.ts b/src/services/AuthService.ts similarity index 100% rename from src/services/authService.ts rename to src/services/AuthService.ts diff --git a/src/services/SaveParserBackendService.ts b/src/services/SaveParserBackendService.ts index 0faca82..4454e26 100644 --- a/src/services/SaveParserBackendService.ts +++ b/src/services/SaveParserBackendService.ts @@ -10,20 +10,28 @@ export interface SaveStatusResponse { } export class SaveParserBackendService { - private API_BASE = 'http://localhost:5226' + private API_BASE = '/api/proxy/parserBackend' + + private async tryGetResponseJson(res: Response): Promise { + try { + const body = await res.json() + return JSON.stringify(body) + } catch {} + return '' + } async uploadSave(game: string, file: File): Promise { const url = `${this.API_BASE}/uploadSave?game=${encodeURIComponent(game)}` const response = await fetch(url, { method: 'POST', headers: { - 'Content-Type': 'text/plain', + 'Content-Type': 'application/octet-stream', }, body: file, }) 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() @@ -34,7 +42,9 @@ export class SaveParserBackendService { const response = await fetch(url) 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() @@ -45,7 +55,9 @@ export class SaveParserBackendService { const response = await fetch(url) 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()