huge update (pls dont break)

This commit is contained in:
2025-12-06 04:07:19 +01:00
parent b76a113c4d
commit e54214a92b
10 changed files with 401 additions and 277 deletions
-113
View File
@@ -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 },
);
}
}
-68
View File
@@ -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 },
);
}
}
-61
View File
@@ -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 });
}
}
+76
View File
@@ -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<string, string>,
levels: { kozep: "Közép", emelt: "Emelt" } as Record<string, string>,
};
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 });
}
}
+55
View File
@@ -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 });
}
}
+30
View File
@@ -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 });
}
}
+4 -1
View File
@@ -18,7 +18,10 @@ const ResourceComponent = ({ label, link }: ResourceProps) => {
if (link) { if (link) {
try { try {
setIsLoading(true); 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 }; const data = (await response.json()) as { status: number };
setStatus(data.status); setStatus(data.status);
} catch { } catch {
+97
View File
@@ -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;
}
+14 -19
View File
@@ -15,36 +15,31 @@ export const fetchData = async (
dispatch: Dispatch<Action>, dispatch: Dispatch<Action>,
) => { ) => {
try { 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 response = await fetch(url);
const data = await response.json();
if (response.ok) { if (response.ok && data.found) {
const data = (await response.json()) as { const links = data.links;
flZipUrl?: string;
utZipUrl?: string;
flPdfUrl: string;
utPdfUrl: string;
flMp3Url?: string;
};
if (data.utZipUrl && data.flZipUrl) { if (links.utZipUrl && links.flZipUrl) {
dispatch({ type: "SET_FL_ZIP_LINK", payload: data.flZipUrl }); dispatch({ type: "SET_FL_ZIP_LINK", payload: links.flZipUrl });
dispatch({ type: "SET_UT_ZIP_LINK", payload: data.utZipUrl }); dispatch({ type: "SET_UT_ZIP_LINK", payload: links.utZipUrl });
} }
if (data.utPdfUrl && data.flPdfUrl) { if (links.utPdfUrl && links.flPdfUrl) {
dispatch({ type: "SET_FL_PDF_LINK", payload: data.flPdfUrl }); dispatch({ type: "SET_FL_PDF_LINK", payload: links.flPdfUrl });
dispatch({ type: "SET_UT_PDF_LINK", payload: data.utPdfUrl }); dispatch({ type: "SET_UT_PDF_LINK", payload: links.utPdfUrl });
} }
if (data.flMp3Url) { if (links.flMp3Url) {
dispatch({ type: "SET_FL_MP3_LINK", payload: data.flMp3Url }); dispatch({ type: "SET_FL_MP3_LINK", payload: links.flMp3Url });
} }
} else { } else {
console.error("Hiba történt az API hívás során."); console.log("Incomplete selection or not found:", data);
} }
} catch (error) { } catch (error) {
console.error("Hiba történt az API hívás során.", error); console.error("API Error", error);
} }
}; };
+125 -15
View File
@@ -1,16 +1,126 @@
export const subjects = [ export interface Subject {
{ value: "magyir", label: "Magyar nyelv és irodalom" }, value: string;
{ value: "mat", label: "Matematika" }, slug: string;
{ value: "tort", label: "Történelem" }, label: string;
{ value: "angol", label: "Angol nyelv" }, aliases: string[];
{ value: "nemet", label: "Német nyelv" }, }
{ value: "inf", label: "Informatika" },
{ value: "digkult", label: "Digitális kultúra" }, export const subjects: Subject[] = [
{ value: "bio", label: "Biológia" }, {
{ value: "infoism", label: "Informatikai ismeretek" }, value: "magyir",
{ value: "ker", label: "Kereskedelmi ismeretek" }, slug: "magyar-irodalom",
{ value: "kozg", label: "Közgazdasági ismeretek" }, label: "Magyar nyelv és irodalom",
{ value: "kem", label: "Kémia" }, aliases: ["magyar", "magyir"],
{ value: "fldr", label: "Földrajz" }, },
{ value: "fiz", label: "Fizika" }, {
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;