XIVLauncherRemoteOTP/static/index.html

386 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XIVLauncher Remote OTP</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Global KDE Oxygen SVG Gradient & Filter Defs -->
<svg style="display: none;">
<defs>
<linearGradient id="goldGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffe673" />
<stop offset="30%" stop-color="#ffd03b" />
<stop offset="70%" stop-color="#e6a817" />
<stop offset="100%" stop-color="#b87a00" />
</linearGradient>
<linearGradient id="silverGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f5f5f5" />
<stop offset="50%" stop-color="#cfcfcf" />
<stop offset="100%" stop-color="#8c8c8c" />
</linearGradient>
<linearGradient id="glassShine" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.85" />
<stop offset="50%" stop-color="#ffffff" stop-opacity="0.15" />
<stop offset="51%" stop-color="#ffffff" stop-opacity="0.0" />
</linearGradient>
<linearGradient id="glassShineRed" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.0" />
</linearGradient>
<radialGradient id="blueBall" cx="30%" cy="30%" r="70%">
<stop offset="0%" stop-color="#a4ebff" />
<stop offset="30%" stop-color="#29b6f6" />
<stop offset="75%" stop-color="#0288d1" />
<stop offset="100%" stop-color="#01579b" />
</radialGradient>
<radialGradient id="redBall" cx="30%" cy="30%" r="70%">
<stop offset="0%" stop-color="#ff9999" />
<stop offset="30%" stop-color="#ef4444" />
<stop offset="75%" stop-color="#dc2626" />
<stop offset="100%" stop-color="#991b1b" />
</radialGradient>
<radialGradient id="greenBall" cx="30%" cy="30%" r="70%">
<stop offset="0%" stop-color="#c2f0c2" />
<stop offset="30%" stop-color="#4caf50" />
<stop offset="75%" stop-color="#388e3c" />
<stop offset="100%" stop-color="#1b5e20" />
</radialGradient>
<linearGradient id="blueArrowGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#9ddcff" />
<stop offset="100%" stop-color="#1d73c9" />
</linearGradient>
<linearGradient id="pencilYellow" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#ffe066" />
<stop offset="50%" stop-color="#ffcc00" />
<stop offset="100%" stop-color="#cc9900" />
</linearGradient>
<linearGradient id="goldKeyGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffe680" />
<stop offset="40%" stop-color="#ffd42a" />
<stop offset="80%" stop-color="#d4aa00" />
<stop offset="100%" stop-color="#806600" />
</linearGradient>
<linearGradient id="paperGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#e2e8f0" />
</linearGradient>
</defs>
</svg>
<div class="container">
<header>
<div class="logo">
<svg class="icon-oxygen" width="32" height="32" viewBox="0 0 48 48" fill="none">
<path d="M14 20V12a10 10 0 1 1 20 0v8" stroke="url(#silverGrad)" stroke-width="6" stroke-linecap="round" />
<rect x="8" y="18" width="32" height="26" rx="4" fill="url(#goldGrad)" stroke="#8f5900" stroke-width="1.5" />
<rect x="9" y="19" width="30" height="12" rx="3" fill="url(#glassShine)" />
<circle cx="24" cy="30" r="3" fill="#201000" />
<path d="M22 30h4l2 8h-8z" fill="#201000" />
</svg>
<h1>XIVLauncher Remote OTP</h1>
</div>
<div class="header-actions">
<button id="help-btn" class="btn btn-icon" title="Firewall Help">
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="21" fill="url(#blueBall)" stroke="#013b68" stroke-width="1.5" />
<ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)" />
<path d="M20 18c0-3 2-5 5-5s5 2 5 5c0 2-1 3-3 4s-2 2-2 4v1m0 3v2" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button id="logout-btn" class="btn btn-icon btn-logout" title="Logout" style="display: none;">
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="21" fill="url(#redBall)" stroke="#7f1d1d" stroke-width="1.5" />
<ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)" />
<path d="M24 12v12m-8-8a11 11 0 1 0 16 0" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round" />
</svg>
</button>
</div>
</header>
<nav id="main-nav" class="main-nav" style="display: none;">
<button class="nav-item active" data-tab="instances">Instances</button>
<button class="nav-item" data-tab="otp-secrets">OTP Secrets</button>
<button class="nav-item" id="nav-admin" data-tab="admin" style="display: none;">Admin</button>
<button class="nav-item" data-tab="profile">Profile</button>
</nav>
<main>
<div id="instances-section" class="tab-content active">
<div class="card instances-card">
<div class="instances-header">
<h2>Managed Instances</h2>
<div class="actions">
<button class="btn btn-primary btn-sm" id="show-add-btn">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none" style="margin-right: 4px;">
<circle cx="24" cy="24" r="21" fill="url(#greenBall)" stroke="#0d3c12" stroke-width="1.5" />
<path d="M24 14v20M14 24h20" stroke="#ffffff" stroke-width="5" stroke-linecap="round" />
</svg>
Add
</button>
<button class="btn btn-icon" id="refresh-btn" title="Refresh">
<svg class="icon-oxygen" width="18" height="18" viewBox="0 0 48 48" fill="none">
<path d="M38 24a14 14 0 1 1-4.1-9.9L38 18M38 10v8h-8" stroke="url(#blueArrowGrad)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
</div>
<div id="instances-list" class="instances-list">
<!-- Instances injected via JS -->
</div>
</div>
</div>
<div id="otp-secrets-section" class="tab-content">
<div class="card otp-secrets-card">
<div class="instances-header">
<h2>OTP Secrets</h2>
<button class="btn btn-primary btn-sm" id="show-add-otp-btn">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none" style="margin-right: 4px;">
<circle cx="24" cy="24" r="21" fill="url(#greenBall)" stroke="#0d3c12" stroke-width="1.5" />
<path d="M24 14v20M14 24h20" stroke="#ffffff" stroke-width="5" stroke-linecap="round" />
</svg>
Add Secret
</button>
</div>
<div id="otp-secrets-list" class="otp-secrets-list">
<!-- OTP Secrets injected via JS -->
</div>
</div>
</div>
<div id="admin-section" class="tab-content">
<div class="card user-management-card">
<div class="instances-header">
<h2>User Management</h2>
<button class="btn btn-primary btn-sm" id="show-add-user-btn">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none" style="margin-right: 4px;">
<circle cx="24" cy="24" r="21" fill="url(#greenBall)" stroke="#0d3c12" stroke-width="1.5" />
<path d="M24 14v20M14 24h20" stroke="#ffffff" stroke-width="5" stroke-linecap="round" />
</svg>
Add User
</button>
</div>
<div id="users-list" class="users-list">
<!-- Users injected via JS -->
</div>
</div>
</div>
<div id="profile-section" class="tab-content">
<div class="card profile-card">
<h2>My Profile</h2>
<p class="helper-text">Update your account details below.</p>
<form id="change-username-form" class="profile-form">
<div class="form-group">
<label for="profile-username">Username</label>
<input type="text" id="profile-username" placeholder="Enter new username" required>
</div>
<button type="submit" class="btn btn-primary">Update Username</button>
</form>
<div class="divider"></div>
<form id="change-password-form" class="profile-form">
<div class="form-group">
<label for="profile-new-password">Password</label>
<input type="password" id="profile-new-password" placeholder="Enter new password" required>
</div>
<button type="submit" class="btn btn-primary">Update Password</button>
</form>
<div id="profile-success" class="success-msg" style="display: none;">Profile updated successfully!</div>
</div>
</div>
<div id="logs-toggle-container" class="logs-toggle-container">
<label class="switch-container">
<input type="checkbox" id="logs-toggle">
<span class="switch-slider"></span>
<span class="switch-label">Show Detailed System Logs</span>
</label>
</div>
<div id="logs-section" class="card logs-card" style="display: none;">
<div class="logs-header">
<h3>System Logs</h3>
<div class="logs-actions">
<button class="btn btn-icon btn-sm" id="logs-refresh-btn" title="Refresh Logs">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none">
<path d="M38 24a14 14 0 1 1-4.1-9.9L38 18M38 10v8h-8" stroke="url(#blueArrowGrad)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
</div>
<div id="logs-viewer" class="logs-viewer"></div>
</div>
</main>
</div>
<!-- Modals -->
<div id="add-instance-modal" class="modal">
<div class="modal-content card">
<div class="modal-header">
<h2>Add Instance</h2>
<button class="btn-close">&times;</button>
</div>
<form id="add-form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" placeholder="e.g. Steam Deck" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="ip">IP Address</label>
<input type="text" id="ip" placeholder="192.168.1.100" required>
</div>
<div class="form-group">
<label for="port">Port</label>
<input type="number" id="port" value="4646" required>
</div>
</div>
<div class="form-group">
<label for="otp-secret-id">OTP Secret</label>
<select id="otp-secret-id" required>
<option value="" disabled selected>Select an OTP secret</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add Instance</button>
</form>
</div>
</div>
<div id="add-otp-secret-modal" class="modal">
<div class="modal-content card">
<div class="modal-header">
<h2 id="otp-modal-title">Add OTP Secret</h2>
<button class="btn-close">&times;</button>
</div>
<form id="otp-secret-form">
<div class="form-group">
<label for="otp-name">Name</label>
<input type="text" id="otp-name" placeholder="e.g. Main Account" required>
</div>
<div class="form-group">
<label for="otp-secret">Secret</label>
<input type="password" id="otp-secret" placeholder="Base32 Secret or otpauth:// URL" required>
</div>
<button type="submit" class="btn btn-primary" id="otp-save-btn">Add Secret</button>
</form>
</div>
</div>
<div id="add-user-modal" class="modal">
<div class="modal-content card">
<div class="modal-header">
<h2>Add User</h2>
<button class="btn-close">&times;</button>
</div>
<form id="create-user-form" class="user-create-form">
<div class="form-group">
<label for="new-username">Username</label>
<input type="text" id="new-username" placeholder="Enter username" required>
</div>
<div class="form-group">
<label for="new-password">Password</label>
<input type="password" id="new-password" placeholder="Enter password" required>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" id="new-is-admin">
<span class="checkbox-label">Grant Administrator Privileges</span>
</label>
</div>
<button type="submit" class="btn btn-primary">Create User</button>
</form>
</div>
</div>
<div id="help-modal" class="modal">
<div class="modal-content card">
<div class="modal-header">
<h2>Firewall Help</h2>
<button class="btn-close">&times;</button>
</div>
<p class="helper-text">Run these commands on the machine running XIVLauncher to allow incoming connections on the specified port.</p>
<div class="code-block">
<span class="code-label">UFW</span>
<div class="code-wrapper">
<code id="ufw-cmd">sudo ufw allow from localhost to any port 4646 proto tcp</code>
<button class="btn-copy" onclick="copyToClipboard('ufw-cmd')" title="Copy to clipboard">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none"><rect x="14" y="6" width="26" height="32" rx="2" fill="url(#paperGrad)" stroke="#2e8bdc" stroke-width="2" /><rect x="8" y="12" width="26" height="32" rx="2" fill="url(#paperGrad)" stroke="#2e8bdc" stroke-width="2" /><line x1="14" y1="20" x2="28" y2="20" stroke="#94a3b8" stroke-width="2" /><line x1="14" y1="26" x2="28" y2="26" stroke="#94a3b8" stroke-width="2" /><line x1="14" y1="32" x2="24" y2="32" stroke="#94a3b8" stroke-width="2" /></svg>
</button>
</div>
</div>
<div class="code-block">
<span class="code-label">iptables</span>
<div class="code-wrapper">
<code id="iptables-cmd">sudo iptables -I INPUT -p tcp -s localhost --dport 4646 -j ACCEPT</code>
<button class="btn-copy" onclick="copyToClipboard('iptables-cmd')" title="Copy to clipboard">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none"><rect x="14" y="6" width="26" height="32" rx="2" fill="url(#paperGrad)" stroke="#2e8bdc" stroke-width="2" /><rect x="8" y="12" width="26" height="32" rx="2" fill="url(#paperGrad)" stroke="#2e8bdc" stroke-width="2" /><line x1="14" y1="20" x2="28" y2="20" stroke="#94a3b8" stroke-width="2" /><line x1="14" y1="26" x2="28" y2="26" stroke="#94a3b8" stroke-width="2" /><line x1="14" y1="32" x2="24" y2="32" stroke="#94a3b8" stroke-width="2" /></svg>
</button>
</div>
</div>
<div class="code-block">
<span class="code-label">Windows (PowerShell)</span>
<div class="code-wrapper">
<code id="win-cmd">New-NetFirewallRule -DisplayName "XIVLauncher OTP" -Direction Inbound -RemoteAddress localhost -LocalPort 4646 -Protocol TCP -Action Allow</code>
<button class="btn-copy" onclick="copyToClipboard('win-cmd')" title="Copy to clipboard">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none"><rect x="14" y="6" width="26" height="32" rx="2" fill="url(#paperGrad)" stroke="#2e8bdc" stroke-width="2" /><rect x="8" y="12" width="26" height="32" rx="2" fill="url(#paperGrad)" stroke="#2e8bdc" stroke-width="2" /><line x1="14" y1="20" x2="28" y2="20" stroke="#94a3b8" stroke-width="2" /><line x1="14" y1="26" x2="28" y2="26" stroke="#94a3b8" stroke-width="2" /><line x1="14" y1="32" x2="24" y2="32" stroke="#94a3b8" stroke-width="2" /></svg>
</button>
</div>
</div>
</div>
</div>
<div id="login-overlay" class="login-overlay">
<div class="login-card">
<div class="logo">
<svg class="icon-oxygen" width="32" height="32" viewBox="0 0 48 48" fill="none">
<path d="M14 20V12a10 10 0 1 1 20 0v8" stroke="url(#silverGrad)" stroke-width="6" stroke-linecap="round" />
<rect x="8" y="18" width="32" height="26" rx="4" fill="url(#goldGrad)" stroke="#8f5900" stroke-width="1.5" />
<rect x="9" y="19" width="30" height="12" rx="3" fill="url(#glassShine)" />
<circle cx="24" cy="30" r="3" fill="#201000" />
<path d="M22 30h4l2 8h-8z" fill="#201000" />
</svg>
<h2>Login</h2>
</div>
<form id="login-form">
<div class="form-group">
<label for="login-username">Username</label>
<input type="text" id="login-username" placeholder="Enter your username" required autocomplete="username">
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" placeholder="Enter your password" required autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<div id="login-error" class="login-error" style="display: none;">Invalid username or password</div>
<div class="divider" style="margin: 1.5rem 0;"></div>
<div class="login-firewall-section">
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.5rem;">Firewall Connection Info</h3>
<p class="helper-text" style="font-size: 0.75rem; margin-bottom: 0.75rem;">To allow this server to send OTP codes, ensure your XIVLauncher machine allows connections from:</p>
<div class="firewall-ip-display" style="display: flex; align-items: center; justify-content: space-between; background-color: var(--bg-color); border: 1px solid var(--border-color); border-radius: 6px; padding: 0.5rem 0.75rem; margin-bottom: 0.75rem;">
<span class="ip-label" style="font-size: 0.75rem; color: var(--text-muted); font-weight: 500;">Host IP</span>
<code id="login-firewall-ip" style="font-family: monospace; font-size: 0.875rem; color: #34d399;">Loading...</code>
</div>
<button type="button" id="login-show-help-btn" class="btn btn-secondary btn-sm" style="width: 100%; display: flex; justify-content: center; align-items: center; gap: 0.375rem; font-size: 0.75rem; padding: 0.5rem;">
<svg class="icon-oxygen" width="14" height="14" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="21" fill="url(#blueBall)" stroke="#013b68" stroke-width="1.5" />
<ellipse cx="24" cy="11" rx="14" ry="7" fill="url(#glassShineRed)" />
<path d="M20 18c0-3 2-5 5-5s5 2 5 5c0 2-1 3-3 4s-2 2-2 4v1m0 3v2" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Show Setup Commands
</button>
</div>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>