From e3326998c43a5f035c763aa4c93e94078249f6cb Mon Sep 17 00:00:00 2001 From: skidoodle Date: Fri, 20 Jun 2025 05:15:23 +0200 Subject: [PATCH] app router --- .npmrc | 1 - .prettierrc | 7 -- biome.json | 7 +- postcss.config.js => postcss.config.mjs | 2 +- src/app/api/erettsegi/route.ts | 120 ++++++++++++++++++++++ src/app/api/proxy/route.ts | 79 +++++++++++++++ src/app/api/validate/route.ts | 73 ++++++++++++++ src/app/layout.tsx | 52 ++++++++++ src/{pages/404.tsx => app/not-found.tsx} | 18 ++-- src/{pages/index.tsx => app/page.tsx} | 2 + src/app/providers.tsx | 14 +++ src/components/Buttons.tsx | 2 + src/components/Footer.tsx | 2 + src/components/Selectors.tsx | 2 + src/components/Source.tsx | 2 + src/components/ThemeSwitcher.tsx | 2 + src/hooks/useState.ts | 2 + src/hooks/useYears.tsx | 2 + src/pages/_app.tsx | 30 ------ src/pages/_document.tsx | 35 ------- src/pages/api/erettsegi.ts | 122 ----------------------- src/pages/api/proxy.ts | 77 -------------- src/pages/api/validate.ts | 70 ------------- tsconfig.json | 51 +++++----- 24 files changed, 391 insertions(+), 383 deletions(-) delete mode 100644 .npmrc delete mode 100644 .prettierrc rename postcss.config.js => postcss.config.mjs (72%) create mode 100644 src/app/api/erettsegi/route.ts create mode 100644 src/app/api/proxy/route.ts create mode 100644 src/app/api/validate/route.ts create mode 100644 src/app/layout.tsx rename src/{pages/404.tsx => app/not-found.tsx} (75%) rename src/{pages/index.tsx => app/page.tsx} (99%) create mode 100644 src/app/providers.tsx delete mode 100644 src/pages/_app.tsx delete mode 100644 src/pages/_document.tsx delete mode 100644 src/pages/api/erettsegi.ts delete mode 100644 src/pages/api/proxy.ts delete mode 100644 src/pages/api/validate.ts diff --git a/.npmrc b/.npmrc deleted file mode 100644 index cb15887..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -public-hoist-pattern[]=*@nextui-org/* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index a3c4bea..0000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": false, - "singleQuote": true, - "jsxSingleQuote": true -} diff --git a/biome.json b/biome.json index c054f53..1e3c390 100644 --- a/biome.json +++ b/biome.json @@ -1,16 +1,17 @@ { "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", "vcs": { - "enabled": false, + "enabled": true, "clientKind": "git", - "useIgnoreFile": false + "useIgnoreFile": true }, "files": { "ignoreUnknown": false }, "formatter": { "enabled": true, - "indentStyle": "tab" + "indentStyle": "tab", + "lineWidth": 80 }, "linter": { "enabled": true, diff --git a/postcss.config.js b/postcss.config.mjs similarity index 72% rename from postcss.config.js rename to postcss.config.mjs index c148a5e..c42f31c 100644 --- a/postcss.config.js +++ b/postcss.config.mjs @@ -4,4 +4,4 @@ const config = { }, }; -module.exports = config; +export default config; diff --git a/src/app/api/erettsegi/route.ts b/src/app/api/erettsegi/route.ts new file mode 100644 index 0000000..629da99 --- /dev/null +++ b/src/app/api/erettsegi/route.ts @@ -0,0 +1,120 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { subjects } from "@/utils/subjects"; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = req.nextUrl; + const vizsgatargy = searchParams.get("vizsgatargy"); + const ev = searchParams.get("ev"); + const idoszak = searchParams.get("idoszak"); + const szint = searchParams.get("szint"); + + if (!vizsgatargy || !ev || !idoszak || !szint) { + const missingParams = []; + if (!vizsgatargy) missingParams.push("vizsgatargy"); + if (!ev) missingParams.push("ev"); + if (!idoszak) missingParams.push("idoszak"); + if (!szint) missingParams.push("szint"); + + return NextResponse.json( + { error: `Hiányzó paraméterek: ${missingParams.join(", ")}` }, + { status: 400 }, + ); + } + + if (parseInt(ev, 10) <= 2012) { + return NextResponse.json({ error: "Érvénytelen év" }, { status: 400 }); + } + + const validSubjects = subjects.map((subject) => subject.value); + if (!validSubjects.includes(vizsgatargy)) { + return NextResponse.json( + { error: "Érvénytelen vizsgatárgy" }, + { status: 400 }, + ); + } + + let honap: string; + switch (idoszak) { + case "osz": + honap = "okt"; + break; + case "tavasz": + honap = "maj"; + break; + default: + return NextResponse.json( + { error: "Érvénytelen időszak" }, + { status: 400 }, + ); + } + + let prefix: string; + switch (szint) { + case "emelt": + prefix = `e_${vizsgatargy}`; + break; + case "kozep": + prefix = `k_${vizsgatargy}`; + break; + default: + return NextResponse.json( + { error: "Érvénytelen szint" }, + { status: 400 }, + ); + } + + const protocol = + req.headers.get("x-forwarded-proto") === "https" ? "https" : "http"; + const host = req.headers.get("host"); + const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`; + const proxiedUrl = `${protocol}://${host}/api/proxy?link=${encodeURI(baseUrl)}`; + + const shortev = ev.slice(-2); + const feladat = "fl"; + const utmutato = "ut"; + const forras = "for"; + const megoldas = "meg"; + + let flPdfUrl: string | undefined, + utPdfUrl: string | undefined, + flZipUrl: string | undefined, + utZipUrl: string | undefined, + flMp3Url: string | undefined; + + switch (vizsgatargy) { + case "inf": + case "infoism": + case "digkult": + flZipUrl = `${baseUrl}${prefix}${forras}_${shortev}${honap}_${feladat}.zip`; + flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`; + utZipUrl = `${baseUrl}${prefix}${megoldas}_${shortev}${honap}_${utmutato}.zip`; + utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`; + break; + case "angol": + case "nemet": + flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`; + utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`; + flMp3Url = `${baseUrl}${prefix}_${shortev}${honap}_${feladat}.mp3`; + break; + default: + flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`; + utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`; + break; + } + + return NextResponse.json( + { flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url }, + { + status: 200, + headers: { "Cache-Control": "s-maxage=31536000" }, + }, + ); + } catch (e: unknown) { + console.error("An unexpected error occurred in the API route:", e); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts new file mode 100644 index 0000000..e73df09 --- /dev/null +++ b/src/app/api/proxy/route.ts @@ -0,0 +1,79 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { Agent, fetch } from "undici"; + +const insecureAgent = new Agent({ + connect: { + rejectUnauthorized: false, + }, +}); + +export async function GET(req: NextRequest) { + try { + const { searchParams } = req.nextUrl; + const link = searchParams.get("link"); + + if (!link) { + return NextResponse.json( + { error: "Hiányzó paraméter: link" }, + { status: 400 }, + ); + } + + let url: URL; + try { + url = new URL(link); + } catch (_error) { + return NextResponse.json( + { error: "Érvénytelen link formátum" }, + { status: 400 }, + ); + } + + if (url.hostname !== "dload-oktatas.educatio.hu") { + return NextResponse.json({ error: "Érvénytelen link" }, { status: 400 }); + } + + const externalResponse = await fetch(link, { + method: "GET", + dispatcher: insecureAgent, + }); + + if (!externalResponse.ok) { + return NextResponse.json( + { error: "Hiba történt a külső forrás lekérése során." }, + { status: externalResponse.status }, + ); + } + + const body = await externalResponse.arrayBuffer(); + + const contentType = externalResponse.headers.get("content-type"); + const headers = new Headers(); + headers.set("Cache-Control", "s-maxage=31536000, stale-while-revalidate"); + + if (contentType) { + headers.set("Content-Type", contentType); + if (contentType === "application/pdf") { + const filename = url.pathname.split("/").pop() ?? "document.pdf"; + headers.set("Content-Disposition", `inline; filename="${filename}"`); + } + } + + return new Response(body, { + status: 200, + headers: headers, + }); + } catch (e: unknown) { + console.error("Proxy Error:", e); + if (e instanceof Error) { + return NextResponse.json( + { error: "Internal Server Error", message: e.message }, + { status: 500 }, + ); + } + return NextResponse.json( + { error: "An unexpected internal error occurred" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/validate/route.ts b/src/app/api/validate/route.ts new file mode 100644 index 0000000..6dd450c --- /dev/null +++ b/src/app/api/validate/route.ts @@ -0,0 +1,73 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { Agent, fetch } from "undici"; + +const insecureAgent = new Agent({ + connect: { + rejectUnauthorized: false, + }, +}); + +const ALLOWED_HOSTS = [ + "localhost:3000", + "erettsegi.albert.lol", + "dload-oktatas.educatio.hu", +]; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = req.nextUrl; + const link = searchParams.get("link"); + + if (!link) { + return NextResponse.json( + { error: "Hiányzó paraméter: link" }, + { status: 400 }, + ); + } + + let url: URL; + try { + url = new URL(link); + } catch (_error) { + return NextResponse.json( + { error: "Érvénytelen link formátum" }, + { status: 400 }, + ); + } + + if (!ALLOWED_HOSTS.includes(url.host)) { + return NextResponse.json({ error: "Érvénytelen link" }, { status: 400 }); + } + + const response = await fetch(link, { + method: "HEAD", + dispatcher: insecureAgent, + }); + + return NextResponse.json({ status: response.status }, { status: 200 }); + } catch (e: unknown) { + console.error("Validation Error:", e); + + if (e instanceof Error) { + const cause = e.cause as { code?: unknown } | undefined; + return NextResponse.json( + { + error: "Internal Server Error", + message: e.message, + cause: cause?.code ? String(cause.code) : undefined, + stack: process.env.NODE_ENV === "development" ? e.stack : undefined, + }, + { status: 500 }, + ); + } + + return NextResponse.json( + { + error: "Internal Server Error", + message: "An unexpected error occurred", + details: String(e), + }, + { status: 500 }, + ); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..658f29e --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,52 @@ +import type { Metadata, Viewport } from "next"; +import Script from "next/script"; +import { Providers } from "@/app/providers"; +import { Inter } from "next/font/google"; +import "@/styles/globals.css"; + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", +}); + +export const metadata: Metadata = { + metadataBase: new URL("https://erettsegi.albert.lol"), + title: "Érettségi kereső", + description: "Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫", + icons: { + icon: "/favicon.ico", + }, + openGraph: { + title: "Érettségi kereső", + description: + "Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫", + url: "https://erettsegi.albert.lol", + images: "/logo.png", + }, +}; + +export const viewport: Viewport = { + themeColor: "#121212", + width: "device-width", + initialScale: 1, + maximumScale: 1, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + - - - - - - -
- - - - ); -} diff --git a/src/pages/api/erettsegi.ts b/src/pages/api/erettsegi.ts deleted file mode 100644 index 7dd35d8..0000000 --- a/src/pages/api/erettsegi.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { subjects } from "@/utils/subjects"; - -export default function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const { vizsgatargy, ev, idoszak, szint } = req.query as { - vizsgatargy: string; - ev: string; - idoszak: string; - szint: string; - }; - - const secure = req.headers["x-forwarded-proto"] === "https"; - const protocol = secure ? "https" : "http"; - const address = req.headers.host; - - const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`; - const proxiedUrl = `${protocol}://${address}/api/proxy?link=${encodeURI( - baseUrl, - )}`; - - const missingParams = []; - if (!ev) missingParams.push("ev"); - if (!szint) missingParams.push("szint"); - if (!idoszak) missingParams.push("idoszak"); - if (!vizsgatargy) missingParams.push("vizsgatargy"); - - if (missingParams.length > 0) { - return res - .status(400) - .json({ error: `Hiányzó paraméterek: ${missingParams.join(", ")}` }); - } - - if (ev <= "2012") { - return res.status(400).json({ error: "Érvénytelen év" }); - } - - const validSubjects = subjects.map((subject) => subject.value); - if (!vizsgatargy || !validSubjects.includes(vizsgatargy)) { - return res.status(400).json({ error: "Érvénytelen vizsgatárgy" }); - } - - let honap: string; - switch (idoszak) { - case "osz": - honap = "okt"; - break; - case "tavasz": - honap = "maj"; - break; - default: - return res.status(400).json({ error: "Érvénytelen időszak" }); - } - - let prefix: string; - switch (szint) { - case "emelt": - prefix = `e_${vizsgatargy}`; - break; - case "kozep": - prefix = `k_${vizsgatargy}`; - break; - default: - return res.status(400).json({ error: "Érvénytelen szint" }); - } - - const feladat = "fl"; - const utmutato = "ut"; - const forras = "for"; - const megoldas = "meg"; - const shortev = ev.slice(-2); - - let flPdfUrl: string | undefined, - utPdfUrl: string | undefined, - flZipUrl: string | undefined, - utZipUrl: string | undefined, - flMp3Url: string | undefined; - switch (vizsgatargy) { - case "inf": - case "infoism": - case "digkult": - flZipUrl = `${baseUrl}${prefix}${forras}_${shortev}${honap}_${feladat}.zip`; - flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`; - utZipUrl = `${baseUrl}${prefix}${megoldas}_${shortev}${honap}_${utmutato}.zip`; - utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`; - break; - case "angol": - case "nemet": - flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`; - utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`; - flMp3Url = `${baseUrl}${prefix}_${shortev}${honap}_${feladat}.mp3`; - break; - default: - flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`; - utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`; - break; - } - - res.setHeader("Cache-Control", "s-maxage=31536000"); - res.status(200).json({ flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url }); - } catch (e: unknown) { - if (e instanceof Error) { - let causeCode: string | undefined; - if (e.cause && typeof e.cause === "object" && "code" in e.cause) { - causeCode = String((e.cause as { code: unknown }).code); - } - - res.status(500).json({ - error: "Internal Server Error", - message: e.message, - cause: causeCode, - stack: process.env.NODE_ENV === "development" ? e.stack : undefined, - }); - } else { - res.status(500).json({ - error: "Internal Server Error", - message: "An unexpected error occurred", - details: String(e), - }); - } - } -} diff --git a/src/pages/api/proxy.ts b/src/pages/api/proxy.ts deleted file mode 100644 index f7c5bfe..0000000 --- a/src/pages/api/proxy.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { Agent, fetch } from "undici"; - -const insecureAgent = new Agent({ - connect: { - rejectUnauthorized: false, - }, -}); - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { link } = req.query as { link: string }; - let missingParam: string | null = null; - if (!link) { - missingParam = "link"; - } - - if (missingParam) { - return res - .status(400) - .json({ error: `Hiányzó paraméter: ${missingParam}` }); - } - - const domain = link.split("/")[2]; - if (domain !== "dload-oktatas.educatio.hu") { - return res.status(400).json({ error: "Érvénytelen link" }); - } - - try { - res.setHeader("Cache-Control", "s-maxage=31536000"); - - const response = await fetch(link, { - method: "GET", - dispatcher: insecureAgent, - }); - - const contentType = response.headers.get("content-type"); - - if (contentType === "application/pdf") { - const filename = link.split("/").pop() ?? "document.pdf"; - res.setHeader("Content-Type", contentType); - res.setHeader("Content-Disposition", `inline; filename="${filename}"`); - } - - if (response.ok) { - const arrayBuffer: ArrayBuffer = await response.arrayBuffer(); - const buffer: Buffer = Buffer.from(arrayBuffer); - res.send(buffer); - } else { - res - .status(response.status) - .json({ error: "Hiba történt a lekérés során." }); - } - } catch (e: unknown) { - if (e instanceof Error) { - let causeCode: string | undefined; - if (e.cause && typeof e.cause === "object" && "code" in e.cause) { - causeCode = String((e.cause as { code: unknown }).code); - } - - res.status(500).json({ - error: "Internal Server Error", - message: e.message, - cause: causeCode, - stack: process.env.NODE_ENV === "development" ? e.stack : undefined, - }); - } else { - res.status(500).json({ - error: "Internal Server Error", - message: "An unexpected error occurred", - details: String(e), - }); - } - } -} diff --git a/src/pages/api/validate.ts b/src/pages/api/validate.ts deleted file mode 100644 index 41b98bf..0000000 --- a/src/pages/api/validate.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { Agent, fetch } from "undici"; - -const insecureAgent = new Agent({ - connect: { - rejectUnauthorized: false, - }, -}); - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { link } = req.query as { link: string }; - let missingParam: string | null = null; - - if (!link) { - missingParam = "link"; - } - - if (missingParam) { - return res - .status(400) - .json({ error: `Hiányzó paraméter: ${missingParam}` }); - } - - const domain = link.split("/")[2]; - if ( - domain !== "localhost:3000" && - domain !== "erettsegi.albert.lol" && - domain !== "dload-oktatas.educatio.hu" - ) { - return res.status(400).json({ error: "Érvénytelen link" }); - } - - try { - const { protocol, host } = new URL(link); - if (!protocol || !host) { - return res.status(400).json({ error: "Érvénytelen link" }); - } - - const response = await fetch(link, { - method: "HEAD", - dispatcher: insecureAgent, - }); - - const status = response.status; - res.status(200).json({ status }); - } catch (e: unknown) { - if (e instanceof Error) { - let causeCode: string | undefined; - if (e.cause && typeof e.cause === "object" && "code" in e.cause) { - causeCode = String((e.cause as { code: unknown }).code); - } - - res.status(500).json({ - error: "Internal Server Error", - message: e.message, - cause: causeCode, - stack: process.env.NODE_ENV === "development" ? e.stack : undefined, - }); - } else { - res.status(500).json({ - error: "Internal Server Error", - message: "An unexpected error occurred", - details: String(e), - }); - } - } -} diff --git a/tsconfig.json b/tsconfig.json index 4c01a40..c284e92 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,36 @@ { "compilerOptions": { - /* Base Options: */ - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2022", + /* Basic Options */ + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "isolatedModules": true, + "skipLibCheck": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], /* Strictness */ "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, - "checkJs": true, - /* Bundled projects */ - "lib": ["dom", "dom.iterable", "ES2022"], + /* Module Resolution */ + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + + /* Code Style */ + "forceConsistentCasingInFileNames": true, "noEmit": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "preserve", - "plugins": [{ "name": "next" }], - "incremental": true, + "esModuleInterop": true, /* Path Aliases */ "baseUrl": ".", @@ -29,16 +38,6 @@ "@/*": ["./src/*"] } }, - "include": [ - ".eslintrc.cjs", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.js", - ".next/types/**/*.ts", - "next.config.mjs", - "postcss.config.mjs" - ], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }