Fügt Unterstützung für die neue nuscore API hinzu. Aktualisiert die Backend-Routen zur Verarbeitung von Anfragen an die nuscore API und integriert die neuen Dialogkomponenten im Frontend. Ermöglicht das Erstellen lokaler Kopien von nuscore-Daten und verbessert die Benutzeroberfläche durch neue Schaltflächen und Dialoge. Entfernt veraltete Konsolenausgaben und optimiert die Logik zur PIN-Verwaltung.

This commit is contained in:
Torsten Schulz (local)
2025-10-03 15:57:57 +02:00
parent cc964da9cf
commit 4b1a046149
28 changed files with 4325 additions and 6072 deletions

View File

@@ -595,6 +595,26 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1278,6 +1298,15 @@
"node": ">= 10"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -1778,6 +1807,29 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -1902,6 +1954,18 @@
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2816,6 +2880,26 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
@@ -2823,22 +2907,21 @@
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "4.x || >=6.0.0"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/nodemailer": {
@@ -3124,6 +3207,51 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"ideallyInert": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3792,7 +3920,8 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.7.0",
@@ -3992,15 +4121,26 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 David Frank
Copyright (c) 2016 - 2020 Node Fetch Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
"use strict";
// ref: https://github.com/tc39/proposal-global
var getGlobal = function () {
// the only reliable means to get the global object is
// `Function('return this')()`
// However, this causes CSP violations in Chrome apps.
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
}
var globalObject = getGlobal();
module.exports = exports = globalObject.fetch;
// Needed for TypeScript and Webpack.
if (globalObject.fetch) {
exports.default = globalObject.fetch.bind(globalObject);
}
exports.Headers = globalObject.Headers;
exports.Request = globalObject.Request;
exports.Response = globalObject.Response;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +1,131 @@
{
"name": "node-fetch",
"version": "2.7.0",
"description": "A light-weight module that brings window.fetch to node.js",
"main": "lib/index.js",
"browser": "./browser.js",
"module": "lib/index.mjs",
"files": [
"lib/index.js",
"lib/index.mjs",
"lib/index.es.js",
"browser.js"
],
"engines": {
"node": "4.x || >=6.0.0"
},
"scripts": {
"build": "cross-env BABEL_ENV=rollup rollup -c",
"prepare": "npm run build",
"test": "cross-env BABEL_ENV=test mocha --require babel-register --throw-deprecation test/test.js",
"report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js",
"coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json"
},
"repository": {
"type": "git",
"url": "https://github.com/bitinn/node-fetch.git"
},
"keywords": [
"fetch",
"http",
"promise"
],
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/bitinn/node-fetch/issues"
},
"homepage": "https://github.com/bitinn/node-fetch",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
},
"devDependencies": {
"@ungap/url-search-params": "^0.1.2",
"abort-controller": "^1.1.0",
"abortcontroller-polyfill": "^1.3.0",
"babel-core": "^6.26.3",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-transform-async-generator-functions": "^6.24.1",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "1.4.0",
"babel-register": "^6.16.3",
"chai": "^3.5.0",
"chai-as-promised": "^7.1.1",
"chai-iterator": "^1.1.1",
"chai-string": "~1.3.0",
"codecov": "3.3.0",
"cross-env": "^5.2.0",
"form-data": "^2.3.3",
"is-builtin-module": "^1.0.0",
"mocha": "^5.0.0",
"nyc": "11.9.0",
"parted": "^0.1.1",
"promise": "^8.0.3",
"resumer": "0.0.0",
"rollup": "^0.63.4",
"rollup-plugin-babel": "^3.0.7",
"string-to-arraybuffer": "^1.0.2",
"teeny-request": "3.7.0"
},
"release": {
"branches": [
"+([0-9]).x",
"main",
"next",
{
"name": "beta",
"prerelease": true
}
]
"name": "node-fetch",
"version": "3.3.2",
"description": "A light-weight module that brings Fetch API to node.js",
"main": "./src/index.js",
"sideEffects": false,
"type": "module",
"files": [
"src",
"@types/index.d.ts"
],
"types": "./@types/index.d.ts",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "mocha",
"coverage": "c8 report --reporter=text-lcov | coveralls",
"test-types": "tsd",
"lint": "xo"
},
"repository": {
"type": "git",
"url": "https://github.com/node-fetch/node-fetch.git"
},
"keywords": [
"fetch",
"http",
"promise",
"request",
"curl",
"wget",
"xhr",
"whatwg"
],
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-fetch/node-fetch/issues"
},
"homepage": "https://github.com/node-fetch/node-fetch",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
},
"devDependencies": {
"abort-controller": "^3.0.0",
"abortcontroller-polyfill": "^1.7.1",
"busboy": "^1.4.0",
"c8": "^7.7.2",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-iterator": "^3.0.2",
"chai-string": "^1.5.0",
"coveralls": "^3.1.0",
"form-data": "^4.0.0",
"formdata-node": "^4.2.4",
"mocha": "^9.1.3",
"p-timeout": "^5.0.0",
"stream-consumers": "^1.0.1",
"tsd": "^0.14.0",
"xo": "^0.39.1"
},
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"tsd": {
"cwd": "@types",
"compilerOptions": {
"esModuleInterop": true
}
},
"xo": {
"envs": [
"node",
"browser"
],
"ignores": [
"example.js"
],
"rules": {
"complexity": 0,
"import/extensions": 0,
"import/no-useless-path-segments": 0,
"import/no-anonymous-default-export": 0,
"import/no-named-as-default": 0,
"unicorn/import-index": 0,
"unicorn/no-array-reduce": 0,
"unicorn/prefer-node-protocol": 0,
"unicorn/numeric-separators-style": 0,
"unicorn/explicit-length-check": 0,
"capitalized-comments": 0,
"node/no-unsupported-features/es-syntax": 0,
"@typescript-eslint/member-ordering": 0
},
"overrides": [
{
"files": "test/**/*.js",
"envs": [
"node",
"mocha"
],
"rules": {
"max-nested-callbacks": 0,
"no-unused-expressions": 0,
"no-warning-comments": 0,
"new-cap": 0,
"guard-for-in": 0,
"unicorn/no-array-for-each": 0,
"unicorn/prevent-abbreviations": 0,
"promise/prefer-await-to-then": 0,
"ava/no-import-test-files": 0
}
}
]
},
"runkitExampleFilename": "example.js",
"release": {
"branches": [
"+([0-9]).x",
"main",
"next",
{
"name": "beta",
"prerelease": true
}
]
}
}

View File

@@ -22,8 +22,10 @@
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.3",
"node-fetch": "^3.3.2",
"nodemailer": "^6.9.14",
"pdf-parse": "^1.1.1",
"playwright": "^1.55.1",
"sequelize": "^6.37.3",
"sharp": "^0.33.5"
},
@@ -607,6 +609,26 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1290,6 +1312,15 @@
"node": ">= 10"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -1790,6 +1821,29 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -1914,6 +1968,18 @@
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2827,6 +2893,26 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
@@ -2834,22 +2920,21 @@
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "4.x || >=6.0.0"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/nodemailer": {
@@ -3135,6 +3220,50 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3803,7 +3932,8 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.7.0",
@@ -4002,15 +4132,26 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"

View File

@@ -26,8 +26,10 @@
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.3",
"node-fetch": "^3.3.2",
"nodemailer": "^6.9.14",
"pdf-parse": "^1.1.1",
"playwright": "^1.55.1",
"sequelize": "^6.37.3",
"sharp": "^0.33.5"
},

View File

