lcc_tool/tools/oauth2-tester/index.html
Jan d840b05da2 Enhanced OAuth2 API Tester tool, updated validation and improved CORS configuration:
- Improved OAuth2 Tester UI/UX: added support for multiple HTTP methods, query parameters, request body validation, collapsible result sections, and dynamic input handling.
- Enhanced `UserController` with validation annotations for `UserDTO` in API requests.
- Updated `UserDTO` to include stricter validation constraints (`@NotNull`, `@NotBlank`, `@Email`).
- Adjusted CORS configuration to allow all origins for OAuth endpoints.
2025-10-28 18:01:21 +01:00

796 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth2 API Tester</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f8fafc;
min-height: 100vh;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 700px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
font-size: 14px;
}
input, textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
transition: border-color 0.3s;
}
textarea {
min-height: 100px;
resize: vertical;
font-family: 'Courier New', monospace;
}
select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
transition: border-color 0.3s;
background-color: white;
cursor: pointer;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
.method-selector {
display: grid;
grid-template-columns: 120px 1fr;
gap: 10px;
align-items: start;
}
.method-select {
width: 100%;
}
button {
width: 100%;
padding: 14px;
background: #5AF0B4;
color: #002F54;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
margin-top: 10px;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
display: none;
text-align: center;
margin: 20px 0;
color: #667eea;
}
.loading.active {
display: block;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result {
margin-top: 30px;
display: none;
}
.result.active {
display: block;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.result-title {
font-weight: 600;
color: #333;
font-size: 16px;
}
.result-box {
background: #f8f9fa;
border: none;
border-radius: 6px;
padding: 15px;
max-height: 400px;
overflow-y: auto;
}
.result-box pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #333;
}
.error {
color: #333;
background-color: #f8f9fa;
border-color: #BC2B72;
}
.success {
color: #333;
background-color: #f8f9fa;
border-color: #81c784;
}
.info-section {
background-color: #c3cfdf;
color: #002F54;
border-left: 4px solid #002F54;
padding: 15px;
margin-bottom: 25px;
border-radius: 4px;
}
.info-section h3 {
color: #002F54;
font-size: 14px;
margin-bottom: 8px;
}
.info-section p {
color: #002F54;
font-size: 13px;
line-height: 1.6;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.status-success {
background: #e8f5e9;
color: #388e3c;
}
.status-error {
background-color: #BC2B72;
color: #ffffff;
}
.error-summary {
background-color: #c3cfdf;
color: #002F54;
border-left: 4px solid #002F54;
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
line-height: 1.6;
display: none;
word-wrap: break-word;
overflow-wrap: break-word;
}
.error-summary strong {
font-weight: 600;
}
.details-button {
display: none;
}
.result-box-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: #e9ecef;
border-radius: 6px 6px 0 0;
cursor: pointer;
user-select: none;
margin: -15px -15px 10px -15px;
}
.result-box-header:hover {
background: #dee2e6;
}
.result-box-title {
font-weight: 500;
color: #333;
font-size: 14px;
}
.toggle-icon {
font-size: 18px;
color: #666;
transition: transform 0.3s;
}
.toggle-icon.collapsed {
transform: rotate(-90deg);
}
.section-divider {
border-top: 2px solid #e0e0e0;
margin: 30px 0;
}
.section-title {
font-size: 18px;
color: #333;
margin-bottom: 15px;
font-weight: 600;
}
.params-section {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
.param-row {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 10px;
margin-bottom: 10px;
}
.param-input {
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 13px;
}
.param-remove {
padding: 8px 12px;
background-color: #BC2B72;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.param-remove:hover {
background-color: #a02460;
}
.param-add {
padding: 8px 16px;
background-color: #002F54;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
width: auto;
margin-top: 5px;
}
.param-add:hover {
background-color: #001a30;
}
.body-section {
display: none;
}
.body-section.active {
display: block;
}
.helper-text {
font-size: 12px;
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>OAuth2 API Tester</h1>
<p class="subtitle">Testen Sie APIs mit OAuth2-Authentifizierung</p>
<div class="info-section">
<h3>Funktionen:</h3>
<p>
• Unterstützt alle HTTP-Methoden (GET, POST, PUT, DELETE, PATCH)<br>
• Query-Parameter für alle Methoden<br>
• Request-Body für POST, PUT und PATCH<br>
• OAuth2 Client Credentials Flow
</p>
</div>
<form id="testForm">
<!-- OAuth2 Konfiguration -->
<div class="section-title">OAuth2 Konfiguration</div>
<div class="form-group">
<label for="tokenUrl">Token-URL</label>
<input type="url" id="tokenUrl" required placeholder="z.B. https://api.example.com/oauth/token">
</div>
<div class="form-group">
<label for="clientId">Client ID</label>
<input type="text" id="clientId" required placeholder="Ihre Client ID">
</div>
<div class="form-group">
<label for="clientSecret">Client Secret</label>
<input type="text" id="clientSecret" required placeholder="Ihr Client Secret">
</div>
<div class="section-divider"></div>
<!-- API Request Konfiguration -->
<div class="section-title">API Request Konfiguration</div>
<div class="form-group">
<label for="httpMethod">HTTP-Methode und Endpoint</label>
<div class="method-selector">
<select id="httpMethod" class="method-select">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
<input type="url" id="apiUrl" required placeholder="z.B. https://api.example.com/v1/resource">
</div>
</div>
<!-- Query-Parameter -->
<div class="form-group">
<label>Query-Parameter (optional)</label>
<div class="params-section">
<div id="queryParams"></div>
<button type="button" class="param-add" onclick="addQueryParam()">+ Parameter hinzufügen</button>
</div>
</div>
<!-- Request Body (nur für POST, PUT, PATCH) -->
<div class="form-group body-section" id="bodySection">
<label for="requestBody">Request-Body (JSON)</label>
<textarea id="requestBody" placeholder='z.B. {"key": "value", "number": 123}'></textarea>
<div class="helper-text">Geben Sie valides JSON ein</div>
</div>
<button type="submit" id="submitBtn">
API testen
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Authentifizierung und API-Aufruf wird durchgeführt...</p>
</div>
<div class="result" id="result">
<div class="result-header">
<div>
<span class="result-title">Ergebnis</span>
<span class="status-badge" id="statusBadge"></span>
</div>
</div>
<div id="errorSummary" class="error-summary"></div>
<div class="result-box" id="resultBox">
<div class="result-box-header" id="toggleHeader" style="display: none;">
<span class="result-box-title">Details anzeigen</span>
<span class="toggle-icon" id="toggleIcon"></span>
</div>
<pre id="resultContent"></pre>
</div>
</div>
</div>
<script>
let accessToken = null;
let queryParamCount = 0;
// HTTP-Methode Änderung Handler
document.getElementById('httpMethod').addEventListener('change', function() {
const method = this.value;
const bodySection = document.getElementById('bodySection');
// Zeige Body-Section nur für POST, PUT, PATCH
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
bodySection.classList.add('active');
} else {
bodySection.classList.remove('active');
}
});
// Query-Parameter Funktionen
function addQueryParam() {
const container = document.getElementById('queryParams');
const row = document.createElement('div');
row.className = 'param-row';
row.id = `param-${queryParamCount}`;
row.innerHTML = `
<input type="text" class="param-input" placeholder="Parameter-Name" data-param-key="${queryParamCount}">
<input type="text" class="param-input" placeholder="Wert" data-param-value="${queryParamCount}">
<button type="button" class="param-remove" onclick="removeQueryParam(${queryParamCount})">×</button>
`;
container.appendChild(row);
queryParamCount++;
}
function removeQueryParam(id) {
const row = document.getElementById(`param-${id}`);
if (row) {
row.remove();
}
}
function getQueryParams() {
const params = {};
const keys = document.querySelectorAll('[data-param-key]');
const values = document.querySelectorAll('[data-param-value]');
keys.forEach((keyInput, index) => {
const key = keyInput.value.trim();
const value = values[index].value.trim();
if (key) {
params[key] = value;
}
});
return params;
}
function buildUrlWithParams(baseUrl, params) {
if (Object.keys(params).length === 0) {
return baseUrl;
}
const url = new URL(baseUrl);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
// Form Submit Handler
document.getElementById('testForm').addEventListener('submit', async (e) => {
e.preventDefault();
const tokenUrl = document.getElementById('tokenUrl').value;
const clientId = document.getElementById('clientId').value;
const clientSecret = document.getElementById('clientSecret').value;
const httpMethod = document.getElementById('httpMethod').value;
const baseApiUrl = document.getElementById('apiUrl').value;
const queryParams = getQueryParams();
const apiUrl = buildUrlWithParams(baseApiUrl, queryParams);
let requestBody = null;
if (httpMethod === 'POST' || httpMethod === 'PUT' || httpMethod === 'PATCH') {
const bodyText = document.getElementById('requestBody').value.trim();
if (bodyText) {
try {
requestBody = JSON.parse(bodyText);
} catch (e) {
displayResult({
phase: 'validation',
message: 'Ungültiger Request-Body',
error: 'Der Request-Body muss valides JSON sein: ' + e.message
}, false);
return;
}
}
}
// UI Updates
document.getElementById('loading').classList.add('active');
document.getElementById('result').classList.remove('active');
document.getElementById('submitBtn').disabled = true;
try {
// Schritt 1: OAuth2 Token abrufen
const tokenResponse = await getAccessToken(tokenUrl, clientId, clientSecret);
if (!tokenResponse.success) {
displayResult({
phase: 'token',
message: 'Fehler beim Token-Abruf',
error: tokenResponse.error,
details: tokenResponse.details
}, false);
return;
}
accessToken = tokenResponse.access_token;
// Schritt 2: API Endpoint aufrufen
const apiResponse = await callApi(apiUrl, accessToken, httpMethod, requestBody);
if (!apiResponse.success) {
displayResult({
phase: 'api',
message: 'Fehler beim API-Aufruf',
error: apiResponse.error,
response: apiResponse
}, false);
return;
}
// Erfolg: Zeige nur die API-Antwort
displayResult(apiResponse.body, true);
} catch (error) {
displayResult({
phase: 'unknown',
message: 'Unerwarteter Fehler',
error: error.message,
stack: error.stack
}, false);
} finally {
document.getElementById('loading').classList.remove('active');
document.getElementById('submitBtn').disabled = false;
}
});
async function getAccessToken(tokenUrl, clientId, clientSecret) {
try {
let headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
// Credentials im Body (Standard-Methode)
let body = `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`;
const response = await fetch(tokenUrl, {
method: 'POST',
headers: headers,
body: body
});
let data;
try {
data = await response.json();
} catch (e) {
data = { error: 'Keine JSON-Antwort vom Server' };
}
if (!response.ok) {
return {
success: false,
error: `Token-Abruf fehlgeschlagen (${response.status}): ${JSON.stringify(data)}`,
phase: 'token',
details: {
url: tokenUrl,
responseStatus: response.status,
responseData: data
}
};
}
return {
success: true,
access_token: data.access_token,
token_type: data.token_type,
expires_in: data.expires_in
};
} catch (error) {
return {
success: false,
error: `Netzwerkfehler beim Token-Abruf: ${error.message}. Möglicherweise ein CORS-Problem.`,
phase: 'token'
};
}
}
async function callApi(apiUrl, token, method = 'GET', body = null) {
try {
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
};
const options = {
method: method,
headers: headers
};
// Body nur für POST, PUT, PATCH hinzufügen
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(body);
}
const response = await fetch(apiUrl, options);
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
return {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: data,
success: response.ok,
phase: 'api'
};
} catch (error) {
return {
success: false,
error: `API-Aufruf fehlgeschlagen: ${error.message}`,
phase: 'api'
};
}
}
function displayResult(result, success) {
const resultDiv = document.getElementById('result');
const resultBox = document.getElementById('resultBox');
const resultContent = document.getElementById('resultContent');
const statusBadge = document.getElementById('statusBadge');
const errorSummary = document.getElementById('errorSummary');
const detailsButton = document.getElementById('detailsButton');
resultDiv.classList.add('active');
if (success) {
// Erfolg: Zeige nur die API-Antwort
resultBox.classList.remove('error');
resultBox.classList.add('success');
statusBadge.textContent = 'Erfolgreich';
statusBadge.className = 'status-badge status-success';
errorSummary.style.display = 'none';
// Verstecke Toggle-Header bei Erfolg
document.getElementById('toggleHeader').style.display = 'none';
// Stelle sicher, dass resultBox und resultContent sichtbar sind
resultBox.style.display = 'block';
resultContent.style.display = 'block';
resultContent.textContent = JSON.stringify(result, null, 2);
} else {
// Fehler: Zeige Zusammenfassung mit Details-Button
resultBox.classList.remove('success');
resultBox.classList.add('error');
// Bestimme die Phase
let phaseText = '';
if (result.phase === 'token') {
phaseText = 'Token-Abruf';
statusBadge.textContent = 'Fehler beim Token-Abruf';
} else if (result.phase === 'api') {
phaseText = 'API-Aufruf';
statusBadge.textContent = 'Fehler beim API-Aufruf';
} else if (result.phase === 'validation') {
phaseText = 'Validierung';
statusBadge.textContent = 'Validierungsfehler';
} else {
phaseText = 'Unbekannt';
statusBadge.textContent = 'Fehler';
}
statusBadge.className = 'status-badge status-error';
// Verstecke Fehler-Zusammenfassung
errorSummary.style.display = 'none';
// Zeige Toggle-Header
const toggleHeader = document.getElementById('toggleHeader');
const toggleIcon = document.getElementById('toggleIcon');
toggleHeader.style.display = 'flex';
// Toggle-Funktion
toggleHeader.onclick = function() {
const isHidden = resultContent.style.display === 'none';
resultContent.style.display = isHidden ? 'block' : 'none';
toggleIcon.classList.toggle('collapsed');
toggleHeader.querySelector('.result-box-title').textContent =
isHidden ? 'Details verbergen' : 'Details anzeigen';
};
// Verstecke Details initial
resultBox.style.display = 'block';
resultContent.style.display = 'none';
resultContent.textContent = JSON.stringify(result, null, 2);
toggleIcon.classList.add('collapsed');
}
}
</script>
</body>
</html>