Files
Unifi-Portal/public/index.html
Carsten Graf 2376cf16c7 Inital Commit
2026-02-11 23:13:25 +01:00

722 lines
24 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="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Obi-WLANKenobi - Guest Portal</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Orbitron', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #000000;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
position: relative;
overflow-x: hidden;
overflow-y: auto;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse at top, rgba(13, 110, 253, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom, rgba(13, 110, 253, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.stars {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.star {
position: absolute;
background: white;
border-radius: 50%;
animation: twinkle 3s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.container {
background: rgba(10, 25, 47, 0.95);
border: 2px solid rgba(13, 110, 253, 0.5);
border-radius: 20px;
box-shadow:
0 0 60px rgba(13, 110, 253, 0.4),
inset 0 0 60px rgba(13, 110, 253, 0.1);
padding: 50px;
max-width: 550px;
width: 100%;
text-align: center;
animation: slideUp 0.5s ease-out;
position: relative;
z-index: 10;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
width: 120px;
height: 120px;
margin: 0 auto 20px;
background: radial-gradient(circle, rgba(13, 110, 253, 0.8) 0%, rgba(13, 110, 253, 0.3) 70%);
border: 3px solid #0d6efd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 60px;
box-shadow: 0 0 40px rgba(13, 110, 253, 0.8);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 40px rgba(13, 110, 253, 0.8);
transform: scale(1);
}
50% {
box-shadow: 0 0 60px rgba(13, 110, 253, 1);
transform: scale(1.02);
}
}
h1 {
color: #0d6efd;
font-size: 36px;
margin-bottom: 10px;
font-weight: 900;
text-shadow: 0 0 20px rgba(13, 110, 253, 0.8);
letter-spacing: 2px;
}
.subtitle {
color: #6ea8fe;
font-size: 16px;
margin-bottom: 15px;
line-height: 1.6;
font-style: italic;
}
.quote {
color: #a3cfff;
font-size: 14px;
margin-bottom: 40px;
font-style: italic;
opacity: 0.9;
}
.info-box {
background: rgba(13, 110, 253, 0.1);
border: 1px solid rgba(13, 110, 253, 0.3);
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
text-align: left;
box-shadow: inset 0 0 20px rgba(13, 110, 253, 0.1);
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 14px;
color: #a3cfff;
font-weight: 500;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-icon {
width: 24px;
height: 24px;
margin-right: 15px;
flex-shrink: 0;
color: #0d6efd;
filter: drop-shadow(0 0 5px rgba(13, 110, 253, 0.8));
}
.code-input {
width: 100%;
padding: 16px 20px;
margin-bottom: 20px;
background: rgba(13, 110, 253, 0.1);
border: 2px solid rgba(13, 110, 253, 0.5);
border-radius: 12px;
color: #fff;
font-family: 'Orbitron', sans-serif;
font-size: 18px;
font-weight: 700;
letter-spacing: 3px;
text-align: center;
text-transform: uppercase;
}
.code-input::placeholder {
color: rgba(163, 207, 255, 0.5);
}
.code-input:focus {
outline: none;
border-color: #0d6efd;
box-shadow: 0 0 20px rgba(13, 110, 253, 0.4);
}
.code-label {
color: #6ea8fe;
font-size: 14px;
margin-bottom: 10px;
display: block;
}
.connect-btn {
background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);
color: white;
border: 2px solid #0d6efd;
padding: 20px 40px;
font-size: 20px;
font-weight: 900;
border-radius: 12px;
cursor: pointer;
width: 100%;
transition: all 0.3s ease;
box-shadow:
0 4px 15px rgba(13, 110, 253, 0.6),
0 0 30px rgba(13, 110, 253, 0.4);
text-transform: uppercase;
letter-spacing: 2px;
font-family: 'Orbitron', sans-serif;
}
.connect-btn:hover {
transform: translateY(-2px);
box-shadow:
0 6px 25px rgba(13, 110, 253, 0.8),
0 0 50px rgba(13, 110, 253, 0.6);
background: linear-gradient(135deg, #0a58ca 0%, #084298 100%);
}
.connect-btn:active {
transform: translateY(0);
}
.connect-btn:disabled {
background: rgba(13, 110, 253, 0.3);
border-color: rgba(13, 110, 253, 0.3);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.spinner {
display: none;
width: 24px;
height: 24px;
border: 3px solid rgba(13, 110, 253, 0.3);
border-top-color: #0d6efd;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto;
box-shadow: 0 0 10px rgba(13, 110, 253, 0.5);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.message {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
font-size: 14px;
display: none;
font-weight: 600;
}
.message.success {
background: rgba(13, 110, 253, 0.2);
color: #6ea8fe;
border: 1px solid rgba(13, 110, 253, 0.5);
box-shadow: 0 0 20px rgba(13, 110, 253, 0.3);
}
.message.error {
background: rgba(220, 53, 69, 0.2);
color: #ea868f;
border: 1px solid rgba(220, 53, 69, 0.5);
box-shadow: 0 0 20px rgba(220, 53, 69, 0.3);
}
.terms {
margin-top: 30px;
font-size: 11px;
color: rgba(163, 207, 255, 0.6);
line-height: 1.6;
}
.terms a {
color: #0d6efd;
text-decoration: none;
text-shadow: 0 0 5px rgba(13, 110, 253, 0.5);
}
.terms a:hover {
text-decoration: underline;
text-shadow: 0 0 10px rgba(13, 110, 253, 0.8);
}
.tabs {
display: flex;
gap: 0;
margin-bottom: 24px;
border-bottom: 2px solid rgba(13, 110, 253, 0.3);
}
.tab {
flex: 1;
padding: 12px 16px;
background: transparent;
border: none;
color: #6ea8fe;
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
.tab:hover {
color: #a3cfff;
}
.tab.active {
color: #0d6efd;
border-bottom-color: #0d6efd;
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
.package-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 50vh;
overflow-y: auto;
}
.package-row {
background: rgba(13, 110, 253, 0.1);
border: 2px solid rgba(13, 110, 253, 0.4);
border-radius: 12px;
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.package-row:hover {
border-color: rgba(13, 110, 253, 0.6);
}
.package-row.expanded {
border-color: #0d6efd;
box-shadow: 0 0 15px rgba(13, 110, 253, 0.3);
}
.package-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
cursor: pointer;
text-align: left;
user-select: none;
}
.package-header:focus {
outline: none;
}
.package-name {
color: #0d6efd;
font-size: 16px;
font-weight: 700;
}
.package-price {
color: #6ea8fe;
font-size: 16px;
font-weight: 700;
}
.package-chevron {
margin-left: 10px;
color: #6ea8fe;
font-size: 14px;
transition: transform 0.2s;
}
.package-row.expanded .package-chevron {
transform: rotate(180deg);
}
.package-paypal {
display: none;
padding: 0 18px 18px;
border-top: 1px solid rgba(13, 110, 253, 0.3);
}
.package-row.expanded .package-paypal {
display: block;
}
.package-paypal > div {
min-width: 150px;
margin-top: 14px;
}
@media (max-width: 600px) {
.container {
padding: 30px 20px;
}
h1 {
font-size: 28px;
}
.logo {
width: 100px;
height: 100px;
font-size: 50px;
}
.connect-btn {
padding: 18px 30px;
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="stars" id="stars"></div>
<div class="container">
<div class="logo">⚔️</div>
<h1>OBI-WLANKENOBI</h1>
<p class="subtitle">A Guest Portal From A More Civilized Age</p>
<p class="quote">"Hello there! The Force is strong with this connection."</p>
<div class="info-box">
<div class="info-item">
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
<span>Faster than the Millennium Falcon</span>
</div>
<div class="info-item">
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
<span>Protected by Jedi encryption</span>
</div>
<div class="info-item">
<svg class="info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>24 parsecs of connectivity</span>
</div>
</div>
<div class="tabs">
<button type="button" class="tab active" data-tab="paypal">Mit PayPal kaufen</button>
<button type="button" class="tab" data-tab="code">Ich habe einen Code</button>
</div>
<div id="paypalPanel" class="tab-panel active">
<div id="packagesContainer"></div>
</div>
<div id="codePanel" class="tab-panel">
<label class="code-label" for="wifiCode">Zugangscode eingeben</label>
<input type="text" id="wifiCode" class="code-input" placeholder="XXXXXXXX" maxlength="8" autocomplete="off">
<button class="connect-btn" id="connectBtn" onclick="connectToWiFi()">
<span id="btnText">Use The Force</span>
<div class="spinner" id="spinner"></div>
</button>
</div>
<div class="message" id="message"></div>
<div class="terms">
By connecting, you agree this is the way. May the WiFi be with you.
</div>
</div>
<script>
// Create starfield
function createStars() {
const starsContainer = document.getElementById('stars');
const starCount = 200;
for (let i = 0; i < starCount; i++) {
const star = document.createElement('div');
star.className = 'star';
const size = Math.random() * 2 + 1;
star.style.width = size + 'px';
star.style.height = size + 'px';
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.animationDelay = Math.random() * 3 + 's';
star.style.animationDuration = (Math.random() * 3 + 2) + 's';
starsContainer.appendChild(star);
}
}
createStars();
// PayPal ist jetzt Standard-Tab Pakete beim Start laden
loadPackagesAndPayPal();
// Tabs
document.querySelectorAll('.tab').forEach((t) => {
t.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach((x) => x.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach((x) => x.classList.remove('active'));
t.classList.add('active');
const panel = t.dataset.tab === 'code' ? 'codePanel' : 'paypalPanel';
document.getElementById(panel).classList.add('active');
if (t.dataset.tab === 'paypal') loadPackagesAndPayPal();
});
});
let paypalLoaded = false;
function togglePackage(row) {
const wasExpanded = row.classList.contains('expanded');
document.querySelectorAll('.package-row').forEach((r) => r.classList.remove('expanded'));
document.querySelectorAll('.package-header[aria-expanded]').forEach((h) => h.setAttribute('aria-expanded', 'false'));
if (!wasExpanded) {
row.classList.add('expanded');
row.querySelector('.package-header').setAttribute('aria-expanded', 'true');
}
}
async function loadPackagesAndPayPal() {
const container = document.getElementById('packagesContainer');
container.innerHTML = '<p style="color:#6ea8fe">Lade Pakete...</p>';
try {
const [pkgsRes, configRes] = await Promise.all([
fetch('/api/packages'),
fetch('/api/paypal/config')
]);
const { packages } = await pkgsRes.json();
const { clientId } = await configRes.json();
if (!packages || packages.length === 0) {
container.innerHTML = '<p style="color:#6ea8fe">Derzeit keine Pakete verfügbar.</p>';
return;
}
if (!clientId) {
container.innerHTML = '<p style="color:#ea868f">PayPal ist nicht konfiguriert.</p>';
return;
}
container.innerHTML = '';
container.className = 'package-list';
packages.forEach((pkg) => {
const row = document.createElement('div');
row.className = 'package-row';
row.dataset.packageId = pkg.id;
row.innerHTML = `
<div class="package-header" role="button" tabindex="0" aria-expanded="false">
<span class="package-name">${pkg.name}</span>
<span>
<span class="package-price">${pkg.price.toFixed(2)} ${pkg.currency}</span>
<span class="package-chevron">▼</span>
</span>
</div>
<div class="package-paypal" id="paypal-btn-${pkg.id}"></div>
`;
const header = row.querySelector('.package-header');
header.addEventListener('click', () => togglePackage(row));
header.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePackage(row); } });
container.appendChild(row);
});
if (!paypalLoaded) {
const script = document.createElement('script');
script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}&currency=EUR`;
script.async = true;
script.onload = () => renderPayPalButtons(packages);
document.head.appendChild(script);
paypalLoaded = true;
} else {
renderPayPalButtons(packages);
}
} catch (e) {
container.innerHTML = '<p style="color:#ea868f">Fehler beim Laden.</p>';
}
}
function renderPayPalButtons(packages) {
if (typeof paypal === 'undefined') return;
packages.forEach((pkg) => {
const el = document.getElementById('paypal-btn-' + pkg.id);
if (!el || el.hasChildNodes()) return;
paypal.Buttons({
createOrder: async () => {
const r = await fetch('/api/paypal/create-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packageId: pkg.id })
});
const data = await r.json();
if (!data.success) throw new Error(data.error || 'Order failed');
return data.orderId;
},
onApprove: async (data) => {
const message = document.getElementById('message');
try {
const r = await fetch('/api/paypal/capture-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId: data.orderID })
});
const result = await r.json();
if (result.success) {
message.className = 'message success';
message.textContent = 'Bezahlung erfolgreich! Du wirst verbunden...';
message.style.display = 'block';
setTimeout(() => {
window.location.href = 'http://captive.apple.com/hotspot-detect.html';
}, 1500);
setTimeout(() => {
window.location.href = result.redirectUrl || 'https://google.com';
}, 3500);
} else {
throw new Error(result.error);
}
} catch (err) {
message.className = 'message error';
message.textContent = 'Fehler: ' + (err.message || 'Bezahlung fehlgeschlagen.');
message.style.display = 'block';
}
},
onError: (err) => {
const message = document.getElementById('message');
message.className = 'message error';
message.textContent = 'PayPal Fehler: ' + (err.message || 'Unbekannt');
message.style.display = 'block';
}
}).render('#paypal-btn-' + pkg.id);
});
}
async function connectToWiFi() {
const btn = document.getElementById('connectBtn');
const btnText = document.getElementById('btnText');
const spinner = document.getElementById('spinner');
const message = document.getElementById('message');
const codeInput = document.getElementById('wifiCode');
const code = (codeInput && codeInput.value || '').trim();
if (!code) {
message.className = 'message error';
message.textContent = 'Please enter your access code.';
message.style.display = 'block';
return;
}
// Disable button and show loading
btn.disabled = true;
btnText.style.display = 'none';
spinner.style.display = 'block';
message.style.display = 'none';
try {
const response = await fetch('/authorize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
});
const data = await response.json();
if (data.success) {
// Show success message
message.className = 'message success';
message.textContent = '✨ You are now connected to the Force! Redirecting...';
message.style.display = 'block';
// For iOS/macOS: redirect to Apple's captive portal detection URL
// This triggers the CNA (Captive Network Assistant) to close
setTimeout(() => {
// Try Apple's captive portal URL first (closes iOS popup)
window.location.href = 'http://captive.apple.com/hotspot-detect.html';
}, 1500);
// Fallback: if still here after 3s, try the redirect URL
setTimeout(() => {
window.location.href = data.redirectUrl || 'https://www.google.com';
}, 3500);
} else {
throw new Error(data.message);
}
} catch (error) {
// Show error message
message.className = 'message error';
message.textContent = '⚠️ ' + (error.message || 'The Force is not strong with this one. Please try again.');
message.style.display = 'block';
// Re-enable button
btn.disabled = false;
btnText.style.display = 'block';
spinner.style.display = 'none';
}
}
</script>
</body>
</html>