@@ -0,0 +1,193 @@
import express from 'express';
import fetch from 'node-fetch';
const router = express.Router();
// Cookie-Store für nuscore-Sessions
const cookieStore = new Map();
// Hilfsfunktion zum Extrahieren von Cookies aus Set-Cookie Headers
function extractCookies(setCookieHeaders) {
const cookies = {};
if (setCookieHeaders) {
setCookieHeaders.forEach(cookie => {
const [nameValue] = cookie.split(';');
const [name, value] = nameValue.split('=');
if (name && value) {
cookies[name.trim()] = value.trim();
}
});
}
return cookies;
}
// Hilfsfunktion zum Formatieren von Cookies für Requests
function formatCookies(cookies) {
return Object.entries(cookies)
.map(([name, value]) => `${name}=${value}`)
.join('; ');
}
// Meeting-Info API-Endpunkt
router.get('/meetinginfo/:code', async (req, res) => {
const { code } = req.params;
console.log(`📊 Meeting-Info API für Code: ${code}`);
try {
// Hole Cookies für diesen Code (falls vorhanden)
const cookies = cookieStore.get(code) || {};
const url = `https://ttde-apps.liga.nu/nuliga/rs/tt/2022/meetingentry/reports/${code}/meetinginfo`;
const response = await fetch(url, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Referer': 'https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Connection': 'keep-alive',
...(Object.keys(cookies).length > 0 && { 'Cookie': formatCookies(cookies) })
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Speichere neue Cookies falls vorhanden
const newCookies = extractCookies(response.headers.raw()['set-cookie']);
if (Object.keys(newCookies).length > 0) {
cookieStore.set(code, { ...cookies, ...newCookies });
console.log(`🍪 Cookies für Code ${code} gespeichert:`, Object.keys(newCookies));
}
// CORS-Header setzen
res.set({
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'no-cache, no-store, must-revalidate'
});
res.json(data);
console.log(`✅ Meeting-Info für Code ${code} erfolgreich abgerufen`);
} catch (error) {
console.error(`❌ Fehler beim Abrufen der Meeting-Info für Code ${code}:`, error);
res.status(500).json({
error: 'Fehler beim Abrufen der Meeting-Info',
details: error.message
});
}
});
// Cookie-Initialisierung (für den ersten Request)
router.post('/init-cookies/:code', async (req, res) => {
const { code } = req.params;
console.log(`🍪 Cookie-Initialisierung für Code: ${code}`);
try {
// Erster Request an die nuscore-Seite um Cookies zu erhalten
const response = await fetch(`https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list?code=${code}`, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Connection': 'keep-alive'
}
});
// Extrahiere Cookies
const cookies = extractCookies(response.headers.raw()['set-cookie']);
if (Object.keys(cookies).length > 0) {
cookieStore.set(code, cookies);
console.log(`🍪 Cookies für Code ${code} initialisiert:`, Object.keys(cookies));
}
res.json({
success: true,
cookies: Object.keys(cookies),
message: `Cookies für Code ${code} initialisiert`
});
} catch (error) {
console.error(`❌ Fehler bei Cookie-Initialisierung für Code ${code}:`, error);
res.status(500).json({
error: 'Fehler bei Cookie-Initialisierung',
details: error.message
});
}
});
// Detaillierte Meeting-Daten API-Endpunkt
router.get('/meetingdetails/:uuid', async (req, res) => {
const { uuid } = req.params;
console.log(`📊 Meeting-Details API für UUID: ${uuid}`);
try {
// Hole Cookies für diesen Code (falls vorhanden)
const cookies = cookieStore.get(uuid) || {};
const url = `https://ttde-apps.liga.nu/nuliga/rs/tt/2022/meetingentry/reports/${uuid}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Referer': 'https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Connection': 'keep-alive',
...(Object.keys(cookies).length > 0 && { 'Cookie': formatCookies(cookies) })
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Speichere neue Cookies falls vorhanden
const newCookies = extractCookies(response.headers.raw()['set-cookie']);
if (Object.keys(newCookies).length > 0) {
cookieStore.set(uuid, { ...cookies, ...newCookies });
console.log(`🍪 Cookies für UUID ${uuid} gespeichert:`, Object.keys(newCookies));
}
// CORS-Header setzen
res.set({
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Cache-Control': 'no-cache, no-store, must-revalidate'
});
res.json(data);
console.log(`✅ Meeting-Details für UUID ${uuid} erfolgreich abgerufen`);
} catch (error) {
console.error(`❌ Fehler beim Abrufen der Meeting-Details für UUID ${uuid}:`, error);
res.status(500).json({
error: 'Fehler beim Abrufen der Meeting-Details',
details: error.message
});
}
});
export default router;

View File

@@ -0,0 +1,78 @@
import express from 'express';
import nuscoreProxyService from '../services/nuscoreProxyService.js';
const router = express.Router();
// Hauptroute für nuscore-Seite
router.get('/', async (req, res) => {
try {
const { code, pin } = req.query;
if (!code) {
return res.status(400).json({
error: 'Code-Parameter ist erforderlich'
});
}
console.log(`📊 Proxy-Anfrage für Code: ${code}, PIN: ${pin || 'nicht angegeben'}`);
const html = await nuscoreProxyService.proxyNuscorePage(code, pin);
// CORS-Header setzen für iframe-Einbettung
res.set({
'Content-Type': 'text/html; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'X-Frame-Options': 'ALLOWALL',
'Cache-Control': 'no-cache, no-store, must-revalidate'
});
res.send(html);
} catch (error) {
console.error('❌ Proxy-Fehler:', error);
res.status(500).json({
error: 'Fehler beim Laden der nuscore-Seite',
details: error.message
});
}
});
// Asset-Proxy für CSS, JS, Bilder etc.
router.get('/assets/*', async (req, res) => {
try {
const assetPath = req.params[0];
const originalUrl = `https://ttde-apps.liga.nu/nuliga/nuscore-tt/${assetPath}`;
console.log(`📦 Lade Asset: ${originalUrl}`);
const { content, contentType } = await nuscoreProxyService.proxyAsset(originalUrl);
res.set({
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=3600'
});
res.send(content);
} catch (error) {
console.error(`❌ Asset-Fehler für ${req.params[0]}:`, error.message);
res.status(404).json({
error: 'Asset nicht gefunden',
path: req.params[0]
});
}
});
// Health-Check Route
router.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'nuscore-proxy',
timestamp: new Date().toISOString()
});
});
export default router;

View File

@@ -0,0 +1,140 @@
import express from 'express';
import fetch from 'node-fetch';
const router = express.Router();
// Einfacher HTTP-Proxy ohne Playwright
router.get('/', async (req, res) => {
try {
const { code, pin } = req.query;
if (!code) {
return res.status(400).json({
error: 'Code-Parameter ist erforderlich'
});
}
console.log(`📊 Einfacher Proxy für Code: ${code}, PIN: ${pin || 'nicht angegeben'}`);
// Direkte HTTP-Anfrage an nuscore
const nuscoreUrl = `https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list?code=${encodeURIComponent(code)}`;
const response = await fetch(nuscoreUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: 30000
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
let html = await response.text();
// Einfache HTML-Modifikationen
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, '');
// Entferne Service Worker
html = html.replace(
/navigator\.serviceWorker\.register\([^)]+\)/g,
'// Service Worker deaktiviert für Proxy'
);
// Korrigiere alle Asset-URLs zu absoluten URLs
html = html.replace(
/src="\.\//g,
'src="https://ttde-apps.liga.nu/nuliga/nuscore-tt/'
);
html = html.replace(
/href="\.\//g,
'href="https://ttde-apps.liga.nu/nuliga/nuscore-tt/'
);
// Korrigiere auch URLs die bereits auf localhost zeigen
html = html.replace(
/src="http:\/\/localhost:3000\/nuliga\/nuscore-tt\//g,
'src="https://ttde-apps.liga.nu/nuliga/nuscore-tt/'
);
html = html.replace(
/href="http:\/\/localhost:3000\/nuliga\/nuscore-tt\//g,
'href="https://ttde-apps.liga.nu/nuliga/nuscore-tt/'
);
// Entferne vorhandene base-Tags und füge unseres hinzu
html = html.replace(
/<base[^>]*>/gi,
''
);
// Füge base-Tag hinzu für korrekte Asset-Auflösung
html = html.replace(
'<head>',
'<head><base href="https://ttde-apps.liga.nu/nuliga/nuscore-tt/">'
);
// Debug: Logge die HTML-Struktur
console.log('🔍 HTML nach base-Tag:', html.substring(html.indexOf('<base'), html.indexOf('</head>') + 7));
// Füge PIN-Injektion hinzu falls PIN vorhanden
if (pin) {
const pinScript = `
<script>
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
const pinInput = document.querySelector('input[type="password"][placeholder*="PIN"], input[placeholder*="Pin"], input[placeholder*="pin"]');
if (pinInput) {
pinInput.value = '${pin}';
pinInput.dispatchEvent(new Event('input', { bubbles: true }));
pinInput.dispatchEvent(new Event('change', { bubbles: true }));
console.log('PIN automatisch eingefügt: ${pin}');
}
}, 2000);
});
</script>
`;
html = html.replace('</head>', `${pinScript}</head>`);
}
// CORS-Header setzen für iframe-Einbettung
res.set({
'Content-Type': 'text/html; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'X-Frame-Options': 'ALLOWALL',
'Content-Security-Policy': "frame-ancestors *;",
'Cache-Control': 'no-cache, no-store, must-revalidate'
});
res.send(html);
console.log('✅ Einfacher Proxy erfolgreich');
} catch (error) {
console.error('❌ Einfacher Proxy-Fehler:', error);
res.status(500).json({
error: 'Fehler beim Laden der nuscore-Seite',
details: error.message
});
}
});
// Health-Check Route
router.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'nuscore-simple-proxy',
timestamp: new Date().toISOString()
});
});
export default router;

