Initial commit: Premium Gold Theme with Dynamic QR and Multi-language support

This commit is contained in:
dyzulk
2026-01-12 07:57:04 +07:00
commit 7bd6589564
49 changed files with 2876 additions and 0 deletions

57
alogin.html Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
img/logo-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

32
img/logo-twinpath.svg Normal file
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

158
js/languages.js Normal file
View 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
View 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
View 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
View 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
View 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)&amp;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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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>