From e54214a92b0e8a790881086b787ecdffcaa90d05 Mon Sep 17 00:00:00 2001 From: skidoodle Date: Sat, 6 Dec 2025 04:07:19 +0100 Subject: [PATCH] huge update (pls dont break) --- src/app/api/erettsegi/route.ts | 113 --------------------- src/app/api/proxy/route.ts | 68 ------------- src/app/api/validate/route.ts | 61 ------------ src/app/erettsegi/[...slug]/route.ts | 76 +++++++++++++++ src/app/proxy/[...slug]/route.ts | 55 +++++++++++ src/app/validate/[...slug]/route.ts | 30 ++++++ src/components/Resources.tsx | 5 +- src/utils/edu.ts | 97 +++++++++++++++++++ src/utils/fetch.ts | 33 +++---- src/utils/subjects.ts | 140 ++++++++++++++++++++++++--- 10 files changed, 401 insertions(+), 277 deletions(-) delete mode 100644 src/app/api/erettsegi/route.ts delete mode 100644 src/app/api/proxy/route.ts delete mode 100644 src/app/api/validate/route.ts create mode 100644 src/app/erettsegi/[...slug]/route.ts create mode 100644 src/app/proxy/[...slug]/route.ts create mode 100644 src/app/validate/[...slug]/route.ts create mode 100644 src/utils/edu.ts diff --git a/src/app/api/erettsegi/route.ts b/src/app/api/erettsegi/route.ts deleted file mode 100644 index fd94524..0000000 --- a/src/app/api/erettsegi/route.ts +++ /dev/null @@ -1,113 +0,0 @@ -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 currentYear = new Date().getFullYear(); - return NextResponse.json({ - parameters: { - vizsgatargy: { - type: "string", - required: true, - values: subjects.map((s) => ({ - value: s.value, - label: s.label, - })), - }, - ev: { - type: "integer", - required: true, - values: `2013 - ${currentYear}`, - }, - idoszak: { - type: "string", - required: true, - values: ["tavasz", "osz"], - }, - szint: { - type: "string", - required: true, - values: ["kozep", "emelt"], - }, - }, - example: - "/api/erettsegi?vizsgatargy=mat&ev=2023&idoszak=tavasz&szint=kozep", - }); - } - - if (!vizsgatargy || !ev || !idoszak || !szint) { - const missingParams = [ - !vizsgatargy && "vizsgatargy", - !ev && "ev", - !idoszak && "idoszak", - !szint && "szint", - ].filter(Boolean); - - 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 }, - ); - } - - const honap = idoszak === "osz" ? "okt" : "maj"; - const prefix = szint === "emelt" ? `e_${vizsgatargy}` : `k_${vizsgatargy}`; - - 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"; - - const urls = { - flPdfUrl: `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`, - utPdfUrl: `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`, - flZipUrl: ["inf", "infoism", "digkult"].includes(vizsgatargy) - ? `${baseUrl}${prefix}${forras}_${shortev}${honap}_${feladat}.zip` - : undefined, - utZipUrl: ["inf", "infoism", "digkult"].includes(vizsgatargy) - ? `${baseUrl}${prefix}${megoldas}_${shortev}${honap}_${utmutato}.zip` - : undefined, - flMp3Url: ["angol", "nemet"].includes(vizsgatargy) - ? `${baseUrl}${prefix}_${shortev}${honap}_${feladat}.mp3` - : undefined, - }; - - return NextResponse.json(urls, { - 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 deleted file mode 100644 index 0b18c27..0000000 --- a/src/app/api/proxy/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; - -export async function GET(req: NextRequest) { - try { - const { searchParams } = req.nextUrl; - const link = searchParams.get("link"); - - if (!link) { - return NextResponse.json({ - parameters: { - link: { - type: "string", - required: true, - }, - }, - example: `/api/proxy?link=https://dload-oktatas.educatio.hu/erettsegi/feladatok_2023tavasz_kozep/k_mat_23maj_fl.pdf`, - }); - } - - let url: URL; - try { - url = new URL(link); - } catch { - 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", - }); - - 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 headers = new Headers(externalResponse.headers); - headers.set("Cache-Control", "s-maxage=31536000, stale-while-revalidate"); - - const contentType = externalResponse.headers.get("content-type"); - if (contentType === "application/pdf") { - const filename = url.pathname.split("/").pop() ?? "document.pdf"; - headers.set("Content-Disposition", `inline; filename="${filename}"`); - } - - return new NextResponse(externalResponse.body, { - status: 200, - headers, - }); - - } catch (e: unknown) { - console.error("Proxy Error:", e); - const errorMessage = - e instanceof Error ? e.message : "An unexpected internal error occurred"; - return NextResponse.json( - { error: "Internal Server Error", message: errorMessage }, - { status: 500 }, - ); - } -} diff --git a/src/app/api/validate/route.ts b/src/app/api/validate/route.ts deleted file mode 100644 index c332aa2..0000000 --- a/src/app/api/validate/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; - -const ALLOWED_HOSTS = [ - "localhost:3000", - "erettsegi.albert.lol", - "dload-oktatas.educatio.hu", -]; - -export async function GET(req: NextRequest) { - try { - const { searchParams } = req.nextUrl; - let link = searchParams.get("link"); - - if (!link) { - return NextResponse.json({ - parameters: { - link: { - type: "string", - required: true, - allowed_hosts: ALLOWED_HOSTS, - }, - }, - example: `/api/validate?link=https://dload-oktatas.educatio.hu/erettsegi/feladatok_2023tavasz_kozep/k_mat_23maj_fl.pdf`, - }); - } - - let url: URL; - try { - url = new URL(link); - } catch { - return NextResponse.json( - { error: "Érvénytelen link formátum" }, - { status: 400 }, - ); - } - - if (url.pathname === "/api/proxy" && url.searchParams.has("link")) { - const realTarget = url.searchParams.get("link"); - if (realTarget) { - try { - const realUrl = new URL(realTarget); - link = realTarget; - url = realUrl; - } catch {} - } - } - - if (!ALLOWED_HOSTS.includes(url.host)) { - return NextResponse.json({ error: "Érvénytelen link" }, { status: 400 }); - } - - const response = await fetch(link, { - method: "HEAD", - }); - - return NextResponse.json({ status: response.status }, { status: 200 }); - } catch (error) { - console.error("Validation Error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } -} diff --git a/src/app/erettsegi/[...slug]/route.ts b/src/app/erettsegi/[...slug]/route.ts new file mode 100644 index 0000000..ce4d279 --- /dev/null +++ b/src/app/erettsegi/[...slug]/route.ts @@ -0,0 +1,76 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { parseSegments } from "@/utils/edu"; +import { subjects } from "@/utils/subjects"; + +const LABELS = { + periods: { tavasz: "Tavasz", osz: "Ősz" } as Record, + levels: { kozep: "Közép", emelt: "Emelt" } as Record, +}; + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> }, +) { + try { + const { slug } = await params; + const query = parseSegments(slug); + + const { subject, year, period, level } = query; + + const isComplete = subject && year && period && level; + + if (isComplete) { + const protocol = + req.headers.get("x-forwarded-proto") === "https" ? "https" : "http"; + const host = req.headers.get("host"); + + const proxyBase = `${protocol}://${host}/proxy/${subject.slug}/${year}/${period}/${level}`; + const makeLink = (type: string) => `${proxyBase}/${type}`; + + return NextResponse.json({ + found: true, + meta: { + subject: subject.label, + year, + period: LABELS.periods[period] ?? period, + level: LABELS.levels[level] ?? level, + }, + links: { + flPdfUrl: makeLink("feladat"), + utPdfUrl: makeLink("utmutato"), + flZipUrl: ["inf", "infoism", "digkult"].includes(subject.value) + ? makeLink("forras") + : undefined, + utZipUrl: ["inf", "infoism", "digkult"].includes(subject.value) + ? makeLink("megoldas") + : undefined, + flMp3Url: ["angol", "nemet"].includes(subject.value) + ? makeLink("hang") + : undefined, + }, + }); + } + + const suggestions = { + found: false, + message: "Missing parameters", + currentSelection: { + subject: subject?.label || null, + year: year || null, + period: period ? (LABELS.periods[period] ?? period) : null, + level: level ? (LABELS.levels[level] ?? level) : null, + }, + options: { + subjects: !subject ? subjects.map((s) => s.slug) : undefined, + years: !year ? "2013 - 2024" : undefined, + periods: !period ? Object.values(LABELS.periods) : undefined, + levels: !level ? Object.values(LABELS.levels) : undefined, + }, + }; + + return NextResponse.json(suggestions); + } catch (e) { + console.error(e); + return NextResponse.json({ error: "Server Error" }, { status: 500 }); + } +} diff --git a/src/app/proxy/[...slug]/route.ts b/src/app/proxy/[...slug]/route.ts new file mode 100644 index 0000000..dcad16e --- /dev/null +++ b/src/app/proxy/[...slug]/route.ts @@ -0,0 +1,55 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { parseSegments, getExternalUrl, type ResourceSlug } from "@/utils/edu"; + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> }, +) { + try { + const { slug } = await params; + + const { subject, year, period, level, resourceType } = parseSegments(slug); + + if (!subject || !year || !period || !level || !resourceType) { + return NextResponse.json( + { error: "Invalid download link. Missing parameters." }, + { status: 400 }, + ); + } + + const targetUrl = getExternalUrl( + subject.value, + year, + period, + level, + resourceType as ResourceSlug, + ); + + if (!targetUrl) { + return NextResponse.json( + { error: "This file type does not exist for this subject." }, + { status: 404 }, + ); + } + + const externalResponse = await fetch(targetUrl, { method: "GET" }); + + if (!externalResponse.ok) { + return NextResponse.json( + { error: "File not found on external server." }, + { status: 404 }, + ); + } + + const headers = new Headers(externalResponse.headers); + headers.set("Cache-Control", "s-maxage=31536000, stale-while-revalidate"); + + const filename = targetUrl.split("/").pop() ?? "erettsegi_file"; + headers.set("Content-Disposition", `inline; filename="${filename}"`); + + return new NextResponse(externalResponse.body, { status: 200, headers }); + } catch (e) { + console.error("Proxy Error", e); + return NextResponse.json({ error: "Internal Error" }, { status: 500 }); + } +} diff --git a/src/app/validate/[...slug]/route.ts b/src/app/validate/[...slug]/route.ts new file mode 100644 index 0000000..6cb775d --- /dev/null +++ b/src/app/validate/[...slug]/route.ts @@ -0,0 +1,30 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { parseSegments, getExternalUrl, type ResourceSlug } from "@/utils/edu"; + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ slug: string[] }> }, +) { + try { + const { slug } = await params; + const { subject, year, period, level, resourceType } = parseSegments(slug); + + if (!subject || !year || !period || !level || !resourceType) { + return NextResponse.json({ status: 400 }); + } + + const targetUrl = getExternalUrl( + subject.value, + year, + period, + level, + resourceType as ResourceSlug, + ); + if (!targetUrl) return NextResponse.json({ status: 404 }); + + const res = await fetch(targetUrl, { method: "HEAD" }); + return NextResponse.json({ status: res.status }); + } catch { + return NextResponse.json({ status: 500 }); + } +} diff --git a/src/components/Resources.tsx b/src/components/Resources.tsx index 55c548a..1a3a9b6 100644 --- a/src/components/Resources.tsx +++ b/src/components/Resources.tsx @@ -18,7 +18,10 @@ const ResourceComponent = ({ label, link }: ResourceProps) => { if (link) { try { setIsLoading(true); - const response = await fetch(`/api/validate?link=${encodeURI(link)}`); + + const validateUrl = link.replace("/proxy/", "/validate/"); + + const response = await fetch(validateUrl); const data = (await response.json()) as { status: number }; setStatus(data.status); } catch { diff --git a/src/utils/edu.ts b/src/utils/edu.ts new file mode 100644 index 0000000..20815f8 --- /dev/null +++ b/src/utils/edu.ts @@ -0,0 +1,97 @@ +import { + subjects, + resourceMap, + type Subject, + type ResourceSlug, +} from "@/utils/subjects"; + +export type { ResourceSlug }; + +export type Period = "tavasz" | "osz"; +export type Level = "kozep" | "emelt"; + +interface ParsedQuery { + subject?: Subject; + year?: string; + period?: Period; + level?: Level; + resourceType?: ResourceSlug; +} + +export function parseSegments(segments: string[]): ParsedQuery { + const result: ParsedQuery = {}; + + for (const segment of segments) { + const lower = segment.toLowerCase(); + + if (/^20[1-2][0-9]$/.test(lower)) { + result.year = lower; + continue; + } + + if (lower === "kozep" || lower === "emelt") { + result.level = lower as Level; + continue; + } + + if (lower === "tavasz" || lower === "osz") { + result.period = lower as Period; + continue; + } + + if (Object.keys(resourceMap).includes(lower)) { + result.resourceType = lower as ResourceSlug; + continue; + } + + const foundSubject = subjects.find( + (s) => s.slug === lower || s.aliases.includes(lower), + ); + if (foundSubject) { + result.subject = foundSubject; + } + } + + return result; +} + +export function getExternalUrl( + subjectCode: string, + year: string, + period: string, + level: string, + typeSlug: ResourceSlug, +): string | null { + const typeCode = resourceMap[typeSlug]; + if (!typeCode) return null; + + const month = period === "osz" ? "okt" : "maj"; + const prefix = level === "emelt" ? `e_${subjectCode}` : `k_${subjectCode}`; + const shortYear = year.slice(-2); + + const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${year}${period}_${level}/`; + let filename = ""; + + switch (typeCode) { + case "fl": + filename = `${prefix}_${shortYear}${month}_fl.pdf`; + break; + case "ut": + filename = `${prefix}_${shortYear}${month}_ut.pdf`; + break; + case "for": + if (!["inf", "infoism", "digkult"].includes(subjectCode)) return null; + filename = `${prefix}for_${shortYear}${month}_fl.zip`; + break; + case "meg": + if (!["inf", "infoism", "digkult"].includes(subjectCode)) return null; + filename = `${prefix}meg_${shortYear}${month}_ut.zip`; + break; + case "hang": + if (!["angol", "nemet"].includes(subjectCode)) return null; + filename = `${prefix}_${shortYear}${month}_fl.mp3`; + break; + } + + return filename ? `${baseUrl}${filename}` : null; +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 5e2a004..ab8eb80 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -15,36 +15,31 @@ export const fetchData = async ( dispatch: Dispatch, ) => { try { - const url = `/api/erettsegi?vizsgatargy=${selectedSubject}&ev=${selectedYear}&idoszak=${selectedPeriod}&szint=${selectedLevel}`; + const url = `/erettsegi/${selectedSubject}/${selectedYear}/${selectedPeriod}/${selectedLevel}`; const response = await fetch(url); + const data = await response.json(); - if (response.ok) { - const data = (await response.json()) as { - flZipUrl?: string; - utZipUrl?: string; - flPdfUrl: string; - utPdfUrl: string; - flMp3Url?: string; - }; + if (response.ok && data.found) { + const links = data.links; - if (data.utZipUrl && data.flZipUrl) { - dispatch({ type: "SET_FL_ZIP_LINK", payload: data.flZipUrl }); - dispatch({ type: "SET_UT_ZIP_LINK", payload: data.utZipUrl }); + if (links.utZipUrl && links.flZipUrl) { + dispatch({ type: "SET_FL_ZIP_LINK", payload: links.flZipUrl }); + dispatch({ type: "SET_UT_ZIP_LINK", payload: links.utZipUrl }); } - if (data.utPdfUrl && data.flPdfUrl) { - dispatch({ type: "SET_FL_PDF_LINK", payload: data.flPdfUrl }); - dispatch({ type: "SET_UT_PDF_LINK", payload: data.utPdfUrl }); + if (links.utPdfUrl && links.flPdfUrl) { + dispatch({ type: "SET_FL_PDF_LINK", payload: links.flPdfUrl }); + dispatch({ type: "SET_UT_PDF_LINK", payload: links.utPdfUrl }); } - if (data.flMp3Url) { - dispatch({ type: "SET_FL_MP3_LINK", payload: data.flMp3Url }); + if (links.flMp3Url) { + dispatch({ type: "SET_FL_MP3_LINK", payload: links.flMp3Url }); } } else { - console.error("Hiba történt az API hívás során."); + console.log("Incomplete selection or not found:", data); } } catch (error) { - console.error("Hiba történt az API hívás során.", error); + console.error("API Error", error); } }; diff --git a/src/utils/subjects.ts b/src/utils/subjects.ts index 43fc57f..8956300 100644 --- a/src/utils/subjects.ts +++ b/src/utils/subjects.ts @@ -1,16 +1,126 @@ -export const subjects = [ - { value: "magyir", label: "Magyar nyelv és irodalom" }, - { value: "mat", label: "Matematika" }, - { value: "tort", label: "Történelem" }, - { value: "angol", label: "Angol nyelv" }, - { value: "nemet", label: "Német nyelv" }, - { value: "inf", label: "Informatika" }, - { value: "digkult", label: "Digitális kultúra" }, - { value: "bio", label: "Biológia" }, - { value: "infoism", label: "Informatikai ismeretek" }, - { value: "ker", label: "Kereskedelmi ismeretek" }, - { value: "kozg", label: "Közgazdasági ismeretek" }, - { value: "kem", label: "Kémia" }, - { value: "fldr", label: "Földrajz" }, - { value: "fiz", label: "Fizika" }, +export interface Subject { + value: string; + slug: string; + label: string; + aliases: string[]; +} + +export const subjects: Subject[] = [ + { + value: "magyir", + slug: "magyar-irodalom", + label: "Magyar nyelv és irodalom", + aliases: ["magyar", "magyir"], + }, + { + value: "mat", + slug: "matematika", + label: "Matematika", + aliases: ["mat", "matek"], + }, + { + value: "tort", + slug: "tortenelem", + label: "Történelem", + aliases: ["tort", "tori"], + }, + { value: "angol", slug: "angol", label: "Angol nyelv", aliases: ["angol"] }, + { value: "nemet", slug: "nemet", label: "Német nyelv", aliases: ["nemet"] }, + { + value: "inf", + slug: "informatika", + label: "Informatika", + aliases: ["inf", "info"], + }, + { + value: "digkult", + slug: "digitalis-kultura", + label: "Digitális kultúra", + aliases: ["digkult", "dk"], + }, + { + value: "bio", + slug: "biologia", + label: "Biológia", + aliases: ["bio", "biosz"], + }, + { + value: "infoism", + slug: "informatikai-ismeretek", + label: "Informatikai ismeretek", + aliases: ["infoism"], + }, + { + value: "ker", + slug: "kereskedelem", + label: "Kereskedelmi ismeretek", + aliases: ["ker"], + }, + { + value: "kozg", + slug: "kozgazdasag", + label: "Közgazdasági ismeretek", + aliases: ["kozg", "kozgaz"], + }, + { value: "kem", slug: "kemia", label: "Kémia", aliases: ["kem"] }, + { + value: "fldr", + slug: "foldrajz", + label: "Földrajz", + aliases: ["fldr", "foldrajz", "foci"], + }, + { value: "fiz", slug: "fizika", label: "Fizika", aliases: ["fiz"] }, ]; + +export interface Period extends Subject { + value: string; + slug: string; + label: string; + aliases: string[]; +} + +export const periods: Period[] = [ + { + value: "tavasz", + slug: "tavasz", + label: "Tavasz", + aliases: ["tavasz", "tav", "tavaszi"], + }, + { value: "osz", slug: "osz", label: "Ősz", aliases: ["ősz", "osz", "oszi"] }, +]; + +export type PeriodValue = "tavasz" | "osz"; + +export interface Level { + value: string; + slug: string; + label: string; + aliases: string[]; +} + +export const levels: Level[] = [ + { + value: "kozep", + slug: "kozep", + label: "Közép", + aliases: ["kozep", "kozepszint", "kozep szint"], + }, + { + value: "emelt", + slug: "emelt", + label: "Emelt", + aliases: ["emelt", "emelt szint"], + }, +]; + +export type LevelValue = "kozep" | "emelt"; + +export const resourceMap = { + feladat: "fl", + utmutato: "ut", + forras: "for", + megoldas: "meg", + hang: "hang", +} as const; + +export type ResourceSlug = keyof typeof resourceMap;