View File

@@ -38,6 +38,8 @@ import teamRoutes from './routes/teamRoutes.js';
import clubTeamRoutes from './routes/clubTeamRoutes.js';
import teamDocumentRoutes from './routes/teamDocumentRoutes.js';
import seasonRoutes from './routes/seasonRoutes.js';
import nuscoreSimpleProxyRoutes from './routes/nuscoreSimpleProxyRoutes.js';
import nuscoreApiRoutes from './routes/nuscoreApiRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -87,6 +89,8 @@ app.use('/api/teams', teamRoutes);
app.use('/api/club-teams', clubTeamRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/seasons', seasonRoutes);
app.use('/api/proxy/nuscore', nuscoreSimpleProxyRoutes);
app.use('/api/nuscore', nuscoreApiRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));

View File

@@ -0,0 +1,148 @@
import { chromium } from 'playwright';
class NuscoreProxyService {
constructor() {
this.browser = null;
this.context = null;
}
async initialize() {
if (!this.browser) {
console.log('🚀 Initialisiere Playwright Browser...');
this.browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-features=VizDisplayCompositor'
]
});
this.context = await this.browser.newContext({
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
ignoreHTTPSErrors: true
});
console.log('✅ Playwright Browser initialisiert');
}
}
async proxyNuscorePage(code, pin = null) {
try {
await this.initialize();
console.log(`🔍 Lade nuscore-Seite für Code: ${code}`);
const page = await this.context.newPage();
// Navigiere direkt zur nuscore-Seite ohne Request-Interception
const nuscoreUrl = `https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list?code=${encodeURIComponent(code)}`;
await page.goto(nuscoreUrl, {
waitUntil: 'load',
timeout: 30000
});
console.log('📄 Seite geladen');
// Optional: PIN automatisch einfügen falls vorhanden
if (pin) {
console.log(`🔑 Versuche PIN ${pin} einzufügen...`);
try {
// Suche nach PIN-Eingabefeld
const pinInput = await page.locator('input[type="password"][placeholder*="PIN"], input[placeholder*="Pin"], input[placeholder*="pin"]').first();
if (await pinInput.isVisible()) {
await pinInput.fill(pin);
console.log('✅ PIN eingefügt');
// Optional: Submit-Button klicken falls vorhanden
const submitBtn = await page.locator('button[type="submit"], input[type="submit"], button:has-text("Einloggen"), button:has-text("Anmelden")').first();
if (await submitBtn.isVisible()) {
await submitBtn.click();
await page.waitForLoadState('load', { timeout: 5000 });
}
}
} catch (error) {
console.log('⚠️ PIN-Einfügung fehlgeschlagen:', error.message);
}
}
// HTML-Inhalt abrufen und modifizieren
const html = await page.content();
// HTML modifizieren für Proxy-Betrieb
const modifiedHtml = this.modifyHtmlForProxy(html, code);
await page.close();
console.log('✅ nuscore-Seite erfolgreich proxiert');
return modifiedHtml;
} catch (error) {
console.error('❌ Fehler beim Proxying der nuscore-Seite:', error);
throw error;
}
}
modifyHtmlForProxy(html, code) {
// Entferne problematische Headers/Meta-Tags
let modified = 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, '');
// Entferne Service Worker Registrierung (kann Probleme verursachen)
modified = modified.replace(
/navigator\.serviceWorker\.register\([^)]+\)/g,
'// Service Worker deaktiviert für Proxy'
);
return modified;
}
async proxyAsset(url) {
try {
await this.initialize();
const page = await this.context.newPage();
const response = await page.goto(url, {
waitUntil: 'networkidle',
timeout: 15000
});
if (!response || !response.ok()) {
throw new Error(`Asset nicht erreichbar: ${url}`);
}
const content = await response.body();
const contentType = response.headers()['content-type'] || 'application/octet-stream';
await page.close();
return { content, contentType };
} catch (error) {
console.error(`❌ Fehler beim Laden von Asset ${url}:`, error.message);
throw error;
}
}
async cleanup() {
if (this.context) {
await this.context.close();
this.context = null;
}
if (this.browser) {
await this.browser.close();
this.browser = null;
}
console.log('🧹 Playwright Browser bereinigt');
}
}
// Singleton-Instanz
const nuscoreProxyService = new NuscoreProxyService();
export default nuscoreProxyService;

View File

@@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.7.3",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"jspdf-autotable": "^5.0.2",
@@ -1375,6 +1376,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",

View File

@@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.7.3",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.2",
"jspdf-autotable": "^5.0.2",

View File

