Spaces:
Running on Zero
Running on Zero
fix(security): restore and enhance Cloudflare Turnstile anti-bot widget before start action
Browse files- frontend/app.js +50 -18
- frontend/index.html +1 -1
- frontend/style.css +31 -9
- main.go +1 -2
frontend/app.js
CHANGED
|
@@ -16,6 +16,7 @@ function trackEvent(eventName, eventParams = {}) {
|
|
| 16 |
}
|
| 17 |
|
| 18 |
const DEFAULT_HF_BACKEND = 'https://ilhamdev-wa-full-pfp.hf.space';
|
|
|
|
| 19 |
|
| 20 |
function getDefaultBackendUrl() {
|
| 21 |
const saved = localStorage.getItem('wa_pfp_backend_url');
|
|
@@ -47,8 +48,8 @@ const state = {
|
|
| 47 |
socket: null,
|
| 48 |
backendUrl: getDefaultBackendUrl(),
|
| 49 |
isProcessing: false,
|
| 50 |
-
turnstileEnabled:
|
| 51 |
-
turnstileSiteKey:
|
| 52 |
turnstileToken: null,
|
| 53 |
turnstileWidgetId: null,
|
| 54 |
};
|
|
@@ -139,13 +140,16 @@ let isWakingUp = false;
|
|
| 139 |
function updateStartButtonState() {
|
| 140 |
if (!state.currentImageBase64) {
|
| 141 |
btnStart.disabled = true;
|
|
|
|
| 142 |
return;
|
| 143 |
}
|
| 144 |
if (state.turnstileEnabled && !state.turnstileToken) {
|
| 145 |
btnStart.disabled = true;
|
|
|
|
| 146 |
return;
|
| 147 |
}
|
| 148 |
btnStart.disabled = false;
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
function resetTurnstile() {
|
|
@@ -159,13 +163,21 @@ function resetTurnstile() {
|
|
| 159 |
}
|
| 160 |
if (turnstileFeedback) {
|
| 161 |
turnstileFeedback.textContent = '';
|
| 162 |
-
turnstileFeedback.className = 'turnstile-
|
| 163 |
}
|
| 164 |
updateStartButtonState();
|
| 165 |
}
|
| 166 |
|
| 167 |
let turnstileRetryCount = 0;
|
| 168 |
-
const MAX_TURNSTILE_RETRIES =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
function renderTurnstileWidget() {
|
| 171 |
if (!state.turnstileEnabled || !state.turnstileSiteKey) {
|
|
@@ -184,12 +196,12 @@ function renderTurnstileWidget() {
|
|
| 184 |
const currentTheme = document.documentElement.getAttribute('data-theme') || 'auto';
|
| 185 |
state.turnstileWidgetId = window.turnstile.render('#turnstile-widget', {
|
| 186 |
sitekey: state.turnstileSiteKey,
|
| 187 |
-
theme: currentTheme,
|
| 188 |
callback: (token) => {
|
| 189 |
state.turnstileToken = token;
|
| 190 |
if (turnstileFeedback) {
|
| 191 |
turnstileFeedback.textContent = 'Verifikasi keamanan berhasil.';
|
| 192 |
-
turnstileFeedback.className = 'turnstile-
|
| 193 |
}
|
| 194 |
trackEvent('turnstile_success');
|
| 195 |
updateStartButtonState();
|
|
@@ -197,8 +209,8 @@ function renderTurnstileWidget() {
|
|
| 197 |
'expired-callback': () => {
|
| 198 |
state.turnstileToken = null;
|
| 199 |
if (turnstileFeedback) {
|
| 200 |
-
turnstileFeedback.textContent = 'Verifikasi kedaluwarsa. Silakan
|
| 201 |
-
turnstileFeedback.className = 'turnstile-
|
| 202 |
}
|
| 203 |
updateStartButtonState();
|
| 204 |
if (window.turnstile && state.turnstileWidgetId !== null) {
|
|
@@ -208,8 +220,8 @@ function renderTurnstileWidget() {
|
|
| 208 |
'error-callback': () => {
|
| 209 |
state.turnstileToken = null;
|
| 210 |
if (turnstileFeedback) {
|
| 211 |
-
turnstileFeedback.textContent = 'Gagal memuat
|
| 212 |
-
turnstileFeedback.className = 'turnstile-
|
| 213 |
}
|
| 214 |
updateStartButtonState();
|
| 215 |
},
|
|
@@ -224,7 +236,7 @@ function renderTurnstileWidget() {
|
|
| 224 |
} else {
|
| 225 |
if (turnstileFeedback) {
|
| 226 |
turnstileFeedback.textContent = 'Gagal memuat Cloudflare Turnstile. Periksa koneksi internet atau matikan ad-blocker.';
|
| 227 |
-
turnstileFeedback.className = 'turnstile-
|
| 228 |
}
|
| 229 |
}
|
| 230 |
}
|
|
@@ -239,16 +251,21 @@ async function fetchBackendConfig() {
|
|
| 239 |
});
|
| 240 |
if (res.ok) {
|
| 241 |
const data = await res.json();
|
| 242 |
-
if (data.turnstile_enabled && data.turnstile_site_key) {
|
| 243 |
state.turnstileEnabled = true;
|
| 244 |
-
|
|
|
|
|
|
|
| 245 |
renderTurnstileWidget();
|
| 246 |
} else {
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
}
|
| 254 |
} catch (err) {
|
|
@@ -764,6 +781,17 @@ function applyTheme(theme) {
|
|
| 764 |
if (themeIconSun) themeIconSun.classList.add('hidden');
|
| 765 |
if (themeIconMoon) themeIconMoon.classList.remove('hidden');
|
| 766 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
}
|
| 768 |
|
| 769 |
function initTheme() {
|
|
@@ -794,6 +822,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 794 |
initSettings();
|
| 795 |
initCopyButton();
|
| 796 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
btnStart.addEventListener('click', startProcess);
|
| 798 |
btnCancelSession.addEventListener('click', cancelSession);
|
| 799 |
btnRestart.addEventListener('click', restartApp);
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
const DEFAULT_HF_BACKEND = 'https://ilhamdev-wa-full-pfp.hf.space';
|
| 19 |
+
const DEFAULT_TURNSTILE_SITE_KEY = '0x4AAAAAAFQXC-hDYo4lIswr';
|
| 20 |
|
| 21 |
function getDefaultBackendUrl() {
|
| 22 |
const saved = localStorage.getItem('wa_pfp_backend_url');
|
|
|
|
| 48 |
socket: null,
|
| 49 |
backendUrl: getDefaultBackendUrl(),
|
| 50 |
isProcessing: false,
|
| 51 |
+
turnstileEnabled: true,
|
| 52 |
+
turnstileSiteKey: DEFAULT_TURNSTILE_SITE_KEY,
|
| 53 |
turnstileToken: null,
|
| 54 |
turnstileWidgetId: null,
|
| 55 |
};
|
|
|
|
| 140 |
function updateStartButtonState() {
|
| 141 |
if (!state.currentImageBase64) {
|
| 142 |
btnStart.disabled = true;
|
| 143 |
+
btnStart.title = 'Pilih foto profil terlebih dahulu';
|
| 144 |
return;
|
| 145 |
}
|
| 146 |
if (state.turnstileEnabled && !state.turnstileToken) {
|
| 147 |
btnStart.disabled = true;
|
| 148 |
+
btnStart.title = 'Selesaikan verifikasi keamanan Cloudflare di atas terlebih dahulu';
|
| 149 |
return;
|
| 150 |
}
|
| 151 |
btnStart.disabled = false;
|
| 152 |
+
btnStart.title = 'Mulai pasang foto profil';
|
| 153 |
}
|
| 154 |
|
| 155 |
function resetTurnstile() {
|
|
|
|
| 163 |
}
|
| 164 |
if (turnstileFeedback) {
|
| 165 |
turnstileFeedback.textContent = '';
|
| 166 |
+
turnstileFeedback.className = 'turnstile-note';
|
| 167 |
}
|
| 168 |
updateStartButtonState();
|
| 169 |
}
|
| 170 |
|
| 171 |
let turnstileRetryCount = 0;
|
| 172 |
+
const MAX_TURNSTILE_RETRIES = 20;
|
| 173 |
+
|
| 174 |
+
// Callback otomatis saat script Cloudflare Turnstile selesai dimuat
|
| 175 |
+
window.onloadTurnstileCallback = function() {
|
| 176 |
+
console.log('[Turnstile] Cloudflare Turnstile API siap.');
|
| 177 |
+
if (state.turnstileEnabled && state.turnstileSiteKey) {
|
| 178 |
+
renderTurnstileWidget();
|
| 179 |
+
}
|
| 180 |
+
};
|
| 181 |
|
| 182 |
function renderTurnstileWidget() {
|
| 183 |
if (!state.turnstileEnabled || !state.turnstileSiteKey) {
|
|
|
|
| 196 |
const currentTheme = document.documentElement.getAttribute('data-theme') || 'auto';
|
| 197 |
state.turnstileWidgetId = window.turnstile.render('#turnstile-widget', {
|
| 198 |
sitekey: state.turnstileSiteKey,
|
| 199 |
+
theme: currentTheme === 'light' ? 'light' : 'dark',
|
| 200 |
callback: (token) => {
|
| 201 |
state.turnstileToken = token;
|
| 202 |
if (turnstileFeedback) {
|
| 203 |
turnstileFeedback.textContent = 'Verifikasi keamanan berhasil.';
|
| 204 |
+
turnstileFeedback.className = 'turnstile-note success';
|
| 205 |
}
|
| 206 |
trackEvent('turnstile_success');
|
| 207 |
updateStartButtonState();
|
|
|
|
| 209 |
'expired-callback': () => {
|
| 210 |
state.turnstileToken = null;
|
| 211 |
if (turnstileFeedback) {
|
| 212 |
+
turnstileFeedback.textContent = 'Verifikasi kedaluwarsa. Silakan centang ulang.';
|
| 213 |
+
turnstileFeedback.className = 'turnstile-note error';
|
| 214 |
}
|
| 215 |
updateStartButtonState();
|
| 216 |
if (window.turnstile && state.turnstileWidgetId !== null) {
|
|
|
|
| 220 |
'error-callback': () => {
|
| 221 |
state.turnstileToken = null;
|
| 222 |
if (turnstileFeedback) {
|
| 223 |
+
turnstileFeedback.textContent = 'Gagal memuat verifikasi Cloudflare Turnstile.';
|
| 224 |
+
turnstileFeedback.className = 'turnstile-note error';
|
| 225 |
}
|
| 226 |
updateStartButtonState();
|
| 227 |
},
|
|
|
|
| 236 |
} else {
|
| 237 |
if (turnstileFeedback) {
|
| 238 |
turnstileFeedback.textContent = 'Gagal memuat Cloudflare Turnstile. Periksa koneksi internet atau matikan ad-blocker.';
|
| 239 |
+
turnstileFeedback.className = 'turnstile-note error';
|
| 240 |
}
|
| 241 |
}
|
| 242 |
}
|
|
|
|
| 251 |
});
|
| 252 |
if (res.ok) {
|
| 253 |
const data = await res.json();
|
| 254 |
+
if (data.turnstile_enabled !== false && (data.turnstile_site_key || state.turnstileSiteKey)) {
|
| 255 |
state.turnstileEnabled = true;
|
| 256 |
+
if (data.turnstile_site_key) {
|
| 257 |
+
state.turnstileSiteKey = data.turnstile_site_key;
|
| 258 |
+
}
|
| 259 |
renderTurnstileWidget();
|
| 260 |
} else {
|
| 261 |
+
// Jika server lokal secara sengaja mematikan Turnstile
|
| 262 |
+
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
| 263 |
+
state.turnstileEnabled = false;
|
| 264 |
+
state.turnstileSiteKey = null;
|
| 265 |
+
state.turnstileToken = null;
|
| 266 |
+
if (turnstileWrapper) turnstileWrapper.classList.add('hidden');
|
| 267 |
+
updateStartButtonState();
|
| 268 |
+
}
|
| 269 |
}
|
| 270 |
}
|
| 271 |
} catch (err) {
|
|
|
|
| 781 |
if (themeIconSun) themeIconSun.classList.add('hidden');
|
| 782 |
if (themeIconMoon) themeIconMoon.classList.remove('hidden');
|
| 783 |
}
|
| 784 |
+
|
| 785 |
+
// Sinkronkan tema Turnstile dengan tema halaman
|
| 786 |
+
if (window.turnstile && state.turnstileWidgetId !== null) {
|
| 787 |
+
try {
|
| 788 |
+
window.turnstile.remove(state.turnstileWidgetId);
|
| 789 |
+
state.turnstileWidgetId = null;
|
| 790 |
+
renderTurnstileWidget();
|
| 791 |
+
} catch (e) {
|
| 792 |
+
// Abaikan jika tidak didukung
|
| 793 |
+
}
|
| 794 |
+
}
|
| 795 |
}
|
| 796 |
|
| 797 |
function initTheme() {
|
|
|
|
| 822 |
initSettings();
|
| 823 |
initCopyButton();
|
| 824 |
|
| 825 |
+
// Inisialisasi awal Turnstile & konfigurasi backend
|
| 826 |
+
renderTurnstileWidget();
|
| 827 |
+
fetchBackendConfig();
|
| 828 |
+
|
| 829 |
btnStart.addEventListener('click', startProcess);
|
| 830 |
btnCancelSession.addEventListener('click', cancelSession);
|
| 831 |
btnRestart.addEventListener('click', restartApp);
|
frontend/index.html
CHANGED
|
@@ -105,7 +105,7 @@
|
|
| 105 |
<link rel="stylesheet" href="style.css">
|
| 106 |
|
| 107 |
<!-- Cloudflare Turnstile API -->
|
| 108 |
-
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
| 109 |
</head>
|
| 110 |
<body>
|
| 111 |
<div class="app-layout">
|
|
|
|
| 105 |
<link rel="stylesheet" href="style.css">
|
| 106 |
|
| 107 |
<!-- Cloudflare Turnstile API -->
|
| 108 |
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback&render=explicit" async defer></script>
|
| 109 |
</head>
|
| 110 |
<body>
|
| 111 |
<div class="app-layout">
|
frontend/style.css
CHANGED
|
@@ -1100,35 +1100,57 @@ button, input, select, textarea {
|
|
| 1100 |
}
|
| 1101 |
|
| 1102 |
/* ==========================================================================
|
| 1103 |
-
Turnstile
|
| 1104 |
========================================================================== */
|
| 1105 |
|
| 1106 |
-
.turnstile-container
|
|
|
|
| 1107 |
display: flex;
|
| 1108 |
flex-direction: column;
|
| 1109 |
align-items: center;
|
| 1110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
}
|
| 1113 |
|
| 1114 |
-
.turnstile-slot
|
|
|
|
| 1115 |
min-height: 65px;
|
|
|
|
|
|
|
|
|
|
| 1116 |
max-width: 100%;
|
| 1117 |
overflow-x: auto;
|
| 1118 |
}
|
| 1119 |
|
| 1120 |
-
.turnstile-note
|
| 1121 |
-
|
| 1122 |
-
font-
|
|
|
|
| 1123 |
margin-top: 8px;
|
| 1124 |
text-align: center;
|
|
|
|
|
|
|
| 1125 |
}
|
| 1126 |
|
| 1127 |
-
.turnstile-note.success
|
|
|
|
| 1128 |
color: var(--color-success);
|
| 1129 |
}
|
| 1130 |
|
| 1131 |
-
.turnstile-note.error
|
|
|
|
| 1132 |
color: var(--color-secondary);
|
| 1133 |
}
|
| 1134 |
|
|
|
|
| 1100 |
}
|
| 1101 |
|
| 1102 |
/* ==========================================================================
|
| 1103 |
+
Cloudflare Turnstile Anti-Bot Verification
|
| 1104 |
========================================================================== */
|
| 1105 |
|
| 1106 |
+
.turnstile-container,
|
| 1107 |
+
.turnstile-wrapper {
|
| 1108 |
display: flex;
|
| 1109 |
flex-direction: column;
|
| 1110 |
align-items: center;
|
| 1111 |
+
justify-content: center;
|
| 1112 |
+
margin: 18px 0 14px;
|
| 1113 |
+
padding: 14px 16px;
|
| 1114 |
+
background-color: var(--bg-canvas);
|
| 1115 |
+
border: 2px solid var(--border-color);
|
| 1116 |
+
border-radius: var(--radius-md);
|
| 1117 |
+
box-shadow: 2px 2px 0 var(--shadow-color);
|
| 1118 |
width: 100%;
|
| 1119 |
+
transition: border-color var(--speed-normal), box-shadow var(--speed-normal);
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
.turnstile-container:hover,
|
| 1123 |
+
.turnstile-wrapper:hover {
|
| 1124 |
+
border-color: var(--color-primary);
|
| 1125 |
}
|
| 1126 |
|
| 1127 |
+
.turnstile-slot,
|
| 1128 |
+
.turnstile-widget {
|
| 1129 |
min-height: 65px;
|
| 1130 |
+
display: flex;
|
| 1131 |
+
align-items: center;
|
| 1132 |
+
justify-content: center;
|
| 1133 |
max-width: 100%;
|
| 1134 |
overflow-x: auto;
|
| 1135 |
}
|
| 1136 |
|
| 1137 |
+
.turnstile-note,
|
| 1138 |
+
.turnstile-feedback {
|
| 1139 |
+
font-size: 0.82rem;
|
| 1140 |
+
font-weight: 700;
|
| 1141 |
margin-top: 8px;
|
| 1142 |
text-align: center;
|
| 1143 |
+
color: var(--text-sub);
|
| 1144 |
+
line-height: 1.4;
|
| 1145 |
}
|
| 1146 |
|
| 1147 |
+
.turnstile-note.success,
|
| 1148 |
+
.turnstile-feedback.success {
|
| 1149 |
color: var(--color-success);
|
| 1150 |
}
|
| 1151 |
|
| 1152 |
+
.turnstile-note.error,
|
| 1153 |
+
.turnstile-feedback.error {
|
| 1154 |
color: var(--color-secondary);
|
| 1155 |
}
|
| 1156 |
|
main.go
CHANGED
|
@@ -128,8 +128,7 @@ func registerConfigEndpoint(mux *http.ServeMux) {
|
|
| 128 |
w.Header().Set("Content-Type", "application/json")
|
| 129 |
|
| 130 |
siteKey := os.Getenv("TURNSTILE_SITE_KEY")
|
| 131 |
-
|
| 132 |
-
isEnabled := siteKey != "" && secretKey != ""
|
| 133 |
|
| 134 |
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
| 135 |
"turnstile_enabled": isEnabled,
|
|
|
|
| 128 |
w.Header().Set("Content-Type", "application/json")
|
| 129 |
|
| 130 |
siteKey := os.Getenv("TURNSTILE_SITE_KEY")
|
| 131 |
+
isEnabled := siteKey != ""
|
|
|
|
| 132 |
|
| 133 |
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
| 134 |
"turnstile_enabled": isEnabled,
|