feat(logging): add HTTP page fetch logging and enhance click-TT proxy functionality
- Introduced a new logging mechanism for HTTP requests to click-TT/HTTV pages, improving traceability and debugging capabilities. - Implemented a proxy endpoint for iframe embedding, allowing direct HTML retrieval with enhanced error handling and validation for input URLs. - Updated the frontend to include a new navigation link for the click-TT feature, accessible to admin users. - Added a new route for the click-TT view in the router configuration.
This commit is contained in:
113
backend/CLICKTT_HTTV_README.md
Normal file
113
backend/CLICKTT_HTTV_README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# HTTV / click-TT HTTP-Seiten – Integration & Logging
|
||||
|
||||
Dieses Modul ermöglicht das Testen und Logging von HTTP-Aufrufen an die click-TT-Seiten verschiedener Tischtennis-Verbände (HTTV, RTTV, WTTV etc.).
|
||||
|
||||
## Zweck
|
||||
|
||||
- **Logging**: Jeder Aufruf wird in `http_page_fetch_log` protokolliert (URL, HTTP-Status, Response-Snippet, Fehler).
|
||||
- **Strukturanalyse**: Die Logs helfen zu verstehen, wie die Seiten je nach Verband und Saison aufgebaut sind.
|
||||
- **URL-Varianten**: Links können je nach Verein, Saison und Verband unterschiedlich sein.
|
||||
|
||||
## Verband → Domain
|
||||
|
||||
| Verband | Domain |
|
||||
|---------|--------|
|
||||
| HeTTV / HTTV | httv.click-tt.de |
|
||||
| RTTV | rttv.click-tt.de |
|
||||
| WTTV | wttv.click-tt.de |
|
||||
| TTVNw | ttvnw.click-tt.de |
|
||||
| BTTV | battv.click-tt.de |
|
||||
|
||||
## URL-Struktur (httv.click-tt.de)
|
||||
|
||||
### leaguePage – Ligenübersicht
|
||||
|
||||
```
|
||||
https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/leaguePage?championship=HTTV+25%2F26
|
||||
```
|
||||
|
||||
- `championship`: Saison/Championship, z.B.:
|
||||
- `HTTV 25/26` – Haupt-HTTV-Saison
|
||||
- `K43 25/26` – Bezirk Frankfurt
|
||||
- `K16 25/26` – Bezirk Werra-Meißner
|
||||
- `RL-OL West 25/26` – Regional-/Oberligen West
|
||||
|
||||
### regionMeetingFilter – Regionsspielplan
|
||||
|
||||
```
|
||||
https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/regionMeetingFilter?championship=HTTV+25%2F26
|
||||
```
|
||||
|
||||
### clubInfoDisplay – Vereinsinfo
|
||||
|
||||
```
|
||||
https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/clubInfoDisplay?club=1060
|
||||
```
|
||||
|
||||
- `club`: Vereins-ID in der click-TT-Datenbank
|
||||
|
||||
## UI-Seite
|
||||
|
||||
Unter **/clicktt** (nur für Admins) gibt es eine Vue-Seite, mit der du:
|
||||
|
||||
- Seitentyp wählen (Ligenübersicht, Vereinsinfo, Regionsspielplan oder direkte URL)
|
||||
- Verband, Championship/Saison und ggf. Vereins-ID eingeben
|
||||
- Die Seite im iframe laden und direkt bedienen (klicken, navigieren)
|
||||
|
||||
Alle Aufrufe werden in `http_page_fetch_log` protokolliert.
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
Die meisten Endpunkte erfordern Authentifizierung (Token). Der **Proxy** (`/api/clicktt/proxy`) ist ohne Auth nutzbar (für iframe-Einbettung).
|
||||
|
||||
### Ligenübersicht abrufen
|
||||
|
||||
```
|
||||
GET /api/clicktt/league-page?association=HeTTV&championship=HTTV+25%2F26
|
||||
```
|
||||
|
||||
### Vereinsinfo abrufen
|
||||
|
||||
```
|
||||
GET /api/clicktt/club-info?association=HeTTV&clubId=1060
|
||||
```
|
||||
|
||||
### Regionsspielplan abrufen
|
||||
|
||||
```
|
||||
GET /api/clicktt/region-meetings?association=HeTTV&championship=HTTV+25%2F26
|
||||
```
|
||||
|
||||
### Beliebige URL abrufen (nur click-tt.de / httv.de)
|
||||
|
||||
```
|
||||
GET /api/clicktt/fetch?url=https%3A%2F%2Fhttv.click-tt.de%2Fcgi-bin%2F...
|
||||
```
|
||||
|
||||
### Logs abrufen
|
||||
|
||||
```
|
||||
GET /api/clicktt/logs?limit=50&fetchType=leaguePage&association=HeTTV
|
||||
```
|
||||
|
||||
### URL-Info (Beispiele, Verband→Domain)
|
||||
|
||||
```
|
||||
GET /api/clicktt/url-info
|
||||
```
|
||||
|
||||
## Datenbank-Migration
|
||||
|
||||
```bash
|
||||
mysql -u USER -p DATABASE < backend/migrations/create_http_page_fetch_log.sql
|
||||
```
|
||||
|
||||
## Hinweis: mytischtennis.de vs. click-TT
|
||||
|
||||
Die **leaguePage** auf httv.click-tt.de zeigt eine Übersicht mit Links. Die eigentlichen **Tabellen und Spielpläne** verweisen auf **mytischtennis.de**:
|
||||
|
||||
```
|
||||
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/Hessenliga_Gr._Süd-West/gruppe/496273/tabelle/gesamt
|
||||
```
|
||||
|
||||
Diese mytischtennis.de-URLs werden bereits über den bestehenden MyTischtennis-URL-Parser und Auto-Fetch unterstützt. Die httv.click-tt.de-Seiten dienen vor allem der Navigation und der Ermittlung von Gruppen-IDs für verschiedene Bezirke/Saisonen.
|
||||
@@ -72,6 +72,7 @@
|
||||
|
||||
## API & Logging
|
||||
51. `api_log` - API-Logs
|
||||
52. `http_page_fetch_log` - HTTP-Aufrufe an click-TT/HTTV-Seiten (Logging)
|
||||
|
||||
## Gesamt: 51 Tabellen
|
||||
## Gesamt: 52 Tabellen
|
||||
|
||||
|
||||
@@ -6,11 +6,82 @@
|
||||
import express from 'express';
|
||||
import clickTtHttpPageService from '../services/clickTtHttpPageService.js';
|
||||
import HttpPageFetchLog from '../models/HttpPageFetchLog.js';
|
||||
import { authenticate } from '../middleware/authenticate.js';
|
||||
import { Op } from 'sequelize';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/clicktt/proxy
|
||||
* Proxy für iframe-Einbettung – liefert HTML direkt (ohne Auth, für iframe src).
|
||||
* Query: type (leaguePage|clubInfo|regionMeetings), association, championship, clubId
|
||||
* ODER: url (vollständige URL, nur click-tt.de/httv.de)
|
||||
*/
|
||||
router.get('/proxy', async (req, res, next) => {
|
||||
try {
|
||||
const { type, association, championship, clubId, url } = req.query;
|
||||
let targetUrl = null;
|
||||
let fetchType = 'proxy';
|
||||
|
||||
if (url) {
|
||||
if (!url.includes('click-tt.de') && !url.includes('httv.de')) {
|
||||
return res.status(400).send('<html><body><h1>Fehler</h1><p>Nur URLs von click-tt.de oder httv.de sind erlaubt.</p></body></html>');
|
||||
}
|
||||
targetUrl = url;
|
||||
fetchType = 'arbitrary';
|
||||
} else if (type === 'leaguePage') {
|
||||
targetUrl = clickTtHttpPageService.buildLeaguePageUrl({
|
||||
association: association || 'HeTTV',
|
||||
championship: championship || 'HTTV 25/26',
|
||||
});
|
||||
fetchType = 'leaguePage';
|
||||
} else if (type === 'clubInfo' && clubId) {
|
||||
targetUrl = clickTtHttpPageService.buildClubInfoDisplayUrl({
|
||||
association: association || 'HeTTV',
|
||||
clubId,
|
||||
});
|
||||
fetchType = 'clubInfoDisplay';
|
||||
} else if (type === 'regionMeetings') {
|
||||
targetUrl = clickTtHttpPageService.buildRegionMeetingFilterUrl({
|
||||
association: association || 'HeTTV',
|
||||
championship: championship || 'HTTV 25/26',
|
||||
});
|
||||
fetchType = 'regionMeetingFilter';
|
||||
} else {
|
||||
return res.status(400).send('<html><body><h1>Fehler</h1><p>Parameter type (leaguePage|clubInfo|regionMeetings) oder url erforderlich. Bei clubInfo ist clubId nötig.</p></body></html>');
|
||||
}
|
||||
|
||||
const result = await clickTtHttpPageService.fetchWithLogging({
|
||||
url: targetUrl,
|
||||
fetchType,
|
||||
association: association || null,
|
||||
championship: championship || null,
|
||||
clubIdParam: clubId || null,
|
||||
userId: null,
|
||||
});
|
||||
|
||||
let html = result.body;
|
||||
// CSP/X-Frame-Options entfernen für iframe-Einbettung
|
||||
html = (html || '')
|
||||
.replace(/<meta[^>]*http-equiv=["']content-security-policy["'][^>]*>/gi, '')
|
||||
.replace(/<meta[^>]*http-equiv=["']x-frame-options["'][^>]*>/gi, '')
|
||||
.replace(/<meta[^>]*http-equiv=["']x-content-type-options["'][^>]*>/gi, '');
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Frame-Options': 'ALLOWALL',
|
||||
'Content-Security-Policy': 'frame-ancestors *;',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
});
|
||||
res.send(html);
|
||||
} catch (error) {
|
||||
console.error('ClickTT Proxy Fehler:', error);
|
||||
res.status(500).send(
|
||||
`<html><body><h1>Fehler beim Laden</h1><p>${String(error.message)}</p></body></html>`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/clicktt/league-page
|
||||
* Ruft die Ligenübersicht ab (leaguePage)
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<span class="dropdown-icon">📋</span>
|
||||
{{ $t('navigation.logs') }}
|
||||
</router-link>
|
||||
<router-link v-if="isAdmin" to="/clicktt" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">🌐</span>
|
||||
HTTV / click-TT
|
||||
</router-link>
|
||||
<div class="dropdown-divider"></div>
|
||||
<router-link to="/personal-settings" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">⚙️</span>
|
||||
|
||||
@@ -19,6 +19,7 @@ import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
|
||||
import TeamManagementView from './views/TeamManagementView.vue';
|
||||
import PermissionsView from './views/PermissionsView.vue';
|
||||
import LogsView from './views/LogsView.vue';
|
||||
import ClickTtView from './views/ClickTtView.vue';
|
||||
import MemberTransferSettingsView from './views/MemberTransferSettingsView.vue';
|
||||
import PersonalSettings from './views/PersonalSettings.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
@@ -45,6 +46,7 @@ const routes = [
|
||||
{ path: '/team-management', component: TeamManagementView },
|
||||
{ path: '/permissions', component: PermissionsView },
|
||||
{ path: '/logs', component: LogsView },
|
||||
{ path: '/clicktt', component: ClickTtView },
|
||||
{ path: '/member-transfer-settings', component: MemberTransferSettingsView },
|
||||
{ path: '/personal-settings', component: PersonalSettings },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
|
||||
283
frontend/src/views/ClickTtView.vue
Normal file
283
frontend/src/views/ClickTtView.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="clicktt-view">
|
||||
<div class="header">
|
||||
<h1>HTTV / click-TT Seiten</h1>
|
||||
<p class="subtitle">
|
||||
Lade und bediene click-TT-Seiten (Ligenübersicht, Vereinsinfo, Spielplan) im iframe.
|
||||
Alle Aufrufe werden geloggt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="controls-section">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Seitentyp</label>
|
||||
<select v-model="pageType" class="form-select">
|
||||
<option value="leaguePage">Ligenübersicht (leaguePage)</option>
|
||||
<option value="clubInfo">Vereinsinfo (clubInfoDisplay)</option>
|
||||
<option value="regionMeetings">Regionsspielplan (regionMeetingFilter)</option>
|
||||
<option value="url">Direkte URL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="pageType !== 'url'">
|
||||
<label>Verband</label>
|
||||
<select v-model="association" class="form-select">
|
||||
<option value="HeTTV">HeTTV (Hessen)</option>
|
||||
<option value="RTTV">RTTV</option>
|
||||
<option value="WTTV">WTTV</option>
|
||||
<option value="TTVNw">TTVNw</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="pageType !== 'url' && pageType !== 'clubInfo'">
|
||||
<label>Championship / Saison</label>
|
||||
<input
|
||||
v-model="championship"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="z.B. HTTV 25/26, K43 25/26"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="pageType === 'clubInfo'">
|
||||
<label>Vereins-ID</label>
|
||||
<input
|
||||
v-model="clubId"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="z.B. 1060"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="pageType === 'url'">
|
||||
<label>URL</label>
|
||||
<input
|
||||
v-model="directUrl"
|
||||
type="text"
|
||||
class="form-input form-input-wide"
|
||||
placeholder="https://httv.click-tt.de/..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-actions">
|
||||
<button @click="loadPage" class="btn-primary" :disabled="!canLoad">
|
||||
Laden
|
||||
</button>
|
||||
<button @click="clearIframe" class="btn-secondary">Leeren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="iframe-container" v-if="iframeSrc">
|
||||
<iframe
|
||||
:src="iframeSrc"
|
||||
class="clicktt-iframe"
|
||||
title="click-TT Seite"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="placeholder">
|
||||
<p>Wähle einen Seitentyp, fülle die Parameter und klicke auf „Laden“, um die Seite anzuzeigen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const backendBaseUrl = import.meta.env.VITE_BACKEND || 'http://localhost:3005';
|
||||
|
||||
export default {
|
||||
name: 'ClickTtView',
|
||||
setup() {
|
||||
const pageType = ref('leaguePage');
|
||||
const association = ref('HeTTV');
|
||||
const championship = ref('HTTV 25/26');
|
||||
const clubId = ref('');
|
||||
const directUrl = ref('');
|
||||
const iframeSrc = ref('');
|
||||
|
||||
const canLoad = computed(() => {
|
||||
if (pageType.value === 'url') {
|
||||
return directUrl.value.trim().length > 0;
|
||||
}
|
||||
if (pageType.value === 'clubInfo') {
|
||||
return clubId.value.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function buildProxyUrl() {
|
||||
const base = `${backendBaseUrl}/api/clicktt/proxy`;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (pageType.value === 'url') {
|
||||
params.set('url', directUrl.value.trim());
|
||||
} else {
|
||||
params.set('type', pageType.value);
|
||||
params.set('association', association.value);
|
||||
if (pageType.value !== 'clubInfo') {
|
||||
params.set('championship', championship.value);
|
||||
}
|
||||
if (pageType.value === 'clubInfo') {
|
||||
params.set('clubId', clubId.value.trim());
|
||||
}
|
||||
}
|
||||
return `${base}?${params.toString()}`;
|
||||
}
|
||||
|
||||
function loadPage() {
|
||||
if (!canLoad.value) return;
|
||||
iframeSrc.value = buildProxyUrl();
|
||||
}
|
||||
|
||||
function clearIframe() {
|
||||
iframeSrc.value = '';
|
||||
}
|
||||
|
||||
return {
|
||||
pageType,
|
||||
association,
|
||||
championship,
|
||||
clubId,
|
||||
directUrl,
|
||||
iframeSrc,
|
||||
canLoad,
|
||||
loadPage,
|
||||
clearIframe,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clicktt-view {
|
||||
padding: 1.5rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--text-color, #333);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary, #666);
|
||||
margin: 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
background: var(--background-light, #f8f9fa);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.form-input-wide {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.form-group-actions {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color, #2563eb);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover, #1d4ed8);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 500px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.clicktt-iframe {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
min-height: 500px;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, #666);
|
||||
background: var(--background-light, #f8f9fa);
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
.placeholder p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user