Files

272 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>Public WiFi Portal</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; background: #f5f6f8; color: #333; margin: 0; }
.container { max-width: 900px; margin: 30px auto; padding: 0 20px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.logo { display: flex; align-items: center; gap: 12px; }
.logo-icon { width: 38px; height: 38px; background: #e6f2fb; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
.logo-icon svg { width: 22px; height: 22px; fill: #0078d4; }
.logo-text { display: flex; flex-direction: column; }
.logo-title { font-size: 18px; font-weight: 700; color: #222; }
.logo-subtitle { font-size: 12px; color: #777; }
.connection-status { display: flex; align-items: center; gap: 8px; }
.status-dot { width: 10px; height: 10px; background: #22c55e; border-radius: 50%; box-shadow: 0 0 0 3px rgba(34,197,94,0.2); }
h1 { font-size: 24px; margin: 10px 0 5px; }
.welcome-text { background: #fff; border: 1px solid #eaeaea; border-radius: 8px; padding: 15px; margin-bottom: 15px; }
.network-info { background: #fff; border: 1px solid #eaeaea; border-radius: 8px; margin-bottom: 20px; }
.network-info-header { display: flex; align-items: center; gap: 10px; padding: 12px 15px; border-bottom: 1px solid #f0f0f0; }
.network-info-icon { width: 20px; height: 20px; fill: #0078d4; }
.network-details { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 15px; }
.detail-item { display: flex; align-items: center; gap: 10px; }
.detail-label { width: 140px; font-size: 13px; color: #666; }
.detail-value { font-size: 14px; font-weight: 600; color: #222; }
.signal-strength { display: flex; align-items: flex-end; gap: 3px; height: 16px; }
.signal-bar { width: 6px; background: #0078d4; border-radius: 3px; }
.signal-bar.empty { background: #e0e0e0; }
.auth-section { background: #fff; border: 1px solid #eaeaea; border-radius: 8px; margin-top: 20px; }
.tabs { display: flex; border-bottom: 1px solid #f0f0f0; }
.tab { flex: 1; text-align: center; padding: 12px; cursor: pointer; font-weight: 600; color: #666; transition: all 0.2s ease; }
.tab.active { color: #0078d4; border-bottom: 2px solid #0078d4; background: #f7fbff; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.form-row { display: flex; gap: 12px; }
.form-group { margin-bottom: 12px; }
.form-label { display: block; font-size: 13px; color: #555; margin-bottom: 5px; }
.form-control { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; transition: all 0.2s ease; }
.form-control:focus { border-color: #0078d4; outline: none; box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1); }
.btn { display: inline-block; padding: 10px 16px; font-size: 14px; font-weight: 500; text-align: center; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; border: none; width: 100%; }
.btn-primary { background: #0078d4; color: #fff; }
.btn-primary:hover { background: #006cc1; }
.social-login { margin-top: 20px; text-align: center; display: none; }
.policy-notice { margin-top: 25px; padding-top: 20px; border-top: 1px solid #eaeaea; font-size: 12px; color: #888; line-height: 1.5; }
.policy-link { color: #0078d4; text-decoration: none; }
.policy-link:hover { text-decoration: underline; }
.footer { margin-top: 30px; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #888; }
.loading-overlay { position: fixed; inset: 0; background: rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center; flex-direction: column; z-index: 1000; opacity: 0; visibility: hidden; transition: all 0.3s ease; }
.loading-overlay.show { opacity: 1; visibility: visible; }
.spinner { width: 40px; height: 40px; border: 4px solid rgba(0,120,212,0.2); border-radius: 50%; border-top-color: #0078d4; animation: spin 1s linear infinite; margin-bottom: 15px; }
.loading-text { font-size: 16px; color: #333; font-weight: 500; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.error-message { color: #d32f2f; font-size: 14px; margin-top: 5px; display: none; }
.error-message.show { display: block; }
</style>
</head>
<body>
<div class=\"loading-overlay\" id=\"loadingOverlay\">
<div class=\"spinner\"></div>
<div class=\"loading-text\">Authenticating...</div>
</div>
<div class=\"container\">
<div class=\"header\">
<div class=\"logo\">
<div class=\"logo-icon\">
<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M12,21L15.6,16.2C14.6,15.45 13.35,15 12,15C10.65,15 9.4,15.45 8.4,16.2L12,21M12,3C7.95,3 4.21,4.34 1.2,6.6L3,9C5.5,7.12 8.62,6 12,6C15.38,6 18.5,7.12 21,9L22.8,6.6C19.79,4.34 16.05,3 12,3M12,9C9.3,9 6.81,9.89 4.8,11.4L6.6,13.8C8.1,12.67 9.97,12 12,12C14.03,12 15.9,12.67 17.4,13.8L19.2,11.4C17.19,9.89 14.7,9 12,9Z\"/></svg>
</div>
<div class=\"logo-text\">
<span class=\"logo-title\" id=\"brandTitle\">Public WiFi</span>
<span class=\"logo-subtitle\" id=\"brandSubtitle\">Wireless Network</span>
</div>
</div>
<div class=\"connection-status\">
<span class=\"status-dot\"></span>
<span>Connected</span>
</div>
</div>
<h1>Authentication Required</h1>
<div class=\"welcome-text\">
<p>Welcome to this public WiFi portal. Please authenticate to access the internet. This secure portal helps maintain network quality and security for all users.</p>
</div>
<div class=\"network-info\">
<div class=\"network-info-header\">
<svg class=\"network-info-icon\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M12,21L15.6,16.2C14.6,15.45 13.35,15 12,15C10.65,15 9.4,15.45 8.4,16.2L12,21M12,3C7.95,3 4.21,4.34 1.2,6.6L3,9C5.5,7.12 8.62,6 12,6C15.38,6 18.5,7.12 21,9L22.8,6.6C19.79,4.34 16.05,3 12,3M12,9C9.3,9 6.81,9.89 4.8,11.4L6.6,13.8C8.1,12.67 9.97,12 12,12C14.03,12 15.9,12.67 17.4,13.8L19.2,11.4C17.19,9.89 14.7,9 12,9Z\"/></svg>
<span class=\"network-info-title\">Network Information</span>
</div>
<div class=\"network-details\">
<div class=\"detail-item\">
<span class=\"detail-label\">SSID:</span>
<span class=\"detail-value\" id=\"ssidName\">Unknown</span>
</div>
<div class=\"detail-item\">
<span class=\"detail-label\">Signal Strength:</span>
<div class=\"signal-strength\">
<div class=\"signal-bar\" style=\"height:5px\"></div>
<div class=\"signal-bar\" style=\"height:8px\"></div>
<div class=\"signal-bar\" style=\"height:12px\"></div>
<div class=\"signal-bar\" style=\"height:16px\"></div>
<div class=\"signal-bar empty\" style=\"height:16px\"></div>
</div>
</div>
<div class=\"detail-item\">
<span class=\"detail-label\">Security Type:</span>
<span class=\"detail-value\" id=\"securityType\">Unknown</span>
</div>
<div class=\"detail-item\">
<span class=\"detail-label\">Portal IP:</span>
<span class=\"detail-value\" id=\"ipAddress\">Loading...</span>
</div>
</div>
</div>
<div class=\"auth-section\">
<div class=\"tabs\">
<div class=\"tab active\" data-tab=\"emailTab\">Email Login</div>
<div class=\"tab\" data-tab=\"residentTab\">Resident ID</div>
<div class=\"tab\" data-tab=\"businessTab\">Business Account</div>
</div>
<div class=\"tab-content active\" id=\"emailTab\">
<div class=\"form-group\">
<label for=\"email\" class=\"form-label\">Email</label>
<input type=\"email\" id=\"email\" class=\"form-control\" placeholder=\"you@example.com\" required />
</div>
<div class=\"form-group\">
<label for=\"password\" class=\"form-label\">Password</label>
<input type=\"password\" id=\"password\" class=\"form-control\" placeholder=\"Enter your password\" required />
</div>
<div id=\"error-message\" class=\"error-message\"></div>
<button class=\"btn btn-primary\" onclick=\"authenticate()\">Sign In</button>
<div class=\"policy-notice\">
By signing in, you agree to the <a class=\"policy-link\" href=\"#\">acceptable use policy</a> and <a class=\"policy-link\" href=\"#\">privacy policy</a>.
</div>
</div>
<div class=\"tab-content\" id=\"residentTab\">
<div class=\"form-row\">
<div class=\"form-group\" style=\"flex:1\">
<label for=\"resident-id\" class=\"form-label\">Resident ID</label>
<input type=\"text\" id=\"resident-id\" class=\"form-control\" placeholder=\"ID Number\" />
</div>
<div class=\"form-group\" style=\"flex:1\">
<label for=\"resident-password\" class=\"form-label\">Password</label>
<input type=\"password\" id=\"resident-password\" class=\"form-control\" placeholder=\"Enter password\" />
</div>
</div>
<button class=\"btn btn-primary\" onclick=\"authenticateResident()\">Sign In</button>
</div>
<div class=\"tab-content\" id=\"businessTab\">
<div class=\"form-row\">
<div class=\"form-group\" style=\"flex:1\">
<label for=\"business-id\" class=\"form-label\">Business ID</label>
<input type=\"text\" id=\"business-id\" class=\"form-control\" placeholder=\"Account ID\" />
</div>
<div class=\"form-group\" style=\"flex:1\">
<label for=\"business-password\" class=\"form-label\">Password</label>
<input type=\"password\" id=\"business-password\" class=\"form-control\" placeholder=\"Enter password\" />
</div>
</div>
<button class=\"btn btn-primary\" onclick=\"authenticateBusiness()\">Sign In</button>
</div>
<div class=\"social-login\">
<!-- Social login intentionally hidden for captive portal -->
</div>
<div class=\"footer\">
<div> <span id=\"brandYear\"></span> <span id=\"brandFooter\">Public WiFi</span></div>
<div>Fair Use | Privacy</div>
</div>
</div>
</div>
<script>
const showLoading = () => document.getElementById('loadingOverlay').classList.add('show');
const hideLoading = () => document.getElementById('loadingOverlay').classList.remove('show');
function showError(message) {
const el = document.getElementById('error-message');
el.textContent = message || 'Authentication failed. Please try again.';
el.classList.add('show');
hideLoading();
}
async function authenticate() {
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value.trim();
document.getElementById('error-message').classList.remove('show');
if (!email || !password) { return showError('Please enter email and password.'); }
showLoading();
// Submit to firmware /login endpoint using a standard form POST (works across captive portals)
const form = document.createElement('form');
form.method = 'POST';
form.action = '/login';
const u = document.createElement('input'); u.type = 'hidden'; u.name = 'username'; u.value = email;
const p = document.createElement('input'); p.type = 'hidden'; p.name = 'password'; p.value = password;
form.appendChild(u); form.appendChild(p);
document.body.appendChild(form);
form.submit();
}
async function authenticateResident() {
const id = document.getElementById('resident-id').value.trim();
const pass = document.getElementById('resident-password').value.trim();
if (!id || !pass) { return showError('Please enter your Resident ID and password.'); }
showLoading();
const form = document.createElement('form');
form.method = 'POST';
form.action = '/login';
const u = document.createElement('input'); u.type = 'hidden'; u.name = 'username'; u.value = id;
const p = document.createElement('input'); p.type = 'hidden'; p.name = 'password'; p.value = pass;
form.appendChild(u); form.appendChild(p);
document.body.appendChild(form);
form.submit();
}
async function authenticateBusiness() {
const id = document.getElementById('business-id').value.trim();
const pass = document.getElementById('business-password').value.trim();
if (!id || !pass) { return showError('Please enter your Business ID and password.'); }
showLoading();
const form = document.createElement('form');
form.method = 'POST';
form.action = '/login';
const u = document.createElement('input'); u.type = 'hidden'; u.name = 'username'; u.value = id;
const p = document.createElement('input'); p.type = 'hidden'; p.name = 'password'; p.value = pass;
form.appendChild(u); form.appendChild(p);
document.body.appendChild(form);
form.submit();
}
async function populateBranding() {
try {
const res = await fetch('/api/config');
if (res.ok) {
const cfg = await res.json();
const brand = cfg.branding_name || 'Public WiFi';
const org = cfg.organization || 'Wireless Network';
document.title = brand + ' Portal';
document.getElementById('brandTitle').textContent = brand;
document.getElementById('brandSubtitle').textContent = org;
document.getElementById('brandFooter').textContent = brand;
}
} catch (e) { /* ignore */ }
document.getElementById('brandYear').textContent = new Date().getFullYear();
}
async function populateNetwork() {
try {
const res = await fetch('/api/status');
if (res.ok) {
const s = await res.json();
if (s.target_ssid) document.getElementById('ssidName').textContent = s.target_ssid;
if (s.ap_ip) document.getElementById('ipAddress').textContent = s.ap_ip; else document.getElementById('ipAddress').textContent = 'Unavailable';
} else {
document.getElementById('ipAddress').textContent = 'Unavailable';
}
} catch (e) {
document.getElementById('ipAddress').textContent = 'Unavailable';
}
}
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.getAttribute('data-tab')).classList.add('active');
});
});
window.onload = () => { populateBranding(); populateNetwork(); };
</script>
</body>
</html>