@@ -54,11 +54,13 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import MatchReportDialog from './MatchReportDialog.vue';
import MatchReportApiDialog from './MatchReportApiDialog.vue';
import MatchReportHeaderActions from './MatchReportHeaderActions.vue';
export default {
components: {
MatchReportDialog,
MatchReportApiDialog,
MatchReportHeaderActions
},
name: 'DialogManager',
@@ -69,11 +71,15 @@ export default {
...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront']),
getDialogComponent(componentName) {
console.log('🔍 Suche Komponente:', componentName);
const components = {
'MatchReportDialog': MatchReportDialog,
'MatchReportApiDialog': MatchReportApiDialog,
'MatchReportHeaderActions': MatchReportHeaderActions
};
return components[componentName] || null;
const component = components[componentName] || null;
console.log('🔍 Gefundene Komponente:', component ? component.name : 'null');
return component;
},
bringToFront(dialogId) {
@@ -93,101 +99,31 @@ export default {
// Behandle die verschiedenen Action-Types
if (action.type === 'insertPin') {
// PIN ins iframe einfügen
// PIN via postMessage an das zuletzt geöffnete MatchReport-iframe senden
this.insertPinIntoIframe(action.match);
}
},
insertPinIntoIframe(match) {
console.log('🔍 PIN-Einfügen gestartet für Match:', match);
console.log('📌 Verfügbare PINs:', {
homePin: match.homePin,
guestPin: match.guestPin
});
// Versuche direkten Zugriff auf die MatchReportDialog-Komponente
const matchReportDialogs = document.querySelectorAll('.match-report-dialog');
console.log('🖼️ Gefundene MatchReportDialogs:', matchReportDialogs.length);
if (matchReportDialogs.length > 0) {
// Versuche die insertPinManually Methode aufzurufen
console.log('🎯 Versuche direkten Zugriff auf MatchReportDialog');
// Finde das iframe im aktuellen Dialog
const iframe = matchReportDialogs[matchReportDialogs.length - 1].querySelector('iframe');
if (iframe) {
console.log('✅ Iframe gefunden, versuche PIN-Einfügung');
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
console.log('✅ Direkter DOM-Zugriff möglich');
// Suche nach PIN-Feldern
const pinSelectors = [
'input[type="password"][placeholder*="Vereins"]',
'input[type="password"][placeholder*="Spiel-Pin"]',
'input[type="password"][placeholder*="PIN"]',
'input[type="password"]',
'input[placeholder*="Vereins"]',
'input[placeholder*="Spiel-Pin"]'
];
let pinField = null;
for (const selector of pinSelectors) {
pinField = iframeDoc.querySelector(selector);
if (pinField) {
console.log(`✅ PIN-Feld gefunden mit Selektor: ${selector}`);
break;
}
}
if (pinField && match.homePin) {
console.log('📝 Füge PIN ein:', match.homePin);
pinField.value = match.homePin;
pinField.dispatchEvent(new Event('input', { bubbles: true }));
pinField.dispatchEvent(new Event('change', { bubbles: true }));
pinField.dispatchEvent(new Event('blur', { bubbles: true }));
console.log('✅ PIN erfolgreich eingefügt');
return;
} else {
console.log('❌ PIN-Feld nicht gefunden');
console.log('🔍 Verfügbare Input-Felder:', iframeDoc.querySelectorAll('input'));
}
}
} catch (error) {
console.log('🚫 Cross-Origin-Zugriff blockiert (erwartet)');
}
}
}
// Fallback: PostMessage verwenden
console.log('🔍 PIN-Einfügen gestartet (postMessage)');
const iframes = document.querySelectorAll('iframe');
if (iframes.length > 0) {
const iframe = iframes[iframes.length - 1]; // Neuestes iframe
const message = {
action: 'fillPin',
pin: match.homePin,
timestamp: Date.now(),
source: 'trainingstagebuch'
};
console.log('📤 Sende PostMessage:', message);
const origins = ['https://ttde-apps.liga.nu', 'https://liga.nu', '*'];
origins.forEach(origin => {
try {
iframe.contentWindow.postMessage(message, origin);
console.log(`📤 PostMessage an ${origin} gesendet`);
} catch (e) {
console.log(`❌ PostMessage an ${origin} fehlgeschlagen:`, e.message);
}
});
if (iframes.length === 0) {
console.log('❌ Kein iframe gefunden');
return;
}
const iframe = iframes[iframes.length - 1];
const message = {
action: 'fillPin',
pin: match.homePin || match.guestPin || '',
timestamp: Date.now(),
source: 'trainingstagebuch'
};
try {
iframe.contentWindow.postMessage(message, 'http://localhost:3000');
console.log('📤 PostMessage an http://localhost:3000 gesendet');
} catch (e) {
console.log('PostMessage Fehler (nicht kritisch):', e.message);
}
console.log('💡 Alternative: Verwenden Sie den "📋 PIN kopieren" Button');
console.log('📋 PIN zum Kopieren:', match.homePin || match.guestPin);
},
handlePostMessage(event) {
@@ -195,8 +131,8 @@ export default {
console.log('- Origin:', event.origin);
console.log('- Data:', event.data);
// Nur Nachrichten von nuscore verarbeiten
if (event.origin !== 'https://ttde-apps.liga.nu' && event.origin !== 'https://liga.nu') {
// Nur Nachrichten von unserem Proxy verarbeiten
if (event.origin !== 'http://localhost:3000') {
console.log('🚫 Nachricht von unbekannter Origin ignoriert');
return;
}
@@ -306,7 +242,7 @@ export default {
.dialog-content {
flex: 1;
padding: 16px;
padding: 0;
overflow-y: auto;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,18 @@
<template>
<div class="match-report-dialog">
<!-- nuscore Analyzer für lokale Kopie -->
<NuscoreAnalyzer v-if="showAnalyzer" @close="showAnalyzer = false" />
<!-- Toggle für Analyzer -->
<div class="analyzer-toggle">
<button @click="showAnalyzer = !showAnalyzer" class="toggle-btn">
{{ showAnalyzer ? '🔼 Analyzer verstecken' : '🔍 nuscore analysieren' }}
</button>
<button @click="createLocalCopy" class="toggle-btn" style="margin-left: 10px;">
💾 Lokale Kopie erstellen
</button>
</div>
<div class="report-content">
<iframe
ref="reportIframe"
@@ -15,27 +28,29 @@
</template>
<script>
import NuscoreAnalyzer from './NuscoreAnalyzer.vue';
import { generateSimpleNuscoreHTML } from '../utils/simpleNuscoreHTMLGenerator.js';
export default {
name: 'MatchReportDialog',
components: {
NuscoreAnalyzer
},
props: {
match: {
type: Object,
required: true
}
},
data() {
return {
showAnalyzer: false
};
},
computed: {
reportUrl() {
// Verschiedene URL-Parameter versuchen, die nuscore möglicherweise unterstützt
const baseUrl = 'https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list';
const params = new URLSearchParams();
// Verschiedene Parameter-Namen versuchen
params.set('code', this.match.code);
params.set('gamecode', this.match.code);
params.set('spielcode', this.match.code);
params.set('matchcode', this.match.code);
return `${baseUrl}?${params.toString()}`;
// Verwende die neue API-basierte Lösung
return `http://localhost:3000/api/nuscore/meetinginfo/${this.match.code}`;
}
},
mounted() {
@@ -64,158 +79,24 @@ export default {
onIframeLoad() {
console.log('🔄 Iframe geladen, URL:', this.$refs.reportIframe?.src);
console.log('📊 Iframe-Inhalt:', this.$refs.reportIframe?.contentDocument?.title || 'Kein Titel');
// Warte kurz, damit das iframe vollständig geladen ist
setTimeout(() => {
this.injectContentScript();
}, 2000);
// Überwache URL-Änderungen im iframe
// Sende den Code an das iframe über PostMessage
this.tryPostMessage();
// Überwache URL-Änderungen im iframe (nur Logging, da kein direkter Zugriff möglich)
this.startUrlMonitoring();
},
injectContentScript() {
try {
const iframe = this.$refs.reportIframe;
if (!iframe || !iframe.contentWindow) {
console.log('Iframe noch nicht bereit für Content Script');
return;
}
// Content Script als String definieren
const contentScript = `
(function() {
console.log('Content Script geladen');
// Warte bis die Seite vollständig geladen ist
function waitForElement(selector, callback) {
const element = document.querySelector(selector);
if (element) {
callback(element);
} else {
setTimeout(() => waitForElement(selector, callback), 100);
}
}
// Suche nach dem Input-Feld
waitForElement('#gamecode', function(input) {
console.log('Input-Feld gefunden:', input);
// Code einfügen
input.value = '${this.match.code}';
// Events auslösen
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('blur', { bubbles: true }));
console.log('Code eingefügt:', '${this.match.code}');
// Suche nach dem Button und klicke ihn
setTimeout(() => {
const button = document.querySelector('button.btn-primary');
if (button) {
console.log('Button gefunden, klicke ihn');
button.click();
} else {
console.log('Button nicht gefunden');
}
}, 500);
});
})();
`;
// Script in das iframe injizieren
const script = iframe.contentDocument.createElement('script');
script.textContent = contentScript;
iframe.contentDocument.head.appendChild(script);
console.log('Content Script injiziert');
} catch (error) {
console.log('Fehler beim Injizieren des Content Scripts:', error);
// Fallback zu PostMessage
this.tryPostMessage();
}
},
fillGameCode() {
try {
const iframe = this.$refs.reportIframe;
if (!iframe || !iframe.contentWindow) {
console.log('Iframe noch nicht bereit');
return;
}
// Versuche, das Input-Feld zu finden und zu füllen
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
const gameCodeInput = iframeDoc.getElementById('gamecode');
if (gameCodeInput) {
// Code in das Input-Feld einfügen
gameCodeInput.value = this.match.code;
// Event auslösen, damit Angular die Änderung erkennt
gameCodeInput.dispatchEvent(new Event('input', { bubbles: true }));
gameCodeInput.dispatchEvent(new Event('change', { bubbles: true }));
console.log('Spielcode erfolgreich eingefügt:', this.match.code);
// Optional: Automatisch den Button klicken
setTimeout(() => {
this.clickLoadButton(iframeDoc);
}, 500);
} else {
console.log('Input-Feld mit ID "gamecode" nicht gefunden');
}
} else {
console.log('Kein Zugriff auf iframe-Dokument (Cross-Origin)');
// Fallback: PostMessage verwenden
this.tryPostMessage();
}
} catch (error) {
console.log('Fehler beim Zugriff auf iframe:', error);
// Fallback: PostMessage verwenden
this.tryPostMessage();
}
},
clickLoadButton(iframeDoc) {
try {
// Suche nach dem Button (verschiedene Selektoren)
const buttonSelectors = [
'button.btn-primary',
'button[type="button"]',
'button:contains("Laden")'
];
let loadButton = null;
for (const selector of buttonSelectors) {
loadButton = iframeDoc.querySelector(selector);
if (loadButton) break;
}
if (loadButton) {
loadButton.click();
console.log('Laden-Button erfolgreich geklickt');
} else {
console.log('Laden-Button nicht gefunden');
}
} catch (error) {
console.log('Fehler beim Klicken des Buttons:', error);
}
},
tryPostMessage() {
try {
const iframe = this.$refs.reportIframe;
if (iframe && iframe.contentWindow) {
// Sende Nachricht an das iframe
// Sende Nachricht an das iframe (jetzt localhost:3000)
iframe.contentWindow.postMessage({
action: 'fillGameCode',
code: this.match.code
}, 'https://ttde-apps.liga.nu');
}, 'http://localhost:3000');
console.log('PostMessage gesendet mit Code:', this.match.code);
}
@@ -260,52 +141,18 @@ export default {
return;
}
// Kein DOM-Zugriff mehr; ausschließlich postMessage senden
const message = {
action: 'fillPin',
pin: this.match.homePin,
timestamp: Date.now(),
source: 'trainingstagebuch'
};
console.log('📤 Sende PostMessage:', message);
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Suche nach PIN-Feldern auf der Meeting-Seite
const pinSelectors = [
'input[type="password"][placeholder*="Vereins"]',
'input[type="password"][placeholder*="Spiel-Pin"]',
'input[type="password"][placeholder*="PIN"]',
'input[type="password"]',
'input[placeholder*="Vereins"]',
'input[placeholder*="Spiel-Pin"]'
];
let pinField = null;
for (const selector of pinSelectors) {
pinField = iframeDoc.querySelector(selector);
if (pinField) {
console.log(`✅ PIN-Feld gefunden mit Selektor: ${selector}`);
break;
}
}
if (pinField) {
console.log('📝 Füge PIN ein:', this.match.homePin);
pinField.value = this.match.homePin;
pinField.dispatchEvent(new Event('input', { bubbles: true }));
pinField.dispatchEvent(new Event('change', { bubbles: true }));
pinField.dispatchEvent(new Event('blur', { bubbles: true }));
console.log('✅ PIN erfolgreich eingefügt');
} else {
console.log('❌ PIN-Feld nicht gefunden');
console.log('🔍 Verfügbare Input-Felder:', iframeDoc.querySelectorAll('input'));
}
} catch (error) {
console.log('🚫 Cross-Origin-Zugriff blockiert (erwartet)');
// Fallback: PostMessage
const message = {
action: 'fillPin',
pin: this.match.homePin,
timestamp: Date.now(),
source: 'trainingstagebuch'
};
console.log('📤 Sende PostMessage:', message);
iframe.contentWindow.postMessage(message, 'https://ttde-apps.liga.nu');
} catch (e) {
console.log('PostMessage Fehler (nicht kritisch):', e.message);
iframe.contentWindow.postMessage(message, '*');
}
},
@@ -316,9 +163,28 @@ export default {
this.attemptPinInsertionAfterRedirect();
},
async createLocalCopy() {
console.log('🏗️ Erstelle lokale Kopie mit PIN...');
// Erstelle eine einfache HTML-Datei, die die ursprüngliche nuscore-Seite in einem iframe lädt
const html = generateSimpleNuscoreHTML(this.match);
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'nuscore-local.html';
link.click();
URL.revokeObjectURL(url);
console.log('✅ Lokale Kopie erstellt (iframe-basiert)');
},
handlePostMessage(event) {
// Nur Nachrichten von nuscore verarbeiten
if (event.origin !== 'https://ttde-apps.liga.nu') {
// Nur Nachrichten von unserem Proxy verarbeiten
if (event.origin !== 'http://localhost:3000') {
return;
}
@@ -342,6 +208,27 @@ export default {
padding: 0;
}
.analyzer-toggle {
padding: 10px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.toggle-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.toggle-btn:hover {
background: var(--primary-hover);
}
.match-info {
background: #f8f9fa;
padding: 16px;

View File

@@ -1,8 +1,8 @@
<template>
<div class="match-report-header-actions">
<!-- <button @click="insertPin" class="header-action-btn" title="PIN automatisch einfügen">
<button @click="insertPin" class="header-action-btn" title="PIN automatisch einfügen">
📌 PIN einfügen
</button>-->
</button>
<button @click="copyPin" class="header-action-btn copy-button" title="PIN in Zwischenablage kopieren">
📋 PIN kopieren
</button>
@@ -31,7 +31,7 @@ export default {
});
},
async copyPin() {
async copyPin(event) {
const pin = this.match.homePin || this.match.guestPin;
if (!pin) {
console.warn('⚠️ Keine PIN verfügbar zum Kopieren');
@@ -43,7 +43,7 @@ export default {
console.log('✅ PIN erfolgreich kopiert:', pin);
// Visuelles Feedback
const button = event.target;
const button = event?.target;
const originalText = button.textContent;
button.textContent = '✅ Kopiert!';
button.style.backgroundColor = '#28a745';

View File

@@ -0,0 +1,528 @@
<template>
<div class="nuscore-analyzer">
<div class="analyzer-header">
<h3>🔍 nuscore Analyzer</h3>
<p>Analysiert und lädt nuscore-Ressourcen für lokale Nutzung herunter</p>
</div>
<div class="analyzer-controls">
<button @click="analyzeNuscore" :disabled="isAnalyzing" class="analyze-btn">
{{ isAnalyzing ? '🔄 Analysiere...' : '🔍 nuscore analysieren' }}
</button>
<button @click="downloadResources" :disabled="!resources.length || isDownloading" class="download-btn">
{{ isDownloading ? '⬇️ Lade herunter...' : `⬇️ ${resources.length} Ressourcen herunterladen` }}
</button>
<button @click="createLocalCopy" :disabled="!hasDownloadedResources || isCreating" class="create-btn">
{{ isCreating ? '🏗️ Erstelle...' : '🏗️ Lokale Kopie erstellen' }}
</button>
</div>
<div class="analyzer-results" v-if="resources.length > 0">
<h4>📋 Gefundene Ressourcen:</h4>
<div class="resource-list">
<div
v-for="resource in resources"
:key="resource.url"
class="resource-item"
:class="{ 'downloaded': resource.downloaded }"
>
<span class="resource-type">{{ resource.type }}</span>
<span class="resource-url">{{ resource.url }}</span>
<span class="resource-size">{{ formatSize(resource.size) }}</span>
<span v-if="resource.downloaded" class="status"></span>
</div>
</div>
</div>
<div class="analyzer-log" v-if="logs.length > 0">
<h4>📝 Log:</h4>
<div class="log-content">
<div v-for="(log, index) in logs" :key="index" class="log-entry">
{{ log }}
</div>
</div>
</div>
</div>
</template>
<script>
import { generateSimpleNuscoreHTML } from '../utils/simpleNuscoreHTMLGenerator.js';
export default {
name: 'NuscoreAnalyzer',
data() {
return {
isAnalyzing: false,
isDownloading: false,
isCreating: false,
resources: [],
downloadedResources: [],
logs: []
};
},
computed: {
hasDownloadedResources() {
return this.resources.some(resource => resource.downloaded) || this.downloadedResources.length > 0;
}
},
methods: {
async analyzeNuscore() {
this.isAnalyzing = true;
this.logs = [];
this.resources = [];
this.addLog('🔍 Starte nuscore-Analyse...');
try {
// Öffne nuscore in einem versteckten iframe
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list';
document.body.appendChild(iframe);
// Warte auf das Laden
await new Promise((resolve) => {
iframe.onload = resolve;
setTimeout(resolve, 5000); // Timeout nach 5 Sekunden
});
this.addLog('✅ nuscore-Seite geladen');
// Analysiere die Seite
await this.analyzePage(iframe);
// Entferne das iframe
document.body.removeChild(iframe);
this.addLog(`✅ Analyse abgeschlossen: ${this.resources.length} Ressourcen gefunden`);
} catch (error) {
this.addLog(`❌ Fehler bei der Analyse: ${error.message}`);
} finally {
this.isAnalyzing = false;
}
},
async analyzePage(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Analysiere Script-Tags
const scripts = doc.querySelectorAll('script[src]');
this.addLog(`📜 ${scripts.length} Script-Tags gefunden`);
scripts.forEach(script => {
this.resources.push({
type: 'script',
url: script.src,
size: 0,
downloaded: false
});
});
// Analysiere Link-Tags (CSS)
const stylesheets = doc.querySelectorAll('link[rel="stylesheet"][href]');
this.addLog(`🎨 ${stylesheets.length} Stylesheets gefunden`);
stylesheets.forEach(link => {
this.resources.push({
type: 'stylesheet',
url: link.href,
size: 0,
downloaded: false
});
});
// Analysiere andere Ressourcen
const images = doc.querySelectorAll('img[src]');
this.addLog(`🖼️ ${images.length} Bilder gefunden`);
images.forEach(img => {
this.resources.push({
type: 'image',
url: img.src,
size: 0,
downloaded: false
});
});
} catch (error) {
this.addLog(`⚠️ Cross-Origin-Zugriff blockiert: ${error.message}`);
// Fallback: Analysiere die ursprüngliche URL
await this.analyzeUrlDirectly();
}
},
async analyzeUrlDirectly() {
this.addLog('🔄 Fallback: Bekannte nuscore-Ressourcen verwenden');
try {
// Bekannte nuscore-Ressourcen basierend auf der URL-Struktur
const baseUrl = 'https://ttde-apps.liga.nu';
// Typische nuscore-Ressourcen
const knownResources = [
// JavaScript-Bundles (typische Angular/React-Bundles)
`${baseUrl}/nuliga/nuscore-tt/main.js`,
`${baseUrl}/nuliga/nuscore-tt/runtime.js`,
`${baseUrl}/nuliga/nuscore-tt/vendor.js`,
`${baseUrl}/nuliga/nuscore-tt/polyfills.js`,
`${baseUrl}/nuliga/nuscore-tt/styles.js`,
`${baseUrl}/nuliga/nuscore-tt/main.6c3da9dbb4871026b9a1.js`, // Aus den Logs
// CSS-Dateien
`${baseUrl}/nuliga/nuscore-tt/styles.css`,
`${baseUrl}/nuliga/nuscore-tt/main.css`,
// Weitere mögliche Ressourcen
`${baseUrl}/nuliga/nuscore-tt/assets/main.js`,
`${baseUrl}/nuliga/nuscore-tt/assets/runtime.js`,
`${baseUrl}/nuliga/nuscore-tt/assets/vendor.js`,
`${baseUrl}/nuliga/nuscore-tt/assets/styles.css`,
// Angular-spezifische Ressourcen
`${baseUrl}/nuliga/nuscore-tt/angular.js`,
`${baseUrl}/nuliga/nuscore-tt/angular.min.js`,
// Bootstrap/CSS-Frameworks
`${baseUrl}/nuliga/nuscore-tt/bootstrap.css`,
`${baseUrl}/nuliga/nuscore-tt/bootstrap.min.css`,
// Fonts
`${baseUrl}/nuliga/nuscore-tt/fonts/`,
// Bilder/Assets
`${baseUrl}/nuliga/nuscore-tt/assets/images/`,
`${baseUrl}/nuliga/nuscore-tt/assets/icons/`
];
// Füge bekannte Ressourcen hinzu
knownResources.forEach(url => {
this.resources.push({
type: url.includes('.css') ? 'stylesheet' :
url.includes('.js') ? 'script' :
url.includes('fonts') ? 'font' : 'asset',
url: url,
size: 0,
downloaded: false
});
});
this.addLog(`${this.resources.length} bekannte Ressourcen hinzugefügt`);
// Versuche zusätzlich, Ressourcen aus dem aktuellen iframe zu extrahieren
await this.extractResourcesFromCurrentPage();
} catch (error) {
this.addLog(`❌ Bekannte Ressourcen-Analyse fehlgeschlagen: ${error.message}`);
}
},
async extractResourcesFromCurrentPage() {
this.addLog('🔍 Versuche Ressourcen-Extraktion von der aktuellen Seite');
try {
// Analysiere die aktuelle Seite nach Ressourcen
const scripts = document.querySelectorAll('script[src]');
const stylesheets = document.querySelectorAll('link[rel="stylesheet"][href]');
this.addLog(`📜 ${scripts.length} Scripts auf aktueller Seite gefunden`);
this.addLog(`🎨 ${stylesheets.length} Stylesheets auf aktueller Seite gefunden`);
// Füge gefundene Ressourcen hinzu (falls sie nuscore-relevant sind)
scripts.forEach(script => {
if (script.src.includes('nuscore') || script.src.includes('liga.nu')) {
this.resources.push({
type: 'script',
url: script.src,
size: 0,
downloaded: false
});
}
});
stylesheets.forEach(link => {
if (link.href.includes('nuscore') || link.href.includes('liga.nu')) {
this.resources.push({
type: 'stylesheet',
url: link.href,
size: 0,
downloaded: false
});
}
});
this.addLog(`${this.resources.length} nuscore-relevante Ressourcen gefunden`);
} catch (error) {
this.addLog(`⚠️ Ressourcen-Extraktion fehlgeschlagen: ${error.message}`);
}
},
async downloadResources() {
this.isDownloading = true;
this.downloadedResources = [];
this.addLog('⬇️ Starte Download der Ressourcen...');
this.addLog('⚠️ Hinweis: CORS-Beschränkungen können Downloads blockieren');
for (const resource of this.resources) {
try {
this.addLog(`⬇️ Lade herunter: ${resource.url}`);
// Versuche verschiedene Download-Methoden
let blob = null;
// Methode 1: Direkter fetch
try {
const response = await fetch(resource.url, {
mode: 'cors',
credentials: 'omit'
});
blob = await response.blob();
} catch (corsError) {
this.addLog(`⚠️ CORS-Fehler für ${resource.url}, versuche Alternative...`);
// Methode 2: Proxy über iframe
blob = await this.downloadViaIframe(resource.url);
}
if (blob && blob.size > 0) {
resource.downloaded = true;
resource.size = blob.size;
resource.blob = blob;
resource.localUrl = URL.createObjectURL(blob);
this.downloadedResources.push(resource);
this.addLog(`✅ Heruntergeladen: ${this.formatSize(blob.size)}`);
} else {
this.addLog(`⚠️ Leere oder ungültige Ressource: ${resource.url}`);
}
} catch (error) {
this.addLog(`❌ Download fehlgeschlagen: ${resource.url} - ${error.message}`);
// Fallback: Markiere als "verfügbar aber nicht heruntergeladen"
resource.available = true;
resource.downloaded = false;
}
}
this.addLog(`✅ Download abgeschlossen: ${this.downloadedResources.length}/${this.resources.length} erfolgreich`);
this.addLog(`💡 Tipp: Verwenden Sie die lokale Kopie auch mit externen Ressourcen`);
this.isDownloading = false;
},
async downloadViaIframe(url) {
return new Promise((resolve, reject) => {
try {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
iframe.onload = () => {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
const content = doc.documentElement.outerHTML;
const blob = new Blob([content], { type: 'text/html' });
document.body.removeChild(iframe);
resolve(blob);
} catch (error) {
document.body.removeChild(iframe);
reject(error);
}
};
iframe.onerror = () => {
document.body.removeChild(iframe);
reject(new Error('Iframe load failed'));
};
// Timeout nach 10 Sekunden
setTimeout(() => {
if (document.body.contains(iframe)) {
document.body.removeChild(iframe);
reject(new Error('Download timeout'));
}
}, 10000);
} catch (error) {
reject(error);
}
});
},
async createLocalCopy() {
this.isCreating = true;
this.addLog('🏗️ Erstelle lokale Kopie...');
try {
// Erstelle eine einfache HTML-Datei, die die ursprüngliche nuscore-Seite in einem iframe lädt
const html = generateSimpleNuscoreHTML({ homePin: '', guestPin: '', code: '' });
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'nuscore-local.html';
link.click();
URL.revokeObjectURL(url);
this.addLog('✅ Lokale Kopie erstellt (iframe-basiert)');
this.addLog('💡 Die lokale Kopie lädt die ursprüngliche nuscore-Seite in einem iframe');
this.addLog('💡 PIN-Einfügung funktioniert über PostMessage');
} catch (error) {
this.addLog(`❌ Fehler beim Erstellen der lokalen Kopie: ${error.message}`);
} finally {
this.isCreating = false;
}
},
addLog(message) {
const timestamp = new Date().toLocaleTimeString();
this.logs.push(`[${timestamp}] ${message}`);
},
formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
};
</script>
<style scoped>
.nuscore-analyzer {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin: 20px 0;
}
.analyzer-header h3 {
margin: 0 0 10px 0;
color: var(--primary-color);
}
.analyzer-controls {
display: flex;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}
.analyze-btn, .download-btn, .create-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
}
.analyze-btn {
background: var(--primary-color);
color: white;
}
.analyze-btn:hover:not(:disabled) {
background: var(--primary-hover);
}
.download-btn {
background: var(--secondary-color);
color: white;
}
.download-btn:hover:not(:disabled) {
background: #0056b3;
}
.create-btn {
background: #28a745;
color: white;
}
.create-btn:hover:not(:disabled) {
background: #218838;
}
.analyze-btn:disabled, .download-btn:disabled, .create-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.resource-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
}
.resource-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #eee;
gap: 10px;
}
.resource-item:last-child {
border-bottom: none;
}
.resource-item.downloaded {
background: #d4edda;
}
.resource-type {
font-weight: bold;
min-width: 80px;
font-size: 12px;
}
.resource-url {
flex: 1;
font-family: monospace;
font-size: 12px;
color: #666;
}
.resource-size {
min-width: 60px;
text-align: right;
font-size: 12px;
}
.status {
color: #28a745;
font-weight: bold;
}
.log-content {
max-height: 200px;
overflow-y: auto;
background: #000;
color: #0f0;
padding: 10px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin-bottom: 2px;
}
</style>

View File

@@ -0,0 +1,85 @@
// HTML-Generator für nuscore lokale Kopie
export function generateNuscoreHTML(scripts, stylesheets, useExternalUrls = false) {
const parts = [];
parts.push('<!DOCTYPE html>');
parts.push('<html lang="de">');
// Head
parts.push('<head>');
parts.push(' <meta charset="UTF-8">');
parts.push(' <meta name="viewport" content="width=device-width, initial-scale=1.0">');
parts.push(' <title>nuscore - Lokale Kopie</title>');
parts.push(' <base href="/">');
parts.push(' ');
parts.push(' <!-- Stylesheets -->');
stylesheets.forEach(sheet => {
const href = useExternalUrls ? sheet.url : sheet.localUrl;
parts.push(` <link rel="stylesheet" href="${href}">`);
});
parts.push(' ');
parts.push(' <!-- PIN-Einfügung Script -->');
parts.push(' <script>');
parts.push(' window.addEventListener("message", function(event) {');
parts.push(' if (event.data && event.data.action === "fillPin") {');
parts.push(' console.log("PIN-Einfügung empfangen:", event.data.pin);');
parts.push(' ');
parts.push(' const pinSelectors = [');
parts.push(' "input[type=\\"password\\"][placeholder*=\\"Vereins\\"]",');
parts.push(' "input[type=\\"password\\"][placeholder*=\\"Spiel-Pin\\"]",');
parts.push(' "input[type=\\"password\\"][placeholder*=\\"PIN\\"]",');
parts.push(' "input[type=\\"password\\"]"');
parts.push(' ];');
parts.push(' ');
parts.push(' let pinField = null;');
parts.push(' for (const selector of pinSelectors) {');
parts.push(' pinField = document.querySelector(selector);');
parts.push(' if (pinField) break;');
parts.push(' }');
parts.push(' ');
parts.push(' if (pinField) {');
parts.push(' pinField.value = event.data.pin;');
parts.push(' pinField.dispatchEvent(new Event("input", { bubbles: true }));');
parts.push(' pinField.dispatchEvent(new Event("change", { bubbles: true }));');
parts.push(' console.log("PIN erfolgreich eingefügt:", event.data.pin);');
parts.push(' }');
parts.push(' }');
parts.push(' });');
parts.push(' </script>');
parts.push('</head>');
// Body
parts.push('<body>');
parts.push(' <app-root></app-root>');
parts.push(' ');
parts.push(' <!-- Scripts -->');
scripts.forEach(script => {
const src = useExternalUrls ? script.url : script.localUrl;
parts.push(` <script src="${src}"></script>`);
});
parts.push(' ');
parts.push(' <!-- Initialisierung -->');
parts.push(' <script>');
parts.push(' console.log("nuscore lokale Kopie gestartet");');
parts.push(' console.log("PIN-Einfügung bereit");');
parts.push(' ');
parts.push(' const urlParams = new URLSearchParams(window.location.search);');
parts.push(' const code = urlParams.get("code");');
parts.push(' if (code) {');
parts.push(' console.log("Code aus URL gefunden:", code);');
parts.push(' }');
parts.push(' ');
parts.push(' // Angular Router Konfiguration für lokale Kopie');
parts.push(' if (window.ng && window.ng.core) {');
parts.push(' console.log("Angular gefunden, konfiguriere Router...");');
parts.push(' }');
parts.push(' </script>');
parts.push('</body>');
parts.push('</html>');
return parts.join('\n');
}

View File

@@ -0,0 +1,111 @@
// Einfacher HTML-Generator für nuscore lokale Kopie (iframe-basiert)
export function generateSimpleNuscoreHTML(match) {
const pin = match.homePin || match.guestPin || '';
const code = match.code || '';
return `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nuscore - Lokale Kopie</title>
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
.header { background: #f0f0f0; padding: 10px; border-bottom: 1px solid #ccc; }
.controls { margin: 10px 0; }
.controls button { margin-right: 10px; padding: 5px 10px; }
.iframe-container { width: 100%; height: calc(100vh - 100px); }
.iframe-container iframe { width: 100%; height: 100%; border: none; }
.status { padding: 10px; background: #e8f5e8; border: 1px solid #4caf50; margin: 10px 0; }
</style>
</head>
<body>
<div class="header">
<h1>nuscore - Lokale Kopie</h1>
<div class="controls">
<button onclick="insertPin()">📌 PIN einfügen</button>
<button onclick="copyPin()">📋 PIN kopieren</button>
<input type="password" id="pinInput" placeholder="PIN eingeben" style="margin-left: 10px;">
</div>
<div class="status" id="status">Bereit für PIN-Einfügung</div>
</div>
<div class="iframe-container">
<iframe id="nuscoreFrame" src="https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list"></iframe>
</div>
<script>
let currentPin = "";
// PIN aus Match-Objekt laden
const pin = "${pin}";
const code = "${code}";
if (pin) {
currentPin = pin;
document.getElementById("pinInput").value = pin;
document.getElementById("status").textContent = "PIN aus Match geladen: " + pin;
}
function insertPin() {
const pin = document.getElementById("pinInput").value || currentPin;
if (!pin) {
document.getElementById("status").textContent = "Keine PIN eingegeben";
return;
}
const iframe = document.getElementById("nuscoreFrame");
const message = {
action: "fillPin",
pin: pin,
timestamp: Date.now(),
source: "nuscore-local"
};
try {
iframe.contentWindow.postMessage(message, "https://ttde-apps.liga.nu");
document.getElementById("status").textContent = "PIN gesendet: " + pin;
} catch (error) {
document.getElementById("status").textContent = "Fehler beim Senden der PIN: " + error.message;
}
}
function copyPin() {
const pin = document.getElementById("pinInput").value || currentPin;
if (!pin) {
document.getElementById("status").textContent = "Keine PIN zum Kopieren";
return;
}
navigator.clipboard.writeText(pin).then(() => {
document.getElementById("status").textContent = "PIN kopiert: " + pin;
}).catch(() => {
// Fallback
const textArea = document.createElement("textarea");
textArea.value = pin;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
document.getElementById("status").textContent = "PIN kopiert (Fallback): " + pin;
});
}
// PostMessage Listener für Antworten vom iframe
window.addEventListener("message", function(event) {
if (event.origin !== "https://ttde-apps.liga.nu") return;
if (event.data && event.data.action === "pinInserted") {
document.getElementById("status").textContent = "PIN erfolgreich eingefügt!";
} else if (event.data && event.data.action === "pinError") {
document.getElementById("status").textContent = "PIN-Einfügung fehlgeschlagen: " + event.data.error;
}
});
console.log("nuscore lokale Kopie gestartet");
if (code) console.log("Code aus Match:", code);
if (pin) console.log("PIN aus Match:", pin);
</script>
</body>
</html>`;
}

View File

@@ -93,13 +93,13 @@ import { mapGetters, mapActions } from 'vuex';
import apiClient from '../apiClient.js';
import PDFGenerator from '../components/PDFGenerator.js';
import SeasonSelector from '../components/SeasonSelector.vue';
import MatchReportDialog from '../components/MatchReportDialog.vue';
import MatchReportApiDialog from '../components/MatchReportApiDialog.vue';
export default {
name: 'ScheduleView',
components: {
SeasonSelector,
MatchReportDialog
MatchReportApiDialog
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
@@ -377,15 +377,9 @@ export default {
const title = `${match.homeTeam?.name || 'N/A'} vs ${match.guestTeam?.name || 'N/A'} - ${this.selectedLeague}`;
this.openDialog({
title,
component: 'MatchReportDialog',
component: 'MatchReportApiDialog',
props: {
match
},
headerActions: {
component: 'MatchReportHeaderActions',
props: {
match
}
}
});
},

65
test-iframe.html Normal file
View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nuscore iframe Test</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
iframe { width: 100%; height: 600px; border: 2px solid #007bff; }
.status { padding: 10px; margin: 10px 0; background: #f8f9fa; border-radius: 4px; }
</style>
</head>
<body>
<h1>nuscore iframe Test</h1>
<div class="status">
<strong>Test-URL:</strong> <span id="test-url"></span><br>
<strong>Status:</strong> <span id="status">Lädt...</span>
</div>
<iframe
id="test-iframe"
src="http://localhost:3000/api/proxy/nuscore?code=KXEA9JR5EUPW&pin=1555"
onload="iframeLoaded()"
onerror="iframeError()">
</iframe>
<script>
const iframe = document.getElementById('test-iframe');
const statusEl = document.getElementById('status');
const urlEl = document.getElementById('test-url');
urlEl.textContent = iframe.src;
function iframeLoaded() {
statusEl.textContent = '✅ Iframe geladen';
statusEl.style.color = 'green';
try {
// Versuche auf iframe-Inhalt zuzugreifen
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
console.log('Iframe-Titel:', iframeDoc.title);
console.log('Iframe-Body:', iframeDoc.body ? 'Vorhanden' : 'Nicht vorhanden');
}
} catch (e) {
console.log('Cross-Origin-Zugriff blockiert (erwartet):', e.message);
}
}
function iframeError() {
statusEl.textContent = '❌ Iframe-Fehler';
statusEl.style.color = 'red';
}
// Timeout nach 10 Sekunden
setTimeout(() => {
if (statusEl.textContent === 'Lädt...') {
statusEl.textContent = '⏰ Timeout';
statusEl.style.color = 'orange';
}
}, 10000);
</script>
</body>
</html>

95
test-nuscore-api.html Normal file
View File

@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Nuscore API</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ccc; border-radius: 5px; }
.success { color: green; }
.error { color: red; }
button { padding: 10px 15px; margin: 5px; cursor: pointer; }
pre { background: #f5f5f5; padding: 10px; border-radius: 3px; overflow-x: auto; }
</style>
</head>
<body>
<h1>Test Nuscore API</h1>
<div class="test-section">
<h2>1. Health Check</h2>
<button onclick="testHealth()">Health Check</button>
<div id="health-status"></div>
</div>
<div class="test-section">
<h2>2. Cookie Initialisierung</h2>
<input type="text" id="test-code" value="KXEA9JR5EUPW" placeholder="Code eingeben">
<button onclick="testInitCookies()">Cookies initialisieren</button>
<div id="cookie-status"></div>
</div>
<div class="test-section">
<h2>3. Meeting Info</h2>
<button onclick="testMeetingInfo()">Meeting Info laden</button>
<div id="meeting-status"></div>
<pre id="meeting-data"></pre>
</div>
<script>
async function testHealth() {
const statusDiv = document.getElementById('health-status');
try {
const response = await fetch('http://localhost:3000/api/nuscore/health');
const data = await response.json();
statusDiv.innerHTML = `<div class="success">✅ Health Check OK: ${JSON.stringify(data)}</div>`;
} catch (error) {
statusDiv.innerHTML = `<div class="error">❌ Health Check Fehler: ${error.message}</div>`;
}
}
async function testInitCookies() {
const code = document.getElementById('test-code').value;
const statusDiv = document.getElementById('cookie-status');
try {
const response = await fetch(`http://localhost:3000/api/nuscore/init-cookies/${code}`, {
method: 'POST'
});
const data = await response.json();
if (response.ok) {
statusDiv.innerHTML = `<div class="success">✅ Cookies initialisiert: ${JSON.stringify(data)}</div>`;
} else {
statusDiv.innerHTML = `<div class="error">❌ Cookie-Fehler: ${JSON.stringify(data)}</div>`;
}
} catch (error) {
statusDiv.innerHTML = `<div class="error">❌ Cookie-Fehler: ${error.message}</div>`;
}
}
async function testMeetingInfo() {
const code = document.getElementById('test-code').value;
const statusDiv = document.getElementById('meeting-status');
const dataDiv = document.getElementById('meeting-data');
try {
const response = await fetch(`http://localhost:3000/api/nuscore/meetinginfo/${code}`);
const data = await response.json();
if (response.ok) {
statusDiv.innerHTML = `<div class="success">✅ Meeting Info geladen</div>`;
dataDiv.textContent = JSON.stringify(data, null, 2);
} else {
statusDiv.innerHTML = `<div class="error">❌ Meeting Info Fehler: ${JSON.stringify(data)}</div>`;
}
} catch (error) {
statusDiv.innerHTML = `<div class="error">❌ Meeting Info Fehler: ${error.message}</div>`;
}
}
// Automatisch Health Check beim Laden
window.onload = testHealth;
</script>
</body>
</html>

78
test-proxy.html Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nuscore Proxy Test</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ccc; border-radius: 8px; }
button { padding: 10px 20px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
iframe { width: 100%; height: 600px; border: 1px solid #ccc; }
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<h1>nuscore Proxy Test</h1>
<div class="test-section">
<h2>Health Check</h2>
<button onclick="testHealth()">Health Check</button>
<div id="health-status" class="status"></div>
</div>
<div class="test-section">
<h2>Proxy Test</h2>
<input type="text" id="test-code" placeholder="Code eingeben" value="TEST123">
<button onclick="testProxy()">Proxy Test</button>
<div id="proxy-status" class="status"></div>
</div>
<div class="test-section">
<h2>Iframe Test</h2>
<button onclick="loadIframe()">Iframe laden</button>
<iframe id="test-iframe" style="display: none;"></iframe>
</div>
<script>
async function testHealth() {
const statusDiv = document.getElementById('health-status');
try {
const response = await fetch('http://localhost:3000/api/proxy/nuscore/health');
const data = await response.json();
statusDiv.innerHTML = `<div class="success">✅ Health Check OK: ${JSON.stringify(data)}</div>`;
} catch (error) {
statusDiv.innerHTML = `<div class="error">❌ Health Check Fehler: ${error.message}</div>`;
}
}
async function testProxy() {
const code = document.getElementById('test-code').value;
const statusDiv = document.getElementById('proxy-status');
try {
const response = await fetch(`http://localhost:3000/api/proxy/nuscore?code=${encodeURIComponent(code)}`);
const text = await response.text();
if (response.ok) {
statusDiv.innerHTML = `<div class="success">✅ Proxy OK: ${text.length} Zeichen empfangen</div>`;
} else {
statusDiv.innerHTML = `<div class="error">❌ Proxy Fehler: ${text}</div>`;
}
} catch (error) {
statusDiv.innerHTML = `<div class="error">❌ Proxy Fehler: ${error.message}</div>`;
}
}
function loadIframe() {
const code = document.getElementById('test-code').value;
const iframe = document.getElementById('test-iframe');
iframe.src = `http://localhost:3000/api/proxy/nuscore?code=${encodeURIComponent(code)}`;
iframe.style.display = 'block';
}
</script>
</body>
</html>