Initial commit: Premium Gold Theme with Dynamic QR and Multi-language support
57
alogin.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<!-- <meta http-equiv="refresh" content="2; url=$(link-redirect)"> -->
|
||||||
|
<title>Redirect</title>
|
||||||
|
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ambient-bg">
|
||||||
|
<div class="orb orb-1"></div>
|
||||||
|
<div class="orb orb-2"></div>
|
||||||
|
<div class="orb orb-3"></div>
|
||||||
|
</div>
|
||||||
|
<script language="JavaScript">
|
||||||
|
setTimeout(function() {
|
||||||
|
location.href = '$(link-redirect)';
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
<div class="container" style="text-align: center;">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
</header>
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="border-color: #50e3c2; margin-top: 1.5rem;">
|
||||||
|
<h2 class="mb-4 flex-center" style="color: #50e3c2;">
|
||||||
|
<img src="svg/check-circle.svg" class="icon" data-asset="icon_success" style="width:24px; height:24px; margin-right:8px;">
|
||||||
|
<span data-i18n="success_title">Success</span>
|
||||||
|
</h2>
|
||||||
|
<p class="mb-4 text-sm" style="color: var(--fg-secondary);" data-i18n="success_msg">
|
||||||
|
You are now connected to the network.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm" data-i18n="redirect_msg">Redirecting...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
api.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"captive": $(if logged-in == 'yes')false$(else)true$(endif),
|
||||||
|
"user-portal-url": "$(link-login-only)",
|
||||||
|
$(if session-timeout-secs != 0)
|
||||||
|
"seconds-remaining": $(session-timeout-secs),
|
||||||
|
$(endif)
|
||||||
|
$(if remain-bytes-total)
|
||||||
|
"bytes-remaining": $(remain-bytes-total),
|
||||||
|
$(endif)
|
||||||
|
"can-extend-session": true
|
||||||
|
}
|
||||||
708
css/style.css
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
/* Vercel-inspired Design System */
|
||||||
|
:root {
|
||||||
|
--bg-primary: #000000;
|
||||||
|
--bg-secondary: #0a0a0a;
|
||||||
|
--fg-primary: #ededed;
|
||||||
|
--fg-secondary: #a1a1a1;
|
||||||
|
--accent: #fff;
|
||||||
|
--accent-glow: 0 0 40px -10px rgba(255, 255, 255, 0.3);
|
||||||
|
--border: #333;
|
||||||
|
--border-hover: #555;
|
||||||
|
--font-sans: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-mono: 'Geist Mono', 'Fira Code', monospace;
|
||||||
|
--radius: 8px;
|
||||||
|
--transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Reset */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--fg-primary);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
line-height: 1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden; /* Prevent horizontal scroll from orbs */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient Background Orbs */
|
||||||
|
.ambient-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(circle at 50% 50%, #0a0a0a 0%, #000 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(100px);
|
||||||
|
opacity: 0.12;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-1 {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
background: #fff;
|
||||||
|
top: -100px;
|
||||||
|
right: -100px;
|
||||||
|
animation: float1 30s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-2 {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
background: #444;
|
||||||
|
bottom: -100px;
|
||||||
|
left: -50px;
|
||||||
|
animation: float2 25s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-3 {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
background: #222;
|
||||||
|
top: 40%;
|
||||||
|
left: 20%;
|
||||||
|
animation: float3 20s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float1 {
|
||||||
|
0% { transform: translate(0, 0) rotate(0deg); }
|
||||||
|
100% { transform: translate(-200px, 200px) rotate(90deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float2 {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
100% { transform: translate(150px, -150px) scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float3 {
|
||||||
|
0% { transform: translate(0, 0); }
|
||||||
|
100% { transform: translate(100px, 100px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-top-color: #50e3c2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--fg-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Components */
|
||||||
|
.container {
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1.5rem; /* Increased vertical padding to prevent edge sticking */
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
text-decoration: none !important;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--fg-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #ccc;
|
||||||
|
box-shadow: var(--accent-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--border);
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
border-color: var(--fg-primary);
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover img {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs */
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--fg-primary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--fg-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global Autofill Styling for all inputs/selects/textareas */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus,
|
||||||
|
select:-webkit-autofill,
|
||||||
|
select:-webkit-autofill:hover,
|
||||||
|
select:-webkit-autofill:focus,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill:hover,
|
||||||
|
textarea:-webkit-autofill:focus {
|
||||||
|
-webkit-text-fill-color: var(--fg-primary) !important;
|
||||||
|
-webkit-box-shadow: 0 0 0px 1000px #000 inset !important;
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: var(--fg-primary);
|
||||||
|
border-bottom-color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 2rem;
|
||||||
|
background: linear-gradient(to bottom right, #fff 50%, #666 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: none; /* Hide text logo if image is used */
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
max-width: 150px; /* Adjust size as needed */
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Language Dropdown */
|
||||||
|
.lang-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
color: var(--fg-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover {
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn img {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px); /* More space */
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: none;
|
||||||
|
min-width: 100px; /* Slightly wider */
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.8); /* Deeper shadow */
|
||||||
|
animation: fadeInScale 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option {
|
||||||
|
padding: 10px 16px; /* Larger hit area */
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option:hover {
|
||||||
|
background: rgba(255,255,255,0.08); /* Brighter hover */
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option.active {
|
||||||
|
display: none; /* Hide active language from list to keep it 'relevant' */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
justify-content: flex-start; /* Start from top on smaller mobile to prevent clipping */
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
.lang-dropdown {
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
.pricing-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 700px) {
|
||||||
|
.container {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pricing Grid */
|
||||||
|
.text-time {
|
||||||
|
color: #50e3c2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-quota {
|
||||||
|
color: #3b82f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card:hover {
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 6rem; /* Even more separation from buttons */
|
||||||
|
padding-top: 2rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
color: var(--fg-tertiary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.15); /* Slightly more visible line */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR Scanner Modal */
|
||||||
|
#qr-scanner-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start; /* Change from center to avoid clipping */
|
||||||
|
padding: 2rem 0; /* Add padding for scroll room */
|
||||||
|
overflow-y: auto; /* Enable scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
#reader {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-modal {
|
||||||
|
position: fixed; /* Change from absolute to fixed */
|
||||||
|
top: 1rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
background: rgba(0,0,0,0.5); /* Add subtle bg for better visibility on images */
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1010; /* Ensure it stays on top */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Refinement */
|
||||||
|
.greeting-text {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-greeting {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-time { background: #50e3c2; }
|
||||||
|
.progress-quota { background: #0070f3; }
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-item {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-item .label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-item .value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons & Input Wrappers */
|
||||||
|
.icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon-img {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon {
|
||||||
|
padding-left: 36px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: var(--transition);
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle img {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-toggle {
|
||||||
|
padding-right: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.tab-btn.active img {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility */
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.mt-4 { margin-top: 1rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.text-sm { font-size: 0.875rem; }
|
||||||
|
.opacity-50 { opacity: 0.5; }
|
||||||
|
.flex-center { display: flex; align-items: center; justify-content: center; }
|
||||||
|
|
||||||
|
.scanner-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qr-confirm-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qr-confirm-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
animation: fadeInScale 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-details {
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--fg-primary);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
55
error.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Error</title>
|
||||||
|
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ambient-bg">
|
||||||
|
<div class="orb orb-1"></div>
|
||||||
|
<div class="orb orb-2"></div>
|
||||||
|
<div class="orb orb-3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="container" style="text-align: center;">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
</header>
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="border-color: #ff4d4d;">
|
||||||
|
<h2 class="mb-4 flex-center" style="color: #ff4d4d;">
|
||||||
|
<img src="svg/alert-circle.svg" class="icon" data-asset="icon_error" style="width:24px; height:24px; margin-right:8px;">
|
||||||
|
<span data-i18n="failed_title">Connection Failed</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 2rem; color: var(--fg-secondary);">
|
||||||
|
$(error)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="history.back()" class="btn btn-outline" data-i18n="try_again">
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
104
errors.txt
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# This file contains error messages which are shown to user, when http/https
|
||||||
|
# login is used.
|
||||||
|
# These messages can be changed to make user interface more friendly, including
|
||||||
|
# translations to different languages.
|
||||||
|
#
|
||||||
|
# Various variables can be used here as well. Most frequently used ones are:
|
||||||
|
# $(error-orig) - original error message from hotspot
|
||||||
|
# $(ip) - ip address of a client
|
||||||
|
# $(username) - username of client trying to log in
|
||||||
|
|
||||||
|
# internal-error
|
||||||
|
# It should never happen. If it will, error page will be shown
|
||||||
|
# displaying this error message (error-orig will describe what has happened)
|
||||||
|
|
||||||
|
internal-error = internal error ($(error-orig))
|
||||||
|
|
||||||
|
# config-error
|
||||||
|
# Should never happen if hotspot is configured properly.
|
||||||
|
|
||||||
|
config-error = configuration error ($(error-orig))
|
||||||
|
|
||||||
|
# not-logged-in
|
||||||
|
# Will happen, if status or logout page is requested by user,
|
||||||
|
# which actually is not logged in
|
||||||
|
|
||||||
|
not-logged-in = you are not logged in (ip $(ip))
|
||||||
|
|
||||||
|
# ippool-empty
|
||||||
|
# IP address for user is to be assigned from ip pool, but there are no more
|
||||||
|
# addresses in that pool
|
||||||
|
|
||||||
|
ippool-empty = cannot assign ip address - no more free addresses from pool
|
||||||
|
|
||||||
|
# shutting-down
|
||||||
|
# When shutdown is executed, new clients are not accepted
|
||||||
|
|
||||||
|
shutting-down = hotspot service is shutting down
|
||||||
|
|
||||||
|
# user-session-limit
|
||||||
|
# If user profile has limit of shared-users, then this error will be shown
|
||||||
|
# after reaching this limit
|
||||||
|
|
||||||
|
user-session-limit = no more sessions are allowed for user $(username)
|
||||||
|
|
||||||
|
# license-session-limit
|
||||||
|
# Depending on licence number of active hotspot clients is limited to
|
||||||
|
# one or another amount. If this limit is reached, following error is displayed.
|
||||||
|
|
||||||
|
license-session-limit = session limit reached ($(error-orig))
|
||||||
|
|
||||||
|
# wrong-mac-username
|
||||||
|
# If username looks like MAC address (12:34:56:78:9a:bc), but is not
|
||||||
|
# a MAC address of this client, login is rejected
|
||||||
|
|
||||||
|
wrong-mac-username = invalid username ($(username)): this MAC address is not yours
|
||||||
|
|
||||||
|
# chap-missing
|
||||||
|
# If http-chap login method is used, but hotspot program does not receive
|
||||||
|
# back encrypted password, this error message is shown.
|
||||||
|
# Possible reasons of failure:
|
||||||
|
# - JavaScript is not enabled in web browser;
|
||||||
|
# - login.html page is not valid;
|
||||||
|
# - challenge value has expired on server (more than 1h of inactivity);
|
||||||
|
# - http-chap login method is recently removed;
|
||||||
|
# If JavaScript is enabled and login.html page is valid,
|
||||||
|
# then retrying to login usually fixes this problem.
|
||||||
|
|
||||||
|
chap-missing = web browser did not send challenge response (try again, enable JavaScript)
|
||||||
|
|
||||||
|
# invalid-username
|
||||||
|
# Most general case of invalid username or password. If RADIUS server
|
||||||
|
# has sent an error string with Access-Reject message, then it will
|
||||||
|
# override this setting.
|
||||||
|
|
||||||
|
invalid-username = invalid username or password
|
||||||
|
|
||||||
|
# invalid-mac
|
||||||
|
# Local users (on hotspot server) can be bound to some MAC address. If login
|
||||||
|
# from different MAC is tried, this error message will be shown.
|
||||||
|
|
||||||
|
invalid-mac = user $(username) is not allowed to log in from this MAC address
|
||||||
|
|
||||||
|
# uptime-limit, traffic-limit
|
||||||
|
# For local hotspot users in case if limits are reached
|
||||||
|
|
||||||
|
uptime-limit = user $(username) has reached uptime limit
|
||||||
|
traffic-limit = user $(username) has reached traffic limit
|
||||||
|
|
||||||
|
# radius-timeout
|
||||||
|
# User is authenticated by RADIUS server, but no response is received from it,
|
||||||
|
# following error will be shown.
|
||||||
|
|
||||||
|
radius-timeout = RADIUS server is not responding
|
||||||
|
|
||||||
|
# auth-in-progress
|
||||||
|
# Authorization in progress. Client already has issued an authorization request
|
||||||
|
# which is not yet complete.
|
||||||
|
|
||||||
|
auth-in-progress = already authorizing, retry later
|
||||||
|
|
||||||
|
# radius-reply
|
||||||
|
# Radius server returned some custom error message
|
||||||
|
|
||||||
|
radius-reply = $(error-orig)
|
||||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
img/favicon.ico
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
img/logo-icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
32
img/logo-twinpath.svg
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<svg width="242" height="50" viewBox="0 0 242 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#FFFFFF" />
|
||||||
|
<stop offset="100%" stop-color="#AAAAAA" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Left Bracket < -->
|
||||||
|
<!-- Distance to left edge: 33px -->
|
||||||
|
<!-- Leg: 43. Tip: 33. -->
|
||||||
|
<path d="M43 14 L33 25 L43 36" stroke="url(#textGrad)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<!-- Centered at 121 (Symmetric Center of 33 and 209) -->
|
||||||
|
<text x="121" y="32" font-family="'Geist Sans', 'Inter', -apple-system, sans-serif" font-weight="700" font-size="22" text-anchor="middle" fill="url(#textGrad)" letter-spacing="-0.5px">
|
||||||
|
TwinpathNet
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Slash / -->
|
||||||
|
<!-- Positioned relative to right bracket -->
|
||||||
|
<line x1="196" y1="14" x2="188" y2="36" stroke="url(#textGrad)" stroke-width="3" stroke-linecap="round" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- Right Bracket > -->
|
||||||
|
<!-- Distance to right edge: 33px (242 - 209 = 33) -->
|
||||||
|
<!-- Leg: 199. Tip: 209. -->
|
||||||
|
<path d="M199 14 L209 25 L199 36" stroke="url(#textGrad)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
img/password.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" data-prefix="fas" data-icon="key" class="svg-inline--fa fa-key fa-w-16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#464646" d="M512 176.001C512 273.203 433.202 352 336 352c-11.22 0-22.19-1.062-32.827-3.069l-24.012 27.014A23.999 23.999 0 0 1 261.223 384H224v40c0 13.255-10.745 24-24 24h-40v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24v-78.059c0-6.365 2.529-12.47 7.029-16.971l161.802-161.802C163.108 213.814 160 195.271 160 176 160 78.798 238.797.001 335.999 0 433.488-.001 512 78.511 512 176.001zM336 128c0 26.51 21.49 48 48 48s48-21.49 48-48-21.49-48-48-48-48 21.49-48 48z"/></svg>
|
||||||
|
After Width: | Height: | Size: 644 B |
1
img/user.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" data-prefix="fas" data-icon="user" class="svg-inline--fa fa-user fa-w-14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#464646" d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
64
js/config.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const brandConfig = {
|
||||||
|
brandName: "TwinpathNet",
|
||||||
|
portalUrl: "http://welcome.dyzulk.com/login",
|
||||||
|
creditName: "dyzulk.com",
|
||||||
|
creditUrl: "https://dyzulk.com",
|
||||||
|
assets: {
|
||||||
|
logo: "img/logo-twinpath.svg",
|
||||||
|
icon_ticket: "svg/ticket.svg",
|
||||||
|
icon_user: "svg/user.svg",
|
||||||
|
icon_lock: "svg/lock.svg",
|
||||||
|
icon_scan: "svg/scan-line.svg",
|
||||||
|
icon_image: "svg/image.svg",
|
||||||
|
icon_external: "svg/external-link.svg",
|
||||||
|
icon_copy: "svg/copy.svg",
|
||||||
|
icon_logout: "svg/log-out.svg",
|
||||||
|
icon_success: "svg/check-circle.svg",
|
||||||
|
icon_error: "svg/alert-circle.svg",
|
||||||
|
icon_clock: "svg/clock.svg",
|
||||||
|
icon_upload: "svg/upload-cloud.svg",
|
||||||
|
icon_download: "svg/download-cloud.svg",
|
||||||
|
icon_wifi: "svg/wifi.svg"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyBranding() {
|
||||||
|
// Update Document Title
|
||||||
|
const currentTitle = document.title;
|
||||||
|
if (currentTitle.includes('TwinpathNet')) {
|
||||||
|
document.title = currentTitle.replace('TwinpathNet', brandConfig.brandName);
|
||||||
|
} else if (!currentTitle.includes(brandConfig.brandName)) {
|
||||||
|
document.title = `${brandConfig.brandName} > ${currentTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Elements with data-brand-name
|
||||||
|
document.querySelectorAll('[data-brand-name]').forEach(el => {
|
||||||
|
el.innerText = brandConfig.brandName;
|
||||||
|
if (el.tagName === 'IMG') el.alt = brandConfig.brandName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Elements with data-brand-credit
|
||||||
|
document.querySelectorAll('[data-brand-credit]').forEach(el => {
|
||||||
|
el.innerText = brandConfig.creditName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Links with data-brand-link
|
||||||
|
document.querySelectorAll('[data-brand-link="credit"]').forEach(el => {
|
||||||
|
el.href = brandConfig.creditUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Assets (Images/Icons)
|
||||||
|
document.querySelectorAll('[data-asset]').forEach(el => {
|
||||||
|
const assetKey = el.getAttribute('data-asset');
|
||||||
|
if (brandConfig.assets[assetKey]) {
|
||||||
|
el.src = brandConfig.assets[assetKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply branding as soon as possible
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', applyBranding);
|
||||||
|
} else {
|
||||||
|
applyBranding();
|
||||||
|
}
|
||||||
1
js/html5-qrcode.min.js
vendored
Normal file
158
js/languages.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
const translations = {
|
||||||
|
id: {
|
||||||
|
lang_name: "Bahasa Indonesia",
|
||||||
|
operational: "Beroperasi",
|
||||||
|
online: "Terhubung",
|
||||||
|
tab_voucher: "Voucher",
|
||||||
|
tab_member: "Member",
|
||||||
|
voucher_label: "Kode Voucher",
|
||||||
|
voucher_placeholder: "Masukkan kode voucher...",
|
||||||
|
user_label: "Nama Pengguna",
|
||||||
|
pass_label: "Kata Sandi",
|
||||||
|
login_voucher: "Gunakan Voucher",
|
||||||
|
login_member: "Masuk Member",
|
||||||
|
connect_btn: "Hubungkan",
|
||||||
|
scan_btn: "Scan QR Code",
|
||||||
|
trial_btn: "Coba Gratis",
|
||||||
|
status_dashboard_msg: "Kelola sesi aktif dan pantau penggunaan anda.",
|
||||||
|
connection_stats: "Statistik Koneksi",
|
||||||
|
quota_left: "Sisa Kuota",
|
||||||
|
unlimited: "Tanpa Batas",
|
||||||
|
or_text: "Atau",
|
||||||
|
scan_file_btn: "Scan dari Galeri",
|
||||||
|
point_camera: "Arahkan kamera ke QR Code",
|
||||||
|
confirm_title: "Konfirmasi Voucher",
|
||||||
|
confirm_msg: "Apakah Anda ingin masuk dengan kode ini?",
|
||||||
|
open_browser: "Buka di Browser",
|
||||||
|
copy_url: "Salin Link Portal",
|
||||||
|
manual_tip: "Kamera/Galeri diblokir sistem? Silakan buka di Chrome/Safari atau salin link portal di bawah.",
|
||||||
|
hour: "JAM",
|
||||||
|
hours: "JAM",
|
||||||
|
day: "HARI",
|
||||||
|
week: "MINGGU",
|
||||||
|
pricing_title: "Paket Tersedia",
|
||||||
|
session_info: "Informasi Sesi",
|
||||||
|
ip_address: "Alamat IP",
|
||||||
|
upload: "Unggah",
|
||||||
|
download: "Unduh",
|
||||||
|
time_left: "Sisa Waktu",
|
||||||
|
logout_btn: "Keluar (Log Out)",
|
||||||
|
logged_out: "Sudah Keluar",
|
||||||
|
logged_out_msg: "Anda telah berhasil memutuskan koneksi dari jaringan.",
|
||||||
|
login_again: "Masuk Kembali",
|
||||||
|
failed_title: "Koneksi Gagal",
|
||||||
|
try_again: "Coba Lagi",
|
||||||
|
success_title: "Berhasil",
|
||||||
|
success_msg: "Anda sekarang terhubung ke jaringan.",
|
||||||
|
redirect_msg: "Mengalihkan...",
|
||||||
|
redirect_tip: "Klik di sini jika tidak beralih otomatis",
|
||||||
|
adv_title: "Iklan",
|
||||||
|
adv_msg: "Jika tidak terjadi apa-apa, buka iklan secara manual.",
|
||||||
|
adv_link: "iklan",
|
||||||
|
adv_manually: "secara manual",
|
||||||
|
powered_by: "Didukung oleh"
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
lang_name: "English",
|
||||||
|
operational: "Operational",
|
||||||
|
online: "Online",
|
||||||
|
tab_voucher: "Voucher",
|
||||||
|
tab_member: "Member",
|
||||||
|
voucher_label: "Voucher Code",
|
||||||
|
voucher_placeholder: "Enter code received...",
|
||||||
|
user_label: "Username",
|
||||||
|
pass_label: "Password",
|
||||||
|
login_voucher: "Use Voucher",
|
||||||
|
login_member: "Member Login",
|
||||||
|
connect_btn: "Connect",
|
||||||
|
scan_btn: "Scan QR Code",
|
||||||
|
trial_btn: "Free Trial Access",
|
||||||
|
status_dashboard_msg: "Manage your active session and monitor your usage.",
|
||||||
|
connection_stats: "Connection Stats",
|
||||||
|
quota_left: "Quota Remaining",
|
||||||
|
unlimited: "Unlimited",
|
||||||
|
or_text: "Or",
|
||||||
|
scan_file_btn: "Scan from Gallery",
|
||||||
|
point_camera: "Point camera at QR Code",
|
||||||
|
confirm_title: "Confirm Voucher",
|
||||||
|
confirm_msg: "Do you want to log in with this code?",
|
||||||
|
open_browser: "Open in Browser",
|
||||||
|
copy_url: "Copy Portal Link",
|
||||||
|
manual_tip: "Camera/Gallery restricted? Please open in Chrome/Safari or copy the portal link below.",
|
||||||
|
hour: "HOUR",
|
||||||
|
hours: "HOURS",
|
||||||
|
day: "DAY",
|
||||||
|
week: "WEEK",
|
||||||
|
pricing_title: "Available Packages",
|
||||||
|
session_info: "Session Information",
|
||||||
|
ip_address: "IP Address",
|
||||||
|
upload: "Upload",
|
||||||
|
download: "Download",
|
||||||
|
time_left: "Time Remaining",
|
||||||
|
logout_btn: "Log Out",
|
||||||
|
logged_out: "Logged Out",
|
||||||
|
logged_out_msg: "You have successfully disconnected from the network.",
|
||||||
|
login_again: "Log In Again",
|
||||||
|
failed_title: "Connection Failed",
|
||||||
|
try_again: "Try Again",
|
||||||
|
success_title: "Success",
|
||||||
|
success_msg: "You are now connected to the network.",
|
||||||
|
redirect_msg: "Redirecting...",
|
||||||
|
redirect_tip: "Click here if not redirected automatically",
|
||||||
|
adv_title: "Advertisement",
|
||||||
|
adv_msg: "If nothing happens, open advertisement manually.",
|
||||||
|
adv_link: "advertisement",
|
||||||
|
adv_manually: "manually",
|
||||||
|
powered_by: "Powered by"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyLanguage(lang) {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
if (translations[lang] && translations[lang][key]) {
|
||||||
|
if (el.tagName === 'INPUT' && el.placeholder) {
|
||||||
|
el.placeholder = translations[lang][key];
|
||||||
|
} else {
|
||||||
|
el.innerText = translations[lang][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
localStorage.setItem('twinpath_lang', lang);
|
||||||
|
document.documentElement.lang = lang;
|
||||||
|
|
||||||
|
// Update active state in custom menu
|
||||||
|
document.querySelectorAll('.lang-option').forEach(opt => {
|
||||||
|
opt.classList.toggle('active', opt.getAttribute('data-lang') === lang);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button text
|
||||||
|
const langLabel = document.getElementById('lang-label');
|
||||||
|
if (langLabel) langLabel.innerText = lang.toUpperCase();
|
||||||
|
|
||||||
|
// Refresh dynamic content if on dashboard
|
||||||
|
if (typeof initDashboard === 'function') {
|
||||||
|
initDashboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLangMenu() {
|
||||||
|
const menu = document.getElementById('lang-menu');
|
||||||
|
if (menu) menu.classList.toggle('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
const dropdown = document.querySelector('.lang-dropdown');
|
||||||
|
const menu = document.getElementById('lang-menu');
|
||||||
|
if (menu && dropdown && !dropdown.contains(e.target)) {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function initLanguage() {
|
||||||
|
const savedLang = localStorage.getItem('twinpath_lang') || 'en';
|
||||||
|
applyLanguage(savedLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initLanguage);
|
||||||
217
js/md5.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||||
|
* Digest Algorithm, as defined in RFC 1321.
|
||||||
|
* Version 1.1 Copyright (C) Paul Johnston 1999 - 2002.
|
||||||
|
* Code also contributed by Greg Holt
|
||||||
|
* See http://pajhome.org.uk/site/legal.html for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||||
|
* to work around bugs in some JS interpreters.
|
||||||
|
*/
|
||||||
|
function safe_add(x, y)
|
||||||
|
{
|
||||||
|
var lsw = (x & 0xFFFF) + (y & 0xFFFF)
|
||||||
|
var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
|
||||||
|
return (msw << 16) | (lsw & 0xFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bitwise rotate a 32-bit number to the left.
|
||||||
|
*/
|
||||||
|
function rol(num, cnt)
|
||||||
|
{
|
||||||
|
return (num << cnt) | (num >>> (32 - cnt))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These functions implement the four basic operations the algorithm uses.
|
||||||
|
*/
|
||||||
|
function cmn(q, a, b, x, s, t)
|
||||||
|
{
|
||||||
|
return safe_add(rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b)
|
||||||
|
}
|
||||||
|
function ff(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn((b & c) | ((~b) & d), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
function gg(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn((b & d) | (c & (~d)), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
function hh(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn(b ^ c ^ d, a, b, x, s, t)
|
||||||
|
}
|
||||||
|
function ii(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn(c ^ (b | (~d)), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calculate the MD5 of an array of little-endian words, producing an array
|
||||||
|
* of little-endian words.
|
||||||
|
*/
|
||||||
|
function coreMD5(x)
|
||||||
|
{
|
||||||
|
var a = 1732584193
|
||||||
|
var b = -271733879
|
||||||
|
var c = -1732584194
|
||||||
|
var d = 271733878
|
||||||
|
|
||||||
|
for(i = 0; i < x.length; i += 16)
|
||||||
|
{
|
||||||
|
var olda = a
|
||||||
|
var oldb = b
|
||||||
|
var oldc = c
|
||||||
|
var oldd = d
|
||||||
|
|
||||||
|
a = ff(a, b, c, d, x[i+ 0], 7 , -680876936)
|
||||||
|
d = ff(d, a, b, c, x[i+ 1], 12, -389564586)
|
||||||
|
c = ff(c, d, a, b, x[i+ 2], 17, 606105819)
|
||||||
|
b = ff(b, c, d, a, x[i+ 3], 22, -1044525330)
|
||||||
|
a = ff(a, b, c, d, x[i+ 4], 7 , -176418897)
|
||||||
|
d = ff(d, a, b, c, x[i+ 5], 12, 1200080426)
|
||||||
|
c = ff(c, d, a, b, x[i+ 6], 17, -1473231341)
|
||||||
|
b = ff(b, c, d, a, x[i+ 7], 22, -45705983)
|
||||||
|
a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416)
|
||||||
|
d = ff(d, a, b, c, x[i+ 9], 12, -1958414417)
|
||||||
|
c = ff(c, d, a, b, x[i+10], 17, -42063)
|
||||||
|
b = ff(b, c, d, a, x[i+11], 22, -1990404162)
|
||||||
|
a = ff(a, b, c, d, x[i+12], 7 , 1804603682)
|
||||||
|
d = ff(d, a, b, c, x[i+13], 12, -40341101)
|
||||||
|
c = ff(c, d, a, b, x[i+14], 17, -1502002290)
|
||||||
|
b = ff(b, c, d, a, x[i+15], 22, 1236535329)
|
||||||
|
|
||||||
|
a = gg(a, b, c, d, x[i+ 1], 5 , -165796510)
|
||||||
|
d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632)
|
||||||
|
c = gg(c, d, a, b, x[i+11], 14, 643717713)
|
||||||
|
b = gg(b, c, d, a, x[i+ 0], 20, -373897302)
|
||||||
|
a = gg(a, b, c, d, x[i+ 5], 5 , -701558691)
|
||||||
|
d = gg(d, a, b, c, x[i+10], 9 , 38016083)
|
||||||
|
c = gg(c, d, a, b, x[i+15], 14, -660478335)
|
||||||
|
b = gg(b, c, d, a, x[i+ 4], 20, -405537848)
|
||||||
|
a = gg(a, b, c, d, x[i+ 9], 5 , 568446438)
|
||||||
|
d = gg(d, a, b, c, x[i+14], 9 , -1019803690)
|
||||||
|
c = gg(c, d, a, b, x[i+ 3], 14, -187363961)
|
||||||
|
b = gg(b, c, d, a, x[i+ 8], 20, 1163531501)
|
||||||
|
a = gg(a, b, c, d, x[i+13], 5 , -1444681467)
|
||||||
|
d = gg(d, a, b, c, x[i+ 2], 9 , -51403784)
|
||||||
|
c = gg(c, d, a, b, x[i+ 7], 14, 1735328473)
|
||||||
|
b = gg(b, c, d, a, x[i+12], 20, -1926607734)
|
||||||
|
|
||||||
|
a = hh(a, b, c, d, x[i+ 5], 4 , -378558)
|
||||||
|
d = hh(d, a, b, c, x[i+ 8], 11, -2022574463)
|
||||||
|
c = hh(c, d, a, b, x[i+11], 16, 1839030562)
|
||||||
|
b = hh(b, c, d, a, x[i+14], 23, -35309556)
|
||||||
|
a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060)
|
||||||
|
d = hh(d, a, b, c, x[i+ 4], 11, 1272893353)
|
||||||
|
c = hh(c, d, a, b, x[i+ 7], 16, -155497632)
|
||||||
|
b = hh(b, c, d, a, x[i+10], 23, -1094730640)
|
||||||
|
a = hh(a, b, c, d, x[i+13], 4 , 681279174)
|
||||||
|
d = hh(d, a, b, c, x[i+ 0], 11, -358537222)
|
||||||
|
c = hh(c, d, a, b, x[i+ 3], 16, -722521979)
|
||||||
|
b = hh(b, c, d, a, x[i+ 6], 23, 76029189)
|
||||||
|
a = hh(a, b, c, d, x[i+ 9], 4 , -640364487)
|
||||||
|
d = hh(d, a, b, c, x[i+12], 11, -421815835)
|
||||||
|
c = hh(c, d, a, b, x[i+15], 16, 530742520)
|
||||||
|
b = hh(b, c, d, a, x[i+ 2], 23, -995338651)
|
||||||
|
|
||||||
|
a = ii(a, b, c, d, x[i+ 0], 6 , -198630844)
|
||||||
|
d = ii(d, a, b, c, x[i+ 7], 10, 1126891415)
|
||||||
|
c = ii(c, d, a, b, x[i+14], 15, -1416354905)
|
||||||
|
b = ii(b, c, d, a, x[i+ 5], 21, -57434055)
|
||||||
|
a = ii(a, b, c, d, x[i+12], 6 , 1700485571)
|
||||||
|
d = ii(d, a, b, c, x[i+ 3], 10, -1894986606)
|
||||||
|
c = ii(c, d, a, b, x[i+10], 15, -1051523)
|
||||||
|
b = ii(b, c, d, a, x[i+ 1], 21, -2054922799)
|
||||||
|
a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359)
|
||||||
|
d = ii(d, a, b, c, x[i+15], 10, -30611744)
|
||||||
|
c = ii(c, d, a, b, x[i+ 6], 15, -1560198380)
|
||||||
|
b = ii(b, c, d, a, x[i+13], 21, 1309151649)
|
||||||
|
a = ii(a, b, c, d, x[i+ 4], 6 , -145523070)
|
||||||
|
d = ii(d, a, b, c, x[i+11], 10, -1120210379)
|
||||||
|
c = ii(c, d, a, b, x[i+ 2], 15, 718787259)
|
||||||
|
b = ii(b, c, d, a, x[i+ 9], 21, -343485551)
|
||||||
|
|
||||||
|
a = safe_add(a, olda)
|
||||||
|
b = safe_add(b, oldb)
|
||||||
|
c = safe_add(c, oldc)
|
||||||
|
d = safe_add(d, oldd)
|
||||||
|
}
|
||||||
|
return [a, b, c, d]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a hex string.
|
||||||
|
*/
|
||||||
|
function binl2hex(binarray)
|
||||||
|
{
|
||||||
|
var hex_tab = "0123456789abcdef"
|
||||||
|
var str = ""
|
||||||
|
for(var i = 0; i < binarray.length * 4; i++)
|
||||||
|
{
|
||||||
|
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
|
||||||
|
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8)) & 0xF)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a base64 encoded string.
|
||||||
|
*/
|
||||||
|
function binl2b64(binarray)
|
||||||
|
{
|
||||||
|
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
var str = ""
|
||||||
|
for(var i = 0; i < binarray.length * 32; i += 6)
|
||||||
|
{
|
||||||
|
str += tab.charAt(((binarray[i>>5] << (i%32)) & 0x3F) |
|
||||||
|
((binarray[i>>5+1] >> (32-i%32)) & 0x3F))
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an 8-bit character string to a sequence of 16-word blocks, stored
|
||||||
|
* as an array, and append appropriate padding for MD4/5 calculation.
|
||||||
|
* If any of the characters are >255, the high byte is silently ignored.
|
||||||
|
*/
|
||||||
|
function str2binl(str)
|
||||||
|
{
|
||||||
|
var nblk = ((str.length + 8) >> 6) + 1 // number of 16-word blocks
|
||||||
|
var blks = new Array(nblk * 16)
|
||||||
|
for(var i = 0; i < nblk * 16; i++) blks[i] = 0
|
||||||
|
for(var i = 0; i < str.length; i++)
|
||||||
|
blks[i>>2] |= (str.charCodeAt(i) & 0xFF) << ((i%4) * 8)
|
||||||
|
blks[i>>2] |= 0x80 << ((i%4) * 8)
|
||||||
|
blks[nblk*16-2] = str.length * 8
|
||||||
|
return blks
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert a wide-character string to a sequence of 16-word blocks, stored as
|
||||||
|
* an array, and append appropriate padding for MD4/5 calculation.
|
||||||
|
*/
|
||||||
|
function strw2binl(str)
|
||||||
|
{
|
||||||
|
var nblk = ((str.length + 4) >> 5) + 1 // number of 16-word blocks
|
||||||
|
var blks = new Array(nblk * 16)
|
||||||
|
for(var i = 0; i < nblk * 16; i++) blks[i] = 0
|
||||||
|
for(var i = 0; i < str.length; i++)
|
||||||
|
blks[i>>1] |= str.charCodeAt(i) << ((i%2) * 16)
|
||||||
|
blks[i>>1] |= 0x80 << ((i%2) * 16)
|
||||||
|
blks[nblk*16-2] = str.length * 16
|
||||||
|
return blks
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* External interface
|
||||||
|
*/
|
||||||
|
function hexMD5 (str) { return binl2hex(coreMD5( str2binl(str))) }
|
||||||
|
function hexMD5w(str) { return binl2hex(coreMD5(strw2binl(str))) }
|
||||||
|
function b64MD5 (str) { return binl2b64(coreMD5( str2binl(str))) }
|
||||||
|
function b64MD5w(str) { return binl2b64(coreMD5(strw2binl(str))) }
|
||||||
|
/* Backward compatibility */
|
||||||
|
function calcMD5(str) { return binl2hex(coreMD5( str2binl(str))) }
|
||||||
212
js/qr-scanner.js
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
let html5QrCode;
|
||||||
|
let scannedUrl = "";
|
||||||
|
|
||||||
|
function safePause() {
|
||||||
|
try {
|
||||||
|
if (html5QrCode && html5QrCode.isScanning) {
|
||||||
|
html5QrCode.pause();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("SafePause: Scanner already paused or not scanning", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeResume() {
|
||||||
|
try {
|
||||||
|
if (html5QrCode && html5QrCode.isScanning) {
|
||||||
|
html5QrCode.resume();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("SafeResume: Scanner not paused or not scanning", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDecodedText(decodedText) {
|
||||||
|
console.log(`Scan result: ${decodedText}`);
|
||||||
|
|
||||||
|
let username = decodedText;
|
||||||
|
let password = "";
|
||||||
|
scannedUrl = "";
|
||||||
|
|
||||||
|
// Check if result is a URL (common for Mikhmon vouchers)
|
||||||
|
try {
|
||||||
|
if (decodedText.startsWith('http://') || decodedText.startsWith('https://')) {
|
||||||
|
scannedUrl = decodedText; // Store for redirection
|
||||||
|
const url = new URL(decodedText);
|
||||||
|
const searchParams = url.search || (decodedText.includes('?') ? '?' + decodedText.split('?')[1] : '');
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
|
if (params.has('username')) {
|
||||||
|
username = params.get('username');
|
||||||
|
}
|
||||||
|
if (params.has('password')) {
|
||||||
|
password = params.get('password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing QR URL:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill inputs
|
||||||
|
const voucherInput = document.getElementById('voucher-input');
|
||||||
|
const passField = document.getElementById('voucher-pass');
|
||||||
|
if (voucherInput) voucherInput.value = username;
|
||||||
|
if (passField) passField.value = password || username;
|
||||||
|
|
||||||
|
// Show confirmation overlay
|
||||||
|
const overlay = document.getElementById('qr-confirm-overlay');
|
||||||
|
const confirmUser = document.getElementById('confirm-user');
|
||||||
|
if (overlay && confirmUser) {
|
||||||
|
confirmUser.innerText = username;
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause camera scanning while confirming
|
||||||
|
safePause();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelConfirm() {
|
||||||
|
const overlay = document.getElementById('qr-confirm-overlay');
|
||||||
|
if (overlay) overlay.classList.add('hidden');
|
||||||
|
|
||||||
|
safeResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
function proceedSubmit() {
|
||||||
|
// If it's a URL, redirect directly
|
||||||
|
if (scannedUrl) {
|
||||||
|
console.log("Redirecting to scanned URL:", scannedUrl);
|
||||||
|
window.location.href = scannedUrl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to voucher mode for manual codes
|
||||||
|
setMode('voucher');
|
||||||
|
|
||||||
|
// Sync values to the actual form fields
|
||||||
|
const voucherInput = document.getElementById('voucher-input');
|
||||||
|
const voucherPass = document.getElementById('voucher-pass');
|
||||||
|
const form = document.login;
|
||||||
|
|
||||||
|
if (form && voucherInput) {
|
||||||
|
form.username.value = voucherInput.value;
|
||||||
|
form.password.value = voucherPass ? voucherPass.value : voucherInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close scanner
|
||||||
|
closeQR();
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
const submitBtn = document.querySelector('button[type="submit"]');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanFromFile(event) {
|
||||||
|
try {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
console.log("No file selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("File selected:", file.name, file.type, file.size);
|
||||||
|
|
||||||
|
// Ensure library is available
|
||||||
|
if (typeof Html5Qrcode === 'undefined') {
|
||||||
|
alert("QR Scanner library not loaded. Please wait or refresh.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause camera gracefully
|
||||||
|
safePause();
|
||||||
|
|
||||||
|
// Show a loading state
|
||||||
|
const confirmUser = document.getElementById('confirm-user');
|
||||||
|
if (confirmUser) confirmUser.innerText = "Scanning file...";
|
||||||
|
|
||||||
|
// Hide overlay if it was open
|
||||||
|
const overlay = document.getElementById('qr-confirm-overlay');
|
||||||
|
if (overlay) overlay.classList.add('hidden');
|
||||||
|
|
||||||
|
// Reuse instance if possible, or create temporary one for file
|
||||||
|
const fileScanner = new Html5Qrcode("qr-file-reader");
|
||||||
|
|
||||||
|
fileScanner.scanFile(file, true)
|
||||||
|
.then(decodedText => {
|
||||||
|
console.log("Success scanning file:", decodedText);
|
||||||
|
handleDecodedText(decodedText);
|
||||||
|
fileScanner.clear(); // Cleanup
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Error scanning file: ${err}`);
|
||||||
|
let msg = "No QR Code found.";
|
||||||
|
if (typeof err === "string" && err.includes("not found")) {
|
||||||
|
msg = "QR Code not detected. Try a clearer or closer photo.";
|
||||||
|
}
|
||||||
|
alert(msg);
|
||||||
|
|
||||||
|
if (confirmUser) confirmUser.innerText = "";
|
||||||
|
|
||||||
|
// Resume camera
|
||||||
|
if (html5QrCode && html5QrCode.isScanning) {
|
||||||
|
html5QrCode.resume();
|
||||||
|
}
|
||||||
|
fileScanner.clear(); // Cleanup
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset input so searching for the same file again triggers change
|
||||||
|
event.target.value = "";
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fatal error in scanFromFile:", e);
|
||||||
|
const errorMsg = e.message || JSON.stringify(e) || e;
|
||||||
|
alert("An error occurred while opening the file: " + errorMsg);
|
||||||
|
|
||||||
|
// Try to resume camera if it crashed here
|
||||||
|
if (html5QrCode && html5QrCode.isScanning) {
|
||||||
|
html5QrCode.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function openQR() {
|
||||||
|
const modal = document.getElementById('qr-scanner-modal');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
if (!html5QrCode) {
|
||||||
|
html5QrCode = new Html5Qrcode("reader");
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
||||||
|
|
||||||
|
html5QrCode.start(
|
||||||
|
{ facingMode: "environment" },
|
||||||
|
config,
|
||||||
|
handleDecodedText
|
||||||
|
).catch(err => {
|
||||||
|
console.error("Scanner start error:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeQR() {
|
||||||
|
const modal = document.getElementById('qr-scanner-modal');
|
||||||
|
const overlay = document.getElementById('qr-confirm-overlay');
|
||||||
|
|
||||||
|
if (modal) modal.style.display = 'none';
|
||||||
|
if (overlay) overlay.classList.add('hidden');
|
||||||
|
|
||||||
|
if (html5QrCode) {
|
||||||
|
html5QrCode.stop().then(() => {
|
||||||
|
console.log("Scanner stopped");
|
||||||
|
}).catch((err) => {
|
||||||
|
// Ignore error if already stopped
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
255
js/script.js
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
function setMode(mode) {
|
||||||
|
const voucherMode = document.getElementById('voucher-mode');
|
||||||
|
const memberMode = document.getElementById('member-mode');
|
||||||
|
const voucherTab = document.querySelector('.tab-btn:nth-child(1)');
|
||||||
|
const memberTab = document.querySelector('.tab-btn:nth-child(2)');
|
||||||
|
const form = document.login;
|
||||||
|
|
||||||
|
if (mode === 'voucher') {
|
||||||
|
voucherMode.classList.remove('hidden');
|
||||||
|
memberMode.classList.add('hidden');
|
||||||
|
voucherTab.classList.add('active');
|
||||||
|
memberTab.classList.remove('active');
|
||||||
|
|
||||||
|
// Sync to form immediately
|
||||||
|
if (form) {
|
||||||
|
const code = document.getElementById('voucher-input').value;
|
||||||
|
form.username.value = code;
|
||||||
|
form.password.value = document.getElementById('voucher-pass').value || code;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
voucherMode.classList.add('hidden');
|
||||||
|
memberMode.classList.remove('hidden');
|
||||||
|
voucherTab.classList.remove('active');
|
||||||
|
memberTab.classList.add('active');
|
||||||
|
|
||||||
|
// Sync to form immediately
|
||||||
|
if (form) {
|
||||||
|
form.username.value = document.getElementById('member-user').value;
|
||||||
|
form.password.value = document.getElementById('member-pass').value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update login button text based on mode
|
||||||
|
const loginBtn = document.getElementById('login-btn');
|
||||||
|
if (loginBtn) {
|
||||||
|
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||||
|
const key = mode === 'voucher' ? 'login_voucher' : 'login_member';
|
||||||
|
loginBtn.setAttribute('data-i18n', key);
|
||||||
|
if (typeof translations !== 'undefined' && translations[lang] && translations[lang][key]) {
|
||||||
|
loginBtn.innerText = translations[lang][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doLogin() {
|
||||||
|
const form = document.login;
|
||||||
|
const mode = document.querySelector('.tab-btn.active').innerText.toLowerCase();
|
||||||
|
|
||||||
|
// Sync inputs based on mode
|
||||||
|
if (mode === 'voucher') {
|
||||||
|
const code = document.getElementById('voucher-input').value;
|
||||||
|
form.username.value = code;
|
||||||
|
form.password.value = code; // Voucher usually uses same code for user/pass or just user with empty pass (depends on config)
|
||||||
|
// Note: Check your hotspot config. Often Vouchers are "Username = Password"
|
||||||
|
} else {
|
||||||
|
form.username.value = document.getElementById('member-user').value;
|
||||||
|
form.password.value = document.getElementById('member-pass').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CHAP security if available
|
||||||
|
// Note: This relies on variables injected by TwinpathNet (Mikrotik) into the HTML/JS context
|
||||||
|
// We assume 'hexMD5' is available from md5.js
|
||||||
|
/*
|
||||||
|
TwinpathNet usually puts this logic in the <script> block directly in login.html
|
||||||
|
to access $(chap-id) and $(chap-challenge).
|
||||||
|
Since we extracted this to an external file, we need to ensure those vars are accessible.
|
||||||
|
However, usually $(variables) are NOT replaced in .js files by TwinpathNet.
|
||||||
|
They are ONLY replaced in .html files.
|
||||||
|
|
||||||
|
So, strict MD5 hashing MUST be done inside login.html <script> block, OR
|
||||||
|
we submit plain text and let RouterOS handle PAP (if enabled).
|
||||||
|
|
||||||
|
For this "Cool" template, we will assume PAP is enabled for simplicity,
|
||||||
|
OR we can trust the form to submit properly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.value = text;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(el);
|
||||||
|
alert("URL Berhasil Disalin! Silakan buka Chrome dan tempel (paste) di sana.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInExternalBrowser() {
|
||||||
|
const url = brandConfig.portalUrl;
|
||||||
|
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||||
|
|
||||||
|
if (/android/i.test(userAgent)) {
|
||||||
|
// Android Intent for Chrome
|
||||||
|
const domain = new URL(brandConfig.portalUrl).hostname;
|
||||||
|
window.location.href = `intent://${domain}#Intent;scheme=http;package=com.android.chrome;end`;
|
||||||
|
} else if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
|
||||||
|
// iOS: No magic intent from CNA usually works, so we guide them to use the copy button
|
||||||
|
alert("Untuk pengguna iPhone/iPad: Silakan klik tombol 'Salin Link Portal' lalu buka Safari dan tempel alamatnya.");
|
||||||
|
} else {
|
||||||
|
// Other devices
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMikrotikTime(timeStr) {
|
||||||
|
if (!timeStr || timeStr === 'unlimited' || timeStr.includes('$(')) return timeStr;
|
||||||
|
|
||||||
|
const regex = /(\d+)([wdhms])/g;
|
||||||
|
let parts = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
const unitMap = {
|
||||||
|
'w': { 'en': 'w', 'id': ' Minggu' },
|
||||||
|
'd': { 'en': 'd', 'id': ' Hari' },
|
||||||
|
'h': { 'en': 'h', 'id': ' Jam' },
|
||||||
|
'm': { 'en': 'm', 'id': ' Menit' },
|
||||||
|
's': { 'en': 's', 'id': ' Detik' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||||
|
|
||||||
|
while ((match = regex.exec(timeStr)) !== null) {
|
||||||
|
const val = match[1];
|
||||||
|
const unit = match[2];
|
||||||
|
parts.push(val + (unitMap[unit][lang] || unit));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) return timeStr;
|
||||||
|
return parts.slice(0, 3).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMikrotikTimeToSeconds(timeStr) {
|
||||||
|
if (!timeStr || timeStr.includes('$(')) return 0;
|
||||||
|
const regex = /(\d+)([wdhms])/g;
|
||||||
|
let totalSeconds = 0;
|
||||||
|
let match;
|
||||||
|
const multipliers = { 'w': 604800, 'd': 86400, 'h': 3600, 'm': 60, 's': 1 };
|
||||||
|
|
||||||
|
while ((match = regex.exec(timeStr)) !== null) {
|
||||||
|
totalSeconds += parseInt(match[1]) * (multipliers[match[2]] || 0);
|
||||||
|
}
|
||||||
|
return totalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressBars() {
|
||||||
|
const data = document.getElementById('dashboard-data');
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const limitUptime = parseMikrotikTimeToSeconds(data.getAttribute('data-limit-uptime'));
|
||||||
|
const uptime = parseMikrotikTimeToSeconds(data.getAttribute('data-uptime'));
|
||||||
|
const limitBytes = parseInt(data.getAttribute('data-limit-bytes')) || 0;
|
||||||
|
const bytesOut = parseInt(data.getAttribute('data-bytes-out')) || 0;
|
||||||
|
|
||||||
|
// Time Progress: (Total - Used) / Total -> Remaining %
|
||||||
|
if (limitUptime > 0) {
|
||||||
|
const remainingTime = Math.max(0, limitUptime - uptime);
|
||||||
|
const timePercent = (remainingTime / limitUptime) * 100;
|
||||||
|
const timeBar = document.querySelector('.progress-time');
|
||||||
|
if (timeBar) timeBar.style.width = timePercent + '%';
|
||||||
|
} else {
|
||||||
|
const timeBar = document.querySelector('.progress-time');
|
||||||
|
if (timeBar) timeBar.style.width = '100%'; // Unlimited
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quota Progress: (Total - Used) / Total -> Remaining %
|
||||||
|
if (limitBytes > 0) {
|
||||||
|
const remainingBytes = Math.max(0, limitBytes - bytesOut);
|
||||||
|
const quotaPercent = (remainingBytes / limitBytes) * 100;
|
||||||
|
const quotaBar = document.querySelector('.progress-quota');
|
||||||
|
if (quotaBar) quotaBar.style.width = quotaPercent + '%';
|
||||||
|
} else {
|
||||||
|
const quotaBar = document.querySelector('.progress-quota');
|
||||||
|
if (quotaBar) quotaBar.style.width = '100%'; // Unlimited
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDashboard() {
|
||||||
|
// 1. Set Greetings
|
||||||
|
const greetingEl = document.getElementById('greeting-text');
|
||||||
|
if (greetingEl) {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||||
|
let greeting = "";
|
||||||
|
|
||||||
|
if (lang === 'id') {
|
||||||
|
if (hour < 11) greeting = "Selamat Pagi";
|
||||||
|
else if (hour < 15) greeting = "Selamat Siang";
|
||||||
|
else if (hour < 19) greeting = "Selamat Sore";
|
||||||
|
else greeting = "Selamat Malam";
|
||||||
|
} else {
|
||||||
|
if (hour < 12) greeting = "Good Morning";
|
||||||
|
else if (hour < 17) greeting = "Good Afternoon";
|
||||||
|
else greeting = "Good Evening";
|
||||||
|
}
|
||||||
|
greetingEl.innerText = greeting + "!";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Format Time Components
|
||||||
|
document.querySelectorAll('[data-type="mikrotik-time"]').forEach(el => {
|
||||||
|
let val = el.innerText.trim();
|
||||||
|
// If empty or Mikrotik failed to replace (still contains $), it's Unlimited
|
||||||
|
if (!val || val.includes('$(')) {
|
||||||
|
const lang = localStorage.getItem('twinpath_lang') || 'en';
|
||||||
|
el.innerText = (translations[lang] && translations[lang]['unlimited']) || 'Unlimited';
|
||||||
|
} else {
|
||||||
|
el.innerText = formatMikrotikTime(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Dynamic Progress Bars
|
||||||
|
updateProgressBars();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update DOMContentLoaded to include initDashboard
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Determine page type
|
||||||
|
const isLogin = !!document.getElementById('voucher-input');
|
||||||
|
const isStatus = !!document.getElementById('greeting-text');
|
||||||
|
|
||||||
|
if (isLogin) {
|
||||||
|
setMode('voucher');
|
||||||
|
}
|
||||||
|
|
||||||
|
initDashboard();
|
||||||
|
|
||||||
|
// Escape modal
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const modal = document.getElementById('qr-scanner-modal');
|
||||||
|
if (modal && modal.style.display === 'flex') {
|
||||||
|
if (typeof closeQR === 'function') {
|
||||||
|
closeQR();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function togglePassword(inputId, button) {
|
||||||
|
const passwordInput = document.getElementById(inputId);
|
||||||
|
const toggleIcon = button.querySelector('img');
|
||||||
|
|
||||||
|
if (passwordInput.type === 'password') {
|
||||||
|
passwordInput.type = 'text';
|
||||||
|
toggleIcon.src = 'svg/eye-off.svg';
|
||||||
|
} else {
|
||||||
|
passwordInput.type = 'password';
|
||||||
|
toggleIcon.src = 'svg/eye.svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
205
login.html
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login</title>
|
||||||
|
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ambient-bg">
|
||||||
|
<div class="orb orb-1"></div>
|
||||||
|
<div class="orb orb-2"></div>
|
||||||
|
<div class="orb orb-3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
<div class="status-badge">
|
||||||
|
<span style="color: #50e3c2">●</span> <span data-i18n="operational">Operational</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Card -->
|
||||||
|
<div class="card">
|
||||||
|
<!-- TwinpathNet Error Message (Only shows if error exists) -->
|
||||||
|
$(if error)
|
||||||
|
<div style="color: #ff4d4d; margin-bottom: 1rem; font-size: 0.875rem; text-align: center;">
|
||||||
|
$(error)
|
||||||
|
</div>
|
||||||
|
$(endif)
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab-btn active" onclick="setMode('voucher')">
|
||||||
|
<img src="svg/ticket.svg" alt="" data-asset="icon_ticket"> <span data-i18n="tab_voucher">Voucher</span>
|
||||||
|
</div>
|
||||||
|
<div class="tab-btn" onclick="setMode('member')">
|
||||||
|
<img src="svg/user.svg" alt="" data-asset="icon_user"> <span data-i18n="tab_member">Member</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<form name="login" action="$(link-login-only)" method="post" onsubmit="return doLogin()">
|
||||||
|
<input type="hidden" name="dst" value="$(link-orig)">
|
||||||
|
<input type="hidden" name="popup" value="true">
|
||||||
|
|
||||||
|
<!-- Voucher Mode -->
|
||||||
|
<div id="voucher-mode">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label" data-i18n="voucher_label">Voucher Code</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<img src="svg/ticket.svg" class="input-icon-img" alt="" data-asset="icon_ticket">
|
||||||
|
<input type="text" name="username" id="voucher-input" class="input-field input-with-icon" data-i18n="voucher_placeholder" placeholder="Enter code received..." value="$(username)">
|
||||||
|
</div>
|
||||||
|
<!-- Hide password field for voucher (username=password) -->
|
||||||
|
<input type="hidden" name="password" id="voucher-pass">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Member Mode -->
|
||||||
|
<div id="member-mode" class="hidden">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label" data-i18n="user_label">Username</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<img src="svg/user.svg" class="input-icon-img" alt="" data-asset="icon_user">
|
||||||
|
<input type="text" id="member-user" class="input-field input-with-icon" placeholder="username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label" data-i18n="pass_label">Password</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<img src="svg/lock.svg" class="input-icon-img" alt="" data-asset="icon_lock">
|
||||||
|
<input type="password" id="member-pass" class="input-field input-with-icon input-with-toggle" placeholder="••••••••">
|
||||||
|
<button type="button" class="password-toggle" onclick="togglePassword('member-pass', this)">
|
||||||
|
<img src="svg/eye.svg" alt="" id="toggle-icon-member-pass">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div style="display: grid; gap: 0.75rem;">
|
||||||
|
<button type="submit" id="login-btn" class="btn btn-primary" data-i18n="login_voucher">Use Voucher</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-outline" onclick="openQR()">
|
||||||
|
<img src="svg/scan-line.svg" width="16" height="16" alt="" data-asset="icon_scan" style="margin-right: 0.5rem; vertical-align: text-bottom;">
|
||||||
|
<span data-i18n="scan_btn">Scan QR Code</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
$(if trial == 'yes')
|
||||||
|
<div style="text-align: center; margin-top: 0.5rem; font-size: 0.8rem; color: var(--fg-secondary);">
|
||||||
|
<span data-i18n="or_text">Or</span>
|
||||||
|
<button type="button" onclick="location.href='$(link-login-only)?dst=$(link-orig-esc)&username=T-$(mac-esc)'" class="btn btn-outline" style="margin-top: 0.5rem" data-i18n="trial_btn">
|
||||||
|
Free Trial Access
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
$(endif)
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pricing Section -->
|
||||||
|
<div>
|
||||||
|
<h3 style="font-size: 1rem; margin-bottom: 1rem; color: var(--fg-secondary);" data-i18n="pricing_title">Available Packages</h3>
|
||||||
|
<div class="pricing-grid">
|
||||||
|
<!-- Paket 1 -->
|
||||||
|
<div class="pricing-card">
|
||||||
|
<div class="duration">3 <span data-i18n="hours">HOURS</span></div>
|
||||||
|
<div class="price">2K</div>
|
||||||
|
</div>
|
||||||
|
<!-- Paket 2 -->
|
||||||
|
<div class="pricing-card">
|
||||||
|
<div class="duration">1 <span data-i18n="day">DAY</span></div>
|
||||||
|
<div class="price">5K</div>
|
||||||
|
</div>
|
||||||
|
<!-- Paket 3 -->
|
||||||
|
<div class="pricing-card">
|
||||||
|
<div class="duration">1 <span data-i18n="week">WEEK</span></div>
|
||||||
|
<div class="price">10K</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Modal -->
|
||||||
|
<div id="qr-scanner-modal">
|
||||||
|
<button class="close-modal" onclick="closeQR()">×</button>
|
||||||
|
<div id="reader"></div>
|
||||||
|
<div class="scanner-controls">
|
||||||
|
<p style="margin-bottom: 1rem; color: #fff;" data-i18n="point_camera">Point camera at QR Code</p>
|
||||||
|
|
||||||
|
<input type="file" id="qr-input-file" accept="image/*" style="display:none" onchange="scanFromFile(event)">
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-outline" onclick="document.getElementById('qr-input-file').click()" style="background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.2); color: #fff;">
|
||||||
|
<img src="svg/image.svg" width="16" height="16" alt="" data-asset="icon_image" style="margin-right: 0.5rem; vertical-align: text-bottom; filter: invert(1);">
|
||||||
|
<span data-i18n="scan_file_btn">Scan from Gallery</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style="margin-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.1); pt: 1rem; width: 100%;">
|
||||||
|
<p style="font-size: 0.70rem; color: #aaa; margin-bottom: 0.5rem;" data-i18n="manual_tip">If it doesn't open automatically, please copy the link and paste it into your browser.</p>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
||||||
|
<button type="button" onclick="openInExternalBrowser()" class="btn btn-outline" style="font-size: 0.7rem; padding: 0.5rem; border-color: #50e3c2; color: #50e3c2;">
|
||||||
|
<img src="svg/external-link.svg" width="14" height="14" alt="" data-asset="icon_external" style="margin-right: 0.4rem; vertical-align: text-bottom;">
|
||||||
|
<span data-i18n="open_browser">Open in Browser</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline" onclick="copyToClipboard(brandConfig.portalUrl)" style="font-size: 0.7rem; padding: 0.5rem; border-color: #aaa; color: #aaa;">
|
||||||
|
<img src="svg/copy.svg" width="14" height="14" alt="" data-asset="icon_copy" style="margin-right: 0.4rem; vertical-align: text-bottom; opacity: 0.7;">
|
||||||
|
<span data-i18n="copy_url">Copy Portal Link</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Overlay -->
|
||||||
|
<div id="qr-confirm-overlay" class="hidden">
|
||||||
|
<div class="confirm-card">
|
||||||
|
<h3 data-i18n="confirm_title">Confirm Voucher</h3>
|
||||||
|
<p data-i18n="confirm_msg" style="font-size: 0.8rem; color: var(--fg-secondary); margin-bottom: 1rem;">Do you want to log in with this code?</p>
|
||||||
|
|
||||||
|
<div class="confirm-details">
|
||||||
|
<div class="confirm-item">
|
||||||
|
<span class="confirm-label" data-i18n="user_label">Username</span>
|
||||||
|
<span id="confirm-user" class="confirm-value"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 1.5rem;">
|
||||||
|
<button type="button" class="btn btn-outline" onclick="cancelConfirm()" data-i18n="try_again">Try Again</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="proceedSubmit()" data-i18n="connect_btn">Connect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden element for file scanning -->
|
||||||
|
<div id="qr-file-reader" style="display:none"></div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="js/md5.js"></script>
|
||||||
|
<!-- Note: html5-qrcode library should be downloaded and placed in the folder if offline use is needed -->
|
||||||
|
<script src="js/html5-qrcode.min.js" type="text/javascript"></script>
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
<script src="js/qr-scanner.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
68
logout.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Logout</title>
|
||||||
|
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ambient-bg">
|
||||||
|
<div class="orb orb-1"></div>
|
||||||
|
<div class="orb orb-2"></div>
|
||||||
|
<div class="orb orb-3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="container" style="text-align: center;">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
</header>
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="mb-4 flex-center">
|
||||||
|
<img src="svg/check-circle.svg" class="icon" data-asset="icon_success" style="color:#50e3c2; width:24px; height:24px; margin-right:8px;">
|
||||||
|
<span data-i18n="logged_out">Logged Out</span>
|
||||||
|
</h2>
|
||||||
|
<p class="mb-4 text-sm" style="color: var(--fg-secondary);" data-i18n="logged_out_msg">
|
||||||
|
You have successfully disconnected from the network.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<div class="input-label">
|
||||||
|
<img src="svg/user.svg" class="icon" data-asset="icon_user" style="width:12px; height:12px; margin-right:4px;"> Username
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">$(username)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<div class="input-label">
|
||||||
|
<img src="svg/clock.svg" class="icon" data-asset="icon_clock" style="width:12px; height:12px; margin-right:4px;"> Session Duration
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-time" data-type="mikrotik-time">$(uptime)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="$(link-login)" class="btn btn-primary" data-i18n="login_again">
|
||||||
|
Log In Again
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
217
md5.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||||
|
* Digest Algorithm, as defined in RFC 1321.
|
||||||
|
* Version 1.1 Copyright (C) Paul Johnston 1999 - 2002.
|
||||||
|
* Code also contributed by Greg Holt
|
||||||
|
* See http://pajhome.org.uk/site/legal.html for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||||
|
* to work around bugs in some JS interpreters.
|
||||||
|
*/
|
||||||
|
function safe_add(x, y)
|
||||||
|
{
|
||||||
|
var lsw = (x & 0xFFFF) + (y & 0xFFFF)
|
||||||
|
var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
|
||||||
|
return (msw << 16) | (lsw & 0xFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bitwise rotate a 32-bit number to the left.
|
||||||
|
*/
|
||||||
|
function rol(num, cnt)
|
||||||
|
{
|
||||||
|
return (num << cnt) | (num >>> (32 - cnt))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* These functions implement the four basic operations the algorithm uses.
|
||||||
|
*/
|
||||||
|
function cmn(q, a, b, x, s, t)
|
||||||
|
{
|
||||||
|
return safe_add(rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b)
|
||||||
|
}
|
||||||
|
function ff(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn((b & c) | ((~b) & d), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
function gg(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn((b & d) | (c & (~d)), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
function hh(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn(b ^ c ^ d, a, b, x, s, t)
|
||||||
|
}
|
||||||
|
function ii(a, b, c, d, x, s, t)
|
||||||
|
{
|
||||||
|
return cmn(c ^ (b | (~d)), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calculate the MD5 of an array of little-endian words, producing an array
|
||||||
|
* of little-endian words.
|
||||||
|
*/
|
||||||
|
function coreMD5(x)
|
||||||
|
{
|
||||||
|
var a = 1732584193
|
||||||
|
var b = -271733879
|
||||||
|
var c = -1732584194
|
||||||
|
var d = 271733878
|
||||||
|
|
||||||
|
for(i = 0; i < x.length; i += 16)
|
||||||
|
{
|
||||||
|
var olda = a
|
||||||
|
var oldb = b
|
||||||
|
var oldc = c
|
||||||
|
var oldd = d
|
||||||
|
|
||||||
|
a = ff(a, b, c, d, x[i+ 0], 7 , -680876936)
|
||||||
|
d = ff(d, a, b, c, x[i+ 1], 12, -389564586)
|
||||||
|
c = ff(c, d, a, b, x[i+ 2], 17, 606105819)
|
||||||
|
b = ff(b, c, d, a, x[i+ 3], 22, -1044525330)
|
||||||
|
a = ff(a, b, c, d, x[i+ 4], 7 , -176418897)
|
||||||
|
d = ff(d, a, b, c, x[i+ 5], 12, 1200080426)
|
||||||
|
c = ff(c, d, a, b, x[i+ 6], 17, -1473231341)
|
||||||
|
b = ff(b, c, d, a, x[i+ 7], 22, -45705983)
|
||||||
|
a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416)
|
||||||
|
d = ff(d, a, b, c, x[i+ 9], 12, -1958414417)
|
||||||
|
c = ff(c, d, a, b, x[i+10], 17, -42063)
|
||||||
|
b = ff(b, c, d, a, x[i+11], 22, -1990404162)
|
||||||
|
a = ff(a, b, c, d, x[i+12], 7 , 1804603682)
|
||||||
|
d = ff(d, a, b, c, x[i+13], 12, -40341101)
|
||||||
|
c = ff(c, d, a, b, x[i+14], 17, -1502002290)
|
||||||
|
b = ff(b, c, d, a, x[i+15], 22, 1236535329)
|
||||||
|
|
||||||
|
a = gg(a, b, c, d, x[i+ 1], 5 , -165796510)
|
||||||
|
d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632)
|
||||||
|
c = gg(c, d, a, b, x[i+11], 14, 643717713)
|
||||||
|
b = gg(b, c, d, a, x[i+ 0], 20, -373897302)
|
||||||
|
a = gg(a, b, c, d, x[i+ 5], 5 , -701558691)
|
||||||
|
d = gg(d, a, b, c, x[i+10], 9 , 38016083)
|
||||||
|
c = gg(c, d, a, b, x[i+15], 14, -660478335)
|
||||||
|
b = gg(b, c, d, a, x[i+ 4], 20, -405537848)
|
||||||
|
a = gg(a, b, c, d, x[i+ 9], 5 , 568446438)
|
||||||
|
d = gg(d, a, b, c, x[i+14], 9 , -1019803690)
|
||||||
|
c = gg(c, d, a, b, x[i+ 3], 14, -187363961)
|
||||||
|
b = gg(b, c, d, a, x[i+ 8], 20, 1163531501)
|
||||||
|
a = gg(a, b, c, d, x[i+13], 5 , -1444681467)
|
||||||
|
d = gg(d, a, b, c, x[i+ 2], 9 , -51403784)
|
||||||
|
c = gg(c, d, a, b, x[i+ 7], 14, 1735328473)
|
||||||
|
b = gg(b, c, d, a, x[i+12], 20, -1926607734)
|
||||||
|
|
||||||
|
a = hh(a, b, c, d, x[i+ 5], 4 , -378558)
|
||||||
|
d = hh(d, a, b, c, x[i+ 8], 11, -2022574463)
|
||||||
|
c = hh(c, d, a, b, x[i+11], 16, 1839030562)
|
||||||
|
b = hh(b, c, d, a, x[i+14], 23, -35309556)
|
||||||
|
a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060)
|
||||||
|
d = hh(d, a, b, c, x[i+ 4], 11, 1272893353)
|
||||||
|
c = hh(c, d, a, b, x[i+ 7], 16, -155497632)
|
||||||
|
b = hh(b, c, d, a, x[i+10], 23, -1094730640)
|
||||||
|
a = hh(a, b, c, d, x[i+13], 4 , 681279174)
|
||||||
|
d = hh(d, a, b, c, x[i+ 0], 11, -358537222)
|
||||||
|
c = hh(c, d, a, b, x[i+ 3], 16, -722521979)
|
||||||
|
b = hh(b, c, d, a, x[i+ 6], 23, 76029189)
|
||||||
|
a = hh(a, b, c, d, x[i+ 9], 4 , -640364487)
|
||||||
|
d = hh(d, a, b, c, x[i+12], 11, -421815835)
|
||||||
|
c = hh(c, d, a, b, x[i+15], 16, 530742520)
|
||||||
|
b = hh(b, c, d, a, x[i+ 2], 23, -995338651)
|
||||||
|
|
||||||
|
a = ii(a, b, c, d, x[i+ 0], 6 , -198630844)
|
||||||
|
d = ii(d, a, b, c, x[i+ 7], 10, 1126891415)
|
||||||
|
c = ii(c, d, a, b, x[i+14], 15, -1416354905)
|
||||||
|
b = ii(b, c, d, a, x[i+ 5], 21, -57434055)
|
||||||
|
a = ii(a, b, c, d, x[i+12], 6 , 1700485571)
|
||||||
|
d = ii(d, a, b, c, x[i+ 3], 10, -1894986606)
|
||||||
|
c = ii(c, d, a, b, x[i+10], 15, -1051523)
|
||||||
|
b = ii(b, c, d, a, x[i+ 1], 21, -2054922799)
|
||||||
|
a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359)
|
||||||
|
d = ii(d, a, b, c, x[i+15], 10, -30611744)
|
||||||
|
c = ii(c, d, a, b, x[i+ 6], 15, -1560198380)
|
||||||
|
b = ii(b, c, d, a, x[i+13], 21, 1309151649)
|
||||||
|
a = ii(a, b, c, d, x[i+ 4], 6 , -145523070)
|
||||||
|
d = ii(d, a, b, c, x[i+11], 10, -1120210379)
|
||||||
|
c = ii(c, d, a, b, x[i+ 2], 15, 718787259)
|
||||||
|
b = ii(b, c, d, a, x[i+ 9], 21, -343485551)
|
||||||
|
|
||||||
|
a = safe_add(a, olda)
|
||||||
|
b = safe_add(b, oldb)
|
||||||
|
c = safe_add(c, oldc)
|
||||||
|
d = safe_add(d, oldd)
|
||||||
|
}
|
||||||
|
return [a, b, c, d]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a hex string.
|
||||||
|
*/
|
||||||
|
function binl2hex(binarray)
|
||||||
|
{
|
||||||
|
var hex_tab = "0123456789abcdef"
|
||||||
|
var str = ""
|
||||||
|
for(var i = 0; i < binarray.length * 4; i++)
|
||||||
|
{
|
||||||
|
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
|
||||||
|
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8)) & 0xF)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an array of little-endian words to a base64 encoded string.
|
||||||
|
*/
|
||||||
|
function binl2b64(binarray)
|
||||||
|
{
|
||||||
|
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
var str = ""
|
||||||
|
for(var i = 0; i < binarray.length * 32; i += 6)
|
||||||
|
{
|
||||||
|
str += tab.charAt(((binarray[i>>5] << (i%32)) & 0x3F) |
|
||||||
|
((binarray[i>>5+1] >> (32-i%32)) & 0x3F))
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert an 8-bit character string to a sequence of 16-word blocks, stored
|
||||||
|
* as an array, and append appropriate padding for MD4/5 calculation.
|
||||||
|
* If any of the characters are >255, the high byte is silently ignored.
|
||||||
|
*/
|
||||||
|
function str2binl(str)
|
||||||
|
{
|
||||||
|
var nblk = ((str.length + 8) >> 6) + 1 // number of 16-word blocks
|
||||||
|
var blks = new Array(nblk * 16)
|
||||||
|
for(var i = 0; i < nblk * 16; i++) blks[i] = 0
|
||||||
|
for(var i = 0; i < str.length; i++)
|
||||||
|
blks[i>>2] |= (str.charCodeAt(i) & 0xFF) << ((i%4) * 8)
|
||||||
|
blks[i>>2] |= 0x80 << ((i%4) * 8)
|
||||||
|
blks[nblk*16-2] = str.length * 8
|
||||||
|
return blks
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert a wide-character string to a sequence of 16-word blocks, stored as
|
||||||
|
* an array, and append appropriate padding for MD4/5 calculation.
|
||||||
|
*/
|
||||||
|
function strw2binl(str)
|
||||||
|
{
|
||||||
|
var nblk = ((str.length + 4) >> 5) + 1 // number of 16-word blocks
|
||||||
|
var blks = new Array(nblk * 16)
|
||||||
|
for(var i = 0; i < nblk * 16; i++) blks[i] = 0
|
||||||
|
for(var i = 0; i < str.length; i++)
|
||||||
|
blks[i>>1] |= str.charCodeAt(i) << ((i%2) * 16)
|
||||||
|
blks[i>>1] |= 0x80 << ((i%2) * 16)
|
||||||
|
blks[nblk*16-2] = str.length * 16
|
||||||
|
return blks
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* External interface
|
||||||
|
*/
|
||||||
|
function hexMD5 (str) { return binl2hex(coreMD5( str2binl(str))) }
|
||||||
|
function hexMD5w(str) { return binl2hex(coreMD5(strw2binl(str))) }
|
||||||
|
function b64MD5 (str) { return binl2b64(coreMD5( str2binl(str))) }
|
||||||
|
function b64MD5w(str) { return binl2b64(coreMD5(strw2binl(str))) }
|
||||||
|
/* Backward compatibility */
|
||||||
|
function calcMD5(str) { return binl2hex(coreMD5( str2binl(str))) }
|
||||||
62
radvert.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
<meta http-equiv="refresh" content="2; url=$(link-orig)">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
<title>Advertisement</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
<script>
|
||||||
|
var popup = '';
|
||||||
|
function openOrig() {
|
||||||
|
if (window.focus) popup.focus();
|
||||||
|
location.href = unescape('$(link-orig-esc)');
|
||||||
|
}
|
||||||
|
function openAd() {
|
||||||
|
location.href = unescape('$(link-redirect-esc)');
|
||||||
|
}
|
||||||
|
function openAdvert() {
|
||||||
|
if (window.name != 'hotspot_advert') {
|
||||||
|
popup = open('$(link-redirect)', 'hotspot_advert', '');
|
||||||
|
setTimeout("openOrig()", 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout("openAd()", 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onLoad="openAdvert()">
|
||||||
|
<div class="container" style="text-align: center;">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
</header>
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 data-i18n="adv_title">Advertisement</h1>
|
||||||
|
<p class="text-sm mt-4">
|
||||||
|
<span data-i18n="adv_msg">If nothing happens, open</span>
|
||||||
|
<a href="$(link-redirect)" target="hotspot_advert" style="text-decoration: underline; color: #50e3c2;" data-i18n="adv_link">advertisement</a>
|
||||||
|
<span data-i18n="adv_manually">manually</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
redirect.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
$(if http-status == 302)Hotspot redirect$(endif)
|
||||||
|
$(if http-header == "Location")$(link-redirect)$(endif)
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>...</title>
|
||||||
|
<meta http-equiv="refresh" content="0; url=$(link-redirect)">
|
||||||
|
<meta http-equiv="pragma" content="no-cache">
|
||||||
|
<meta http-equiv="expires" content="-1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
rlogin.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
$(if http-status == 302)Hotspot login required$(endif)
|
||||||
|
$(if http-header == "Location")$(link-redirect)$(endif)
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="refresh" content="2; url=$(link-redirect)">
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ambient-bg">
|
||||||
|
<div class="orb orb-1"></div>
|
||||||
|
<div class="orb orb-2"></div>
|
||||||
|
<div class="orb orb-3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="container" style="text-align: center;">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown" style="margin-top: 1rem;">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top: 2rem;">
|
||||||
|
<div class="flex-center mb-4">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm" data-i18n="redirect_msg">Redirecting...</p>
|
||||||
|
<p class="text-xs" style="color: var(--fg-tertiary); margin-top: 1rem;">
|
||||||
|
<a href="$(link-redirect)" style="color: inherit;" data-i18n="redirect_tip">Click here if not redirected automatically</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
158
status.html
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script src="js/languages.js"></script>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="refresh" content="$(refresh-timeout-secs)">
|
||||||
|
<title>Status</title>
|
||||||
|
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<script src="js/config.js"></script>
|
||||||
|
<script>
|
||||||
|
$(if advert-pending == 'yes')
|
||||||
|
var popup = '';
|
||||||
|
function focusAdvert() {
|
||||||
|
if (window.focus) popup.focus();
|
||||||
|
}
|
||||||
|
function openAdvert() {
|
||||||
|
popup = open('$(link-advert)', 'hotspot_advert', '');
|
||||||
|
setTimeout("focusAdvert()", 1000);
|
||||||
|
}
|
||||||
|
$(endif)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body $(if advert-pending == 'yes') onLoad="openAdvert()" $(endif)>
|
||||||
|
<div class="ambient-bg">
|
||||||
|
<div class="orb orb-1"></div>
|
||||||
|
<div class="orb orb-2"></div>
|
||||||
|
<div class="orb orb-3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Custom Language Dropdown -->
|
||||||
|
<div class="lang-dropdown">
|
||||||
|
<div class="lang-btn" onclick="toggleLangMenu()">
|
||||||
|
<img src="svg/languages.svg" alt="">
|
||||||
|
<span id="lang-label">EN</span>
|
||||||
|
</div>
|
||||||
|
<div class="lang-menu" id="lang-menu">
|
||||||
|
<div class="lang-option" data-lang="en" onclick="applyLanguage('en'); toggleLangMenu()">EN</div>
|
||||||
|
<div class="lang-option" data-lang="id" onclick="applyLanguage('id'); toggleLangMenu()">ID</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<img src="img/logo-twinpath.svg" alt="" data-asset="logo" data-brand-name class="logo-img">
|
||||||
|
<div class="status-badge" style="border-color: #50e3c2; color: #50e3c2;">
|
||||||
|
● <span data-i18n="online">Online</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Greeting Section -->
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<h2 id="greeting-text" class="greeting-text">Welcome Back!</h2>
|
||||||
|
<p class="sub-greeting" data-i18n="status_dashboard_msg">Manage your active session and monitor your usage.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Grid -->
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<!-- Time Info -->
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<div class="label">
|
||||||
|
<img src="svg/clock.svg" class="icon" style="width:14px; height:14px; margin-right:6px; opacity:0.7">
|
||||||
|
<span data-i18n="time_left">Time Remaining</span>
|
||||||
|
</div>
|
||||||
|
<div class="value text-time" data-type="mikrotik-time">$(session-time-left)</div>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar progress-time" style="width: 75%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quota Info -->
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<div class="label">
|
||||||
|
<img src="svg/ticket.svg" class="icon" style="width:14px; height:14px; margin-right:6px; opacity:0.7">
|
||||||
|
<span data-i18n="quota_left">Sisa Kuota</span>
|
||||||
|
</div>
|
||||||
|
<div class="value text-quota active-glow">
|
||||||
|
$(if remain-bytes-total)
|
||||||
|
$(remain-bytes-total-nice)
|
||||||
|
$(else)
|
||||||
|
<span data-i18n="unlimited">Unlimited</span>
|
||||||
|
$(endif)
|
||||||
|
</div>
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar progress-quota" style="width: 60%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Info -->
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<div class="label">
|
||||||
|
<img src="svg/user.svg" class="icon" style="width:14px; height:14px; margin-right:6px; opacity:0.7">
|
||||||
|
User
|
||||||
|
</div>
|
||||||
|
<div class="value">$(username)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IP Info -->
|
||||||
|
<div class="dashboard-item">
|
||||||
|
<div class="label">
|
||||||
|
<img src="svg/wifi.svg" class="icon" style="width:14px; height:14px; margin-right:6px; opacity:0.7">
|
||||||
|
IP Address
|
||||||
|
</div>
|
||||||
|
<div class="value">$(ip)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connection Stats Card -->
|
||||||
|
<div class="card" style="margin-bottom: 1.5rem;">
|
||||||
|
<h3 class="mb-4" data-i18n="connection_stats">Connection Stats</h3>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||||
|
<div>
|
||||||
|
<div class="input-label">
|
||||||
|
<img src="svg/upload-cloud.svg" class="icon" style="width:12px; height:12px; margin-right:4px; opacity:0.7">
|
||||||
|
<span data-i18n="upload">Upload</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">$(bytes-in-nice)</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="input-label">
|
||||||
|
<img src="svg/download-cloud.svg" class="icon" style="width:12px; height:12px; margin-right:4px; opacity:0.7">
|
||||||
|
<span data-i18n="download">Download</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">$(bytes-out-nice)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div style="display: grid; gap: 0.75rem;">
|
||||||
|
<button type="button" class="btn btn-outline btn-disabled" style="border-color: rgba(255,255,255,0.1); color: var(--fg-secondary);">
|
||||||
|
<img src="svg/help-circle.svg" class="icon" style="margin-right:8px; width:16px; height:16px; opacity:0.5">
|
||||||
|
Help Center
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="location.href='$(link-logout)'" class="btn btn-outline" style="border-color: #ff4d4d; color: #ff4d4d;">
|
||||||
|
<img src="svg/log-out.svg" class="icon" style="margin-right:8px; width:16px; height:16px">
|
||||||
|
<span data-i18n="logout_btn">Log Out</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><span data-i18n="powered_by">Powered by</span> <span data-brand-name></span> • <a href="#" data-brand-link="credit" target="_blank" data-brand-credit></a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Data Bridge -->
|
||||||
|
<div id="dashboard-data"
|
||||||
|
data-limit-uptime="$(limit-uptime)"
|
||||||
|
data-limit-bytes="$(limit-bytes-total)"
|
||||||
|
data-uptime="$(uptime)"
|
||||||
|
data-bytes-out="$(bytes-out)"
|
||||||
|
style="display:none">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
svg/alert-circle.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>
|
||||||
1
svg/check-circle.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#50e3c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||||
1
svg/clock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
4
svg/copy.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#aaaaaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 329 B |
1
svg/download-cloud.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M12 12v9"/><path d="m8 17 4 4 4-4"/></svg>
|
||||||
5
svg/external-link.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#50e3c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||||
|
<polyline points="15 3 21 3 21 9"></polyline>
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 362 B |
1
svg/eye-off.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
||||||
|
After Width: | Height: | Size: 412 B |
1
svg/eye.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
5
svg/help-circle.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
1
svg/image.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
|
||||||
|
After Width: | Height: | Size: 354 B |
5
svg/languages.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12" />
|
||||||
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 368 B |
1
svg/lock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||||
1
svg/log-out.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
|
||||||
1
svg/scan-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#a1a1a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/></svg>
|
||||||
1
svg/ticket.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/></svg>
|
||||||
1
svg/upload-cloud.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M12 12v9"/><path d="m16 16-4-4-4 4"/></svg>
|
||||||
1
svg/user.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||||
1
svg/wifi.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ededed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h.01"/><path d="M2 8.82a15 15 0 0 1 20 0"/><path d="M5 12.859a10 10 0 0 1 14 0"/><path d="M8.5 16.429a5 5 0 0 1 7 0"/></svg>
|
||||||
101
xml/WISPAccessGatewayParam.xsd
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified">
|
||||||
|
<xs:element name="WISPAccessGatewayParam">
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:choice>
|
||||||
|
<xs:element name="Redirect" type="RedirectType"/>
|
||||||
|
<xs:element name="Proxy" type="ProxyType"/>
|
||||||
|
<xs:element name="AuthenticationReply" type="AuthenticationReplyType"/>
|
||||||
|
<xs:element name="AuthenticationPollReply" type="AuthenticationPollReplyType"/>
|
||||||
|
<xs:element name="LogoffReply" type="LogoffReplyType"/>
|
||||||
|
<xs:element name="AbortLoginReply" type="AbortLoginReplyType"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:simpleType name="AbortLoginURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="NextURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="AccessProcedureType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="AccessLocationType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LocationNameType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LoginURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="MessageTypeType">
|
||||||
|
<xs:restriction base="xs:integer"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="ResponseCodeType">
|
||||||
|
<xs:restriction base="xs:integer"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="ReplyMessageType">
|
||||||
|
<xs:restriction base="xs:string"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LoginResultsURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="LogoffURLType">
|
||||||
|
<xs:restriction base="xs:anyURI"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="DelayType">
|
||||||
|
<xs:restriction base="xs:integer"/>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:complexType name="RedirectType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="AccessProcedure" type="AccessProcedureType"/>
|
||||||
|
<xs:element name="AccessLocation" type="AccessLocationType"/>
|
||||||
|
<xs:element name="LocationName" type="LocationNameType"/>
|
||||||
|
<xs:element name="LoginURL" type="LoginURLType"/>
|
||||||
|
<xs:element name="AbortLoginURL" type="AbortLoginURLType"/>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="ProxyType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="NextURL" type="NextURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="Delay" type="DelayType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="AuthenticationReplyType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="ReplyMessage" type="ReplyMessageType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="LoginResultsURL" type="LoginResultsURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="LogoffURL" type="LogoffURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="AuthenticationPollReplyType">
|
||||||
|
<xs:all>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="ReplyMessage" type="ReplyMessageType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="Delay" type="DelayType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
<xs:element name="LogoffURL" type="LogoffURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:all>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="LogoffReplyType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="AbortLoginReplyType">
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="MessageType" type="MessageTypeType"/>
|
||||||
|
<xs:element name="ResponseCode" type="ResponseCodeType"/>
|
||||||
|
<xs:element name="LogoffURL" type="LogoffURLType" minOccurs="0" maxOccurs="1"/>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:schema>
|
||||||
18
xml/alogin.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<AuthenticationReply>
|
||||||
|
<MessageType>120</MessageType>
|
||||||
|
<ResponseCode>50</ResponseCode>
|
||||||
|
<LogoffURL>$(link-logout)</LogoffURL>
|
||||||
|
<RedirectionURL>$(link-redirect)</RedirectionURL>
|
||||||
|
$(if radius18[0]) <ReplyMessage>$(radius18[0])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[1]) <ReplyMessage>$(radius18[1])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[2]) <ReplyMessage>$(radius18[2])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[3]) <ReplyMessage>$(radius18[3])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[4]) <ReplyMessage>$(radius18[4])</ReplyMessage> $(endif)
|
||||||
|
</AuthenticationReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
12
xml/error.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<AuthenticationReply>
|
||||||
|
<MessageType>120</MessageType>
|
||||||
|
<ResponseCode>255</ResponseCode>
|
||||||
|
<ReplyMessage>$(error)</ReplyMessage>
|
||||||
|
</AuthenticationReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
11
xml/flogout.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<LogoffReply>
|
||||||
|
<MessageType>130</MessageType>
|
||||||
|
<ResponseCode>150</ResponseCode>
|
||||||
|
</LogoffReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
22
xml/login.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<AuthenticationReply>
|
||||||
|
<MessageType>120</MessageType>
|
||||||
|
<ResponseCode>
|
||||||
|
$(if error-type == 'radius-timeout')
|
||||||
|
102
|
||||||
|
$(else)
|
||||||
|
100
|
||||||
|
$(endif)
|
||||||
|
</ResponseCode>
|
||||||
|
$(if error) <ReplyMessage>$(error)</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[1]) <ReplyMessage>$(radius18[1])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[2]) <ReplyMessage>$(radius18[2])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[3]) <ReplyMessage>$(radius18[3])</ReplyMessage> $(endif)
|
||||||
|
$(if radius18[4]) <ReplyMessage>$(radius18[4])</ReplyMessage> $(endif)
|
||||||
|
</AuthenticationReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
11
xml/logout.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<LogoffReply>
|
||||||
|
<MessageType>130</MessageType>
|
||||||
|
<ResponseCode>150</ResponseCode>
|
||||||
|
</LogoffReply>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||
15
xml/rlogin.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<HTML> <!--
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<WISPAccessGatewayParam
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://$(hostname)/xml/WISPAccessGatewayParam.xsd">
|
||||||
|
<Redirect>
|
||||||
|
<AccessProcedure>1.0</AccessProcedure>
|
||||||
|
<AccessLocation>$(location-id)</AccessLocation>
|
||||||
|
<LocationName>$(location-name)</LocationName>
|
||||||
|
<LoginURL>$(link-login-only)</LoginURL>
|
||||||
|
<MessageType>100</MessageType>
|
||||||
|
<ResponseCode>0</ResponseCode>
|
||||||
|
</Redirect>
|
||||||
|
</WISPAccessGatewayParam>
|
||||||
|
--> </HTML>
|
||||||