Bump version to 1.7.8
This commit is contained in:
+21
-8
@@ -22,6 +22,8 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [editEntry, setEditEntry] = useState<LogbuchEintrag | null>(null);
|
||||
|
||||
const grafikSrc = '/api/statistik/grafik';
|
||||
|
||||
const version = packageJson.version;
|
||||
const buildDate =
|
||||
process.env.NEXT_PUBLIC_BUILD_DATE ||
|
||||
@@ -98,7 +100,10 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
||||
.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => { setActiveTab(tab); if (tab === 'eingabe') setEditEntry(null); }}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
if (tab === 'eingabe') setEditEntry(null);
|
||||
}}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab
|
||||
? 'border-[#85B7D7] text-gray-900'
|
||||
@@ -173,14 +178,22 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
||||
{/* Statistik-Tab */}
|
||||
{activeTab === 'statistik' && (
|
||||
<div className="border-2 border-gray-400 rounded-xl bg-white p-3 print:border-0 print:rounded-none print:p-0">
|
||||
<div className="flex justify-between items-center mb-2 print:hidden">
|
||||
<div className="flex justify-between items-center mb-2 print:hidden gap-2">
|
||||
<span className="text-sm font-semibold text-gray-600">Statistik (alle Kuppeln)</span>
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg"
|
||||
>
|
||||
🖨 Drucken
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => window.print()}
|
||||
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg"
|
||||
>
|
||||
🖨 Drucken
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.open(grafikSrc, '_blank')}
|
||||
className="text-sm px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg"
|
||||
>
|
||||
📊 Grafik
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Statistik />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { grafikProxy } from '../proxy';
|
||||
|
||||
function getSlugFromRequest(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
const prefix = '/api/statistik/grafik';
|
||||
const path = url.pathname;
|
||||
if (!path.startsWith(prefix)) return undefined;
|
||||
const suffix = path.slice(prefix.length);
|
||||
return suffix.split('/').filter(Boolean);
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
return grafikProxy(req, getSlugFromRequest(req));
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
return grafikProxy(req, getSlugFromRequest(req));
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const blockedHeaders = new Set([
|
||||
'x-frame-options',
|
||||
'content-security-policy',
|
||||
'content-security-policy-report-only',
|
||||
'frame-options',
|
||||
'content-encoding',
|
||||
'content-length',
|
||||
'transfer-encoding',
|
||||
]);
|
||||
|
||||
function buildProxyTarget(req: Request, slug?: string[]) {
|
||||
const upstreamUrl = process.env.STATISTIK_GRAFIK_URL;
|
||||
if (!upstreamUrl) {
|
||||
throw new Error('STATISTIK_GRAFIK_URL is not configured');
|
||||
}
|
||||
|
||||
const target = new URL(upstreamUrl);
|
||||
const pathSuffix = slug?.filter(Boolean).join('/') || '';
|
||||
if (pathSuffix) {
|
||||
target.pathname = `${target.pathname.replace(/\/$/, '')}/${pathSuffix}`;
|
||||
}
|
||||
target.search = new URL(req.url).search;
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
function buildAuthHeaders() {
|
||||
const headers: Record<string, string> = {};
|
||||
const user = process.env.STATISTIK_GRAFIK_USER;
|
||||
const pass = process.env.STATISTIK_GRAFIK_PASS;
|
||||
if (user && pass) {
|
||||
const token = Buffer.from(`${user}:${pass}`).toString('base64');
|
||||
headers.Authorization = `Basic ${token}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function rewriteHtml(body: string) {
|
||||
const proxyBase = '/api/statistik/grafik';
|
||||
const upstreamOrigin = new URL(process.env.STATISTIK_GRAFIK_URL!).origin;
|
||||
const escapedProxyBase = escapeRegExp(proxyBase);
|
||||
const proxyBasePattern = escapedProxyBase.replace(/\//g, '\\/');
|
||||
|
||||
return body
|
||||
.replace(/<meta[^>]*?(?:http-equiv|httpEquiv)\s*=\s*['"]?[^'">\s]+['"]?[^>]*>/ig, (m) => {
|
||||
if (/x-frame-options/i.test(m)) return '';
|
||||
if (/content-security-policy/i.test(m)) return '';
|
||||
if (/frame-ancestors/i.test(m)) return '';
|
||||
return m;
|
||||
})
|
||||
.replace(/<head([^>]*)>/i, `<head$1><base href="${proxyBase}/">`)
|
||||
.replace(new RegExp(`(href|src|action)=(["'])(?!${proxyBasePattern}\\/)\/`, 'gi'), `$1=$2${proxyBase}/`)
|
||||
.replace(new RegExp(`url\\((['"]?)(?!${proxyBasePattern}\\/)\/`, 'gi'), `url($1${proxyBase}/`)
|
||||
.replace(new RegExp(escapeRegExp(upstreamOrigin), 'g'), proxyBase);
|
||||
}
|
||||
|
||||
export async function grafikProxy(req: Request, slug?: string[]) {
|
||||
let targetUrl: string;
|
||||
try {
|
||||
targetUrl = buildProxyTarget(req, slug);
|
||||
} catch (error) {
|
||||
return new NextResponse((error as Error).message, { status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
req.headers.forEach((value, key) => {
|
||||
const lower = key.toLowerCase();
|
||||
if (lower === 'host' || lower === 'content-length') return;
|
||||
requestHeaders[key] = value;
|
||||
});
|
||||
Object.assign(requestHeaders, buildAuthHeaders());
|
||||
|
||||
const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await req.arrayBuffer();
|
||||
const upstream = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
headers: requestHeaders,
|
||||
body,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const bodyBuf = await upstream.arrayBuffer();
|
||||
|
||||
const outHeaders: Record<string, string> = {};
|
||||
upstream.headers.forEach((value, key) => {
|
||||
if (!blockedHeaders.has(key.toLowerCase())) {
|
||||
outHeaders[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
const contentType = upstream.headers.get('content-type') || outHeaders['content-type'] || '';
|
||||
if (contentType.toLowerCase().includes('text/html')) {
|
||||
const text = Buffer.from(bodyBuf).toString('utf8');
|
||||
const cleaned = rewriteHtml(text);
|
||||
outHeaders['content-type'] = 'text/html; charset=utf-8';
|
||||
outHeaders['x-frame-options'] = 'ALLOWALL';
|
||||
return new NextResponse(cleaned, { status: upstream.status, headers: outHeaders });
|
||||
}
|
||||
|
||||
outHeaders['x-frame-options'] = 'ALLOWALL';
|
||||
return new NextResponse(Buffer.from(bodyBuf), { status: upstream.status, headers: outHeaders });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new NextResponse(`grafikProxy error: ${message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { grafikProxy } from './proxy';
|
||||
|
||||
function getSlugFromRequest(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
const prefix = '/api/statistik/grafik';
|
||||
const path = url.pathname;
|
||||
if (!path.startsWith(prefix)) return undefined;
|
||||
const suffix = path.slice(prefix.length);
|
||||
return suffix.split('/').filter(Boolean);
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
return grafikProxy(req, getSlugFromRequest(req));
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
return grafikProxy(req, getSlugFromRequest(req));
|
||||
}
|
||||
Reference in New Issue
Block a user