1// Shared Username System 2// Manages a single username across all leaderboards on the site 3 4(function() { 5 try { 6 if (typeof window !== 'undefined') { 7 try { 8 window.__PUPWIFE_DEBUG__ = typeof localStorage !== 'undefined' && localStorage.getItem('pupwife_debug') === '1'; 9 } catch (e) { 10 window.__PUPWIFE_DEBUG__ = false; 11 } 12 if (typeof window.devLog !== 'function') { 13 window.devLog = function() { 14 if (window.__PUPWIFE_DEBUG__ && typeof console !== 'undefined' && console.log) { 15 console.log.apply(console, arguments); 16 } 17 }; 18 } 19 if (typeof window.devWarn !== 'function') { 20 window.devWarn = function() { 21 if (window.__PUPWIFE_DEBUG__ && typeof console !== 'undefined' && console.warn) { 22 console.warn.apply(console, arguments); 23 } 24 }; 25 } 26 } 27 } catch (e) {} 28 29 function openInfoPage(href) { 30 var route = String(href || '').replace(/^\//, ''); 31 if (!route) return; 32 if (window.CitrusDesktop && typeof window.CitrusDesktop.openHref === 'function') { 33 if (window.CitrusDesktop.openHref('/' + route)) return; 34 } 35 window.location.href = '/index.html?open=' + route; 36 } 37 38 function emitDevLog() { 39 if (typeof window !== 'undefined' && typeof window.devLog === 'function') { 40 window.devLog.apply(window, arguments); 41 } 42 } 43 function emitDevWarn() { 44 if (typeof window !== 'undefined' && typeof window.devWarn === 'function') { 45 window.devWarn.apply(window, arguments); 46 } 47 } 48 49 function ensureAuthModalCss() { 50 if (typeof document === 'undefined') return; 51 if (document.querySelector('link[data-pupwife-auth-modal-css]')) return; 52 const link = document.createElement('link'); 53 link.rel = 'stylesheet'; 54 link.href = '/css/shared-username-modal.css'; 55 link.setAttribute('data-pupwife-auth-modal-css', '1'); 56 document.head.appendChild(link); 57 } 58 59 function isSiteDarkMode() { 60 try { 61 if (document.body && document.body.classList.contains('dark-mode')) return true; 62 const settings = localStorage.getItem('puppywifey_settings'); 63 if (settings) { 64 const parsed = JSON.parse(settings); 65 if (parsed && parsed.darkMode) return true; 66 } 67 } catch (e) {} 68 return false; 69 } 70 71 function createAuthOverlay(stacked) { 72 ensureAuthModalCss(); 73 const overlay = document.createElement('div'); 74 overlay.className = 'pupwife-auth-overlay' + (stacked ? ' pupwife-auth-overlay--stack' : ''); 75 if (isSiteDarkMode()) overlay.classList.add('is-dark'); 76 return overlay; 77 } 78 79 function createAuthModal(wide) { 80 const modal = document.createElement('div'); 81 modal.className = 'pupwife-auth-modal' + (wide ? ' pupwife-auth-modal--wide' : ''); 82 return modal; 83 } 84 85 const USERNAME_KEY = 'puppy_shared_username'; 86 const HAS_VISITED_INDEX_KEY = 'puppy_has_visited_index'; 87 88 // Firebase references (will be set when Firebase loads) 89 let usernamesRef = null; 90 let fishFoodRef = null; 91 let userFingerprint = null; 92 let userPassword = null; // Store password for current session 93 94 // Banned word list (slurs only, not curse words) - shared for usernames, passwords, and chatroom messages 95 // Note: "faggot" is explicitly excluded as requested 96 // White/southern slurs (cracker, redneck, hillbilly, etc.) are also excluded as requested 97 const BANNED_WORDS = [ 98 // Racial slurs - African American 99 'nigger', 'nigga', 'n1gger', 'n1gga', 'n!gger', 'n!gga', 'nigg3r', 'nigg@', 'n!gg3r', 'n!gg@', 100 'coon', 'c00n', 'c00ny', 'c0on', 101 'porchmonkey', 'porchm0nkey', 'porchm0nk3y', 102 'junglebunny', 'jungl3bunny', 'jungl3bunn3y', 103 'tarbaby', 't4rbaby', 't@rbaby', 't4rb4by', 104 'mudshark', 'mudsh4rk', 'mudsh@rk', 105 'oreo', '0r30', '0re0', 106 'darky', 'd4rky', 'd@rky', 107 'groid', 'gr01d', 'gr0!d', 108 'mudblood', 'mudbl00d', 'mudbl00d', 109 'ape', '4p3', '@p3', 110 'monkey', 'm0nk3y', 'm0nk3y', 111 // Racial slurs - Asian 112 'chink', 'ch1nk', 'ch!nk', 'ch!nky', 'ch1nky', 113 'gook', 'g00k', 'g00ky', 114 'jap', 'j4p', 'j@p', 115 'slant', 'sl4nt', 'sl@nt', 116 'zipperhead', 'z1pp3rh3ad', 'z!pp3rh3@d', 117 'dink', 'd1nk', 'd!nk', 118 'slope', 'sl0p3', 'sl0p3', 119 'slanteye', 'sl4nt3y3', 'sl@nt3y3', 120 'flatface', 'fl4tf4c3', 'fl@tf@c3', 121 'chinaman', 'ch1n4m4n', 'ch!n@m@n', 122 'coolie', 'c00l13', 'c00l!3', 123 'freshofftheboat', 'fr3sh0ffth3b04t', 'fr3sh0ffth3b0@t', 124 'hapa', 'h4p4', 'h@p@', 125 'hafu', 'h4fu', 'h@fu', 126 // Racial slurs - Hispanic/Latino 127 'spic', 'sp1c', 'sp!c', 'sp!ck', 'sp1ck', 128 'wetback', 'wetb4ck', 'wetb@ck', 'w3tb4ck', 129 'beaner', 'b34n3r', 'b3an3r', 130 'cholo', 'chol0', 'ch0lo', 131 'greaser', 'gr34s3r', 'gr34s3r', 132 'borderhopper', 'b0rd3rh0pp3r', 'b0rd3rh0pp3r', 133 'anchor baby', '4nch0rb4by', '@nch0rb@by', 134 // Racial slurs - Middle Eastern 135 'towelhead', 'towelh3ad', 'towelh@ad', 't0w3lh3ad', 't0w3lh3@d', 136 'raghead', 'r4gh3ad', 'r@gh3@d', 137 'cameljockey', 'c4m3lj0ck3y', 'c@m3lj0ck3y', 138 'sandnigger', 'sandn1gger', 'sandn!gger', 's4ndn1gg3r', 's@ndn!gg3r', 139 'haji', 'h4j1', 'h@j!', 140 'hajji', 'h4jj1', 'h@jj!', 141 // Racial slurs - Jewish 142 'kike', 'k1ke', 'k!ke', 'kik3', 143 'yid', 'y1d', 'y!d', 144 'heeb', 'h33b', 'h33b', 145 'jewboy', 'j3wb0y', 'j3wb0y', 146 // Racial slurs - Native American 147 'redskin', 'r3dsk1n', 'r3dsk!n', 148 'squaw', 'squ4w', 'squ@w', 149 'injun', '!njun', '!njun', 150 'papoose', 'p4p00s3', 'p@p00s3', 151 'savage', 's4v4g3', 's@v@g3', 152 // Racial slurs - Mixed race/obscure 153 'halfbreed', 'h4lfbr33d', 'h@lfbr33d', 154 'halfcaste', 'h4lfc4st3', 'h@lfc@st3', 155 'mulatto', 'mul4tt0', 'mul@tt0', 156 'quadroon', 'qu4dr00n', 'qu@dr00n', 157 'octaroon', '0ct4r00n', '0ct@r00n', 158 'mestizo', 'm3st1z0', 'm3st!z0', 159 'halfie', 'h4lf13', 'h@lf!3', 160 // Racial slurs - Australian/Aboriginal 161 'abo', '4b0', '@b0', 162 'boong', 'b00ng', 'b00ng', 163 'golliwog', 'g0ll1w0g', 'g0ll!w0g', 164 // LGBTQ+ slurs (excluding faggot as requested) 165 'tranny', 'tr4nny', 'tr@nny', 'tr4nn!e', 'tr@nn!e', 166 'shemale', 'sh3m4l3', 'sh3m@l3', 167 'heshe', 'h3sh3', 'h3-sh3', 168 // Disability slurs 169 'retard', 'ret4rd', 'ret@rd', 'r3tard', 'r3t4rd', 'r3t@rd', 170 'mongoloid', 'mongol01d', 'mongol0id', 'm0ng0l01d', 171 'cripple', 'cr1ppl3', 'cr!ppl3', 172 'gimp', 'g1mp', 'g!mp', 173 'downie', 'd0wn13', 'd0wn!3', 174 'spacker', 'sp4ck3r', 'sp@ck3r', 175 // Other offensive ethnic/religious terms 176 'paki', 'p4k1', 'p@k!', 177 'gypsy', 'gyp5y', 'gyps!', 178 'pikey', 'p1k3y', 'p!k3y', 179 'chav', 'ch4v', 'ch@v' 180 ]; 181 182 // Convert leet speak to normal letters for detection 183 function normalizeLeetSpeak(text) { 184 return text.toLowerCase() 185 .replace(/4/g, 'a') 186 .replace(/@/g, 'a') 187 .replace(/3/g, 'e') 188 .replace(/1/g, 'i') 189 .replace(/!/g, 'i') 190 .replace(/0/g, 'o') 191 .replace(/5/g, 's') 192 .replace(/\$/g, 's') 193 .replace(/7/g, 't') 194 .replace(/\+/g, 't') 195 .replace(/[à áâãäå]/g, 'a') 196 .replace(/[èéêë]/g, 'e') 197 .replace(/[ìÃîï]/g, 'i') 198 .replace(/[òóôõö]/g, 'o') 199 .replace(/[ùúûü]/g, 'u') 200 .replace(/[ýÿ]/g, 'y') 201 .replace(/[ñ]/g, 'n') 202 .replace(/[ç]/g, 'c') 203 .replace(/æ/g, 'ae') 204 .replace(/Å/g, 'oe') 205 .replace(/Ã/g, 'ss') 206 .replace(/Â¥/g, 'y') 207 .replace(/¢/g, 'c') 208 .replace(/°/g, 'o') 209 .replace(/©/g, 'c') 210 .replace(/®/g, 'r') 211 .replace(/â/g, 'a') 212 .replace(/\|/g, 'i') 213 .replace(/â¢/g, 'o'); 214 } 215 216 function escapeRegex(str) { 217 return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 218 } 219 220 // Whole-word / whole-phrase matchers (substring includes() caused false positives: grape, Japan, raccoon, etc.) 221 const BANNED_WORD_MATCHERS = (function buildBannedWordMatchers() { 222 const seen = new Set(); 223 const matchers = []; 224 for (const raw of BANNED_WORDS) { 225 const normalized = normalizeLeetSpeak(raw).trim(); 226 if (!normalized || seen.has(normalized)) continue; 227 seen.add(normalized);
228 const parts = normalized.split(/\s+/).filter(Boolean); 229 if (!parts.length) continue; 230 const pattern = parts.map(escapeRegex).join('\\s+'); 231 matchers.push({ 232 term: normalized, 233 re: new RegExp('(?:^|[^a-z0-9])' + pattern + '(?:[^a-z0-9]|$)', 'i') 234 }); 235 } 236 return matchers; 237 })(); 238 239 // Check if text contains any banned words. Returns the banned word found, or null. 240 function containsBannedWord(text) { 241 if (!text || typeof text !== 'string') return null; 242 const normalized = normalizeLeetSpeak(text); 243 for (const matcher of BANNED_WORD_MATCHERS) { 244 if (matcher.re.test(normalized)) { 245 return matcher.term; 246 } 247 } 248 return null; 249 } 250 251 // Initialize Firebase refs 252 function initFirebase() { 253 if (typeof firebase === 'undefined' || !firebase.database) { 254 return false; 255 } 256 if (typeof window.ensureFirebaseApp === 'function' && !window.ensureFirebaseApp()) { 257 return false; 258 } 259 try { 260 firebase.app(); 261 } catch (e) { 262 return false; 263 } 264 265 usernamesRef = firebase.database().ref('usernames'); 266 fishFoodRef = firebase.database().ref('fishFoodScores'); 267 268 userFingerprint = generateFingerprint(); 269 270 checkNotifications().catch(function (err) { 271 emitDevWarn('Could not check notifications (permission denied):', err); 272 }); 273 274 return true; 275 } 276 277 // Update IP address for already logged-in users (throttled to avoid performance issues) 278 async function updateIPForLoggedInUser(username) { 279 // Only update if user has a password (is properly logged in, not guest) 280 const hasPassword = localStorage.getItem('puppy_username_password'); 281 const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true'; 282 283 if (!hasPassword || isGuestMode || !username) { 284 return; // Not a logged-in user 285 } 286 287 // Throttle: Only update IP if it hasn't been updated in the last hour 288 const lastIPUpdateKey = `puppy_last_ip_update_${username}`; 289 const lastUpdate = localStorage.getItem(lastIPUpdateKey); 290 const now = Date.now(); 291 const oneHour = 60 * 60 * 1000; // 1 hour in milliseconds 292 293 if (lastUpdate && (now - parseInt(lastUpdate)) < oneHour) { 294 // IP was updated recently, skip to avoid performance issues 295 return; 296 } 297 298 // Run asynchronously without blocking 299 setTimeout(async () => { 300 try { 301 const userIP = await getUserIP(); 302 if (userIP && firebase && firebase.database) { 303 const userIPsRef = firebase.database().ref('admin/userIPs'); 304 const ipData = { 305 ipAddress: userIP, 306 ipLastUpdated: Date.now() 307 }; 308 await userIPsRef.child(username).set(ipData); 309 // Store timestamp to throttle future updates 310 localStorage.setItem(lastIPUpdateKey, now.toString()); 311 } else { 312 if (!userIP) { 313 emitDevWarn(`â ï¸ Could not get IP address for ${username} - IP fetch failed or was blocked`); 314 } 315 } 316 } catch (error) { 317 console.error('â Could not update IP address for logged-in user:', error); 318 console.error(' Error details:', { 319 code: error.code, 320 message: error.message, 321 username: username 322 }); 323 } 324 }, 0); // Run in next event loop tick to avoid blocking 325 } 326 327 // Check if existing user needs to add a password 328 async function checkPasswordRequirement() { 329 const existingUsername = localStorage.getItem(USERNAME_KEY); 330 if (!existingUsername) return; 331 332 // Check localStorage first (faster, no Firebase permission needed) 333 const storedPassword = localStorage.getItem('puppy_username_password'); 334 335 // Always try to verify with Firebase to see if password exists there 336 if (usernamesRef) { 337 try { 338 const usernameData = await usernamesRef.child(existingUsername).once('value'); 339 if (usernameData.exists()) { 340 const data = usernameData.val(); 341 // Check if it's old format (just fingerprint) or new format (object with fingerprint and password) 342 if (typeof data === 'string' || !data.password) { 343 // Old format or no password in Firebase - prompt to add one 344 // Clear localStorage password if it exists (might be stale) 345 if (storedPassword) { 346 localStorage.removeItem('puppy_username_password'); 347 } 348 promptForPassword(existingUsername, true); 349 return; 350 } else { 351 // Has password in Firebase 352 if (!storedPassword) { 353 // No password in localStorage - need to login 354 promptForLogin(existingUsername); 355 return; 356 } else { 357 // Verify password matches 358 if (storedPassword !== data.password) { 359 // Password doesn't match - need to login again 360 promptForLogin(existingUsername); 361 return; 362 } else { 363 // Password matches - we're logged in 364 userPassword = storedPassword; 365 return; 366 } 367 } 368 } 369 } else { 370 // Username doesn't exist in Firebase - prompt to add password 371 if (storedPassword) { 372 localStorage.removeItem('puppy_username_password'); 373 } 374 promptForPassword(existingUsername, true); 375 return; 376 } 377 } catch (error) { 378 // Firebase permission denied or other error 379 emitDevWarn('Could not check password in Firebase (permission denied):', error); 380 381 // If no password in localStorage, prompt to add one 382 if (!storedPassword) { 383 promptForPassword(existingUsername, true); 384 return; 385 } else { 386 // We have a password in localStorage but can't verify with Firebase 387 // Trust localStorage - user is logged in 388 userPassword = storedPassword; 389 emitDevLog('â Using password from localStorage (Firebase read blocked) - user is logged in'); 390 // Don't prompt again - user is already logged in 391 return; 392 } 393 } 394 } else { 395 // No Firebase ref yet 396 if (!storedPassword) { 397 // No password in localStorage - prompt to add one 398 promptForPassword(existingUsername, true); 399 return; 400 } else { 401 // We have a password in localStorage - use it 402 userPassword = storedPassword; 403 } 404 } 405 } 406 407 // Check for admin notifications 408 async function checkNotifications() { 409 if (!userFingerprint) return; 410 411 try {
412 const notificationsRef = firebase.database().ref('notifications'); 413 const snapshot = await notificationsRef.once('value'); 414 415 const unreadNotifications = []; 416 snapshot.forEach((childSnapshot) => { 417 const notification = childSnapshot.val(); 418 if (!notification || notification.fingerprint !== userFingerprint) return; 419 if (!notification.read) { 420 unreadNotifications.push({ 421 key: childSnapshot.key, 422 ...notification 423 }); 424 } 425 }); 426 427 if (unreadNotifications.length > 0) { 428 // Show the most recent notification 429 const latest = unreadNotifications.sort((a, b) => b.timestamp - a.timestamp)[0]; 430 showNotification(latest); 431 } 432 } catch (error) { 433 console.error('Error checking notifications:', error); 434 } 435 } 436 437 // Show notification popup to user 438 function showNotification(notification) { 439 const overlay = createAuthOverlay(true); 440 const modal = createAuthModal(); 441 modal.style.maxWidth = '500px'; 442 443 let actionButton = ''; 444 if (notification.type === 'username_changed' && notification.newUsername) { 445 actionButton = `<button id="notification-ok" style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 446 background-color: #c8a8ff; color: white; border: none; border-radius: 8px; cursor: pointer; 447 font-family: 'Comic Sans MS', sans-serif;">Got it! â¨</button>`; 448 } else if (notification.type === 'username_deleted') { 449 actionButton = `<button id="notification-ok" style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 450 background-color: #c8a8ff; color: white; border: none; border-radius: 8px; cursor: pointer; 451 font-family: 'Comic Sans MS', sans-serif;">Choose New Username</button>`; 452 } 453 454 modal.innerHTML = ` 455 <h2 style="color: #633aac; margin-top: 0;">ð¬ Important Notice</h2> 456 <p style="color: #555; margin: 16px 0; line-height: 1.6;">${notification.message}</p> 457 ${actionButton} 458 `; 459 460 overlay.appendChild(modal); 461 document.body.appendChild(overlay); 462 463 // Handle button click 464 const okBtn = document.getElementById('notification-ok'); 465 if (okBtn) { 466 okBtn.addEventListener('click', async () => { 467 // Mark notification as read 468 try { 469 const notificationsRef = firebase.database().ref('notifications'); 470 await notificationsRef.child(notification.key).update({ read: true }); 471 } catch (error) { 472 console.error('Error marking notification as read:', error); 473 } 474 475 // Remove overlay 476 overlay.style.opacity = '1'; 477 overlay.style.transition = 'opacity 0.3s ease'; 478 overlay.style.opacity = '0'; 479 setTimeout(() => overlay.remove(), 300); 480 481 // If username was deleted, prompt for new username 482 if (notification.type === 'username_deleted') { 483 // Clear current username 484 localStorage.removeItem(USERNAME_KEY); 485 // Prompt for new username 486 setTimeout(() => { 487 promptForUsername(); 488 }, 500); 489 } else if (notification.type === 'username_changed' && notification.newUsername) { 490 // Update stored username 491 localStorage.setItem(USERNAME_KEY, notification.newUsername); 492 // Dispatch event so leaderboards know username changed 493 window.dispatchEvent(new CustomEvent('usernameSet', { 494 detail: { username: notification.newUsername } 495 })); 496 } 497 }); 498 } 499 } 500 501 // Get user's IP address (shared function) with timeout to prevent freezing 502 async function getUserIP() { 503 const timeout = 3000; // 3 second timeout to prevent freezing 504 505 try { 506 // Try ipify.org first (simple and reliable) with timeout 507 const controller = new AbortController(); 508 const timeoutId = setTimeout(() => controller.abort(), timeout); 509 510 const response = await fetch('https://api.ipify.org?format=json', { 511 signal: controller.signal 512 }); 513 clearTimeout(timeoutId); 514 515 if (response.ok) { 516 const data = await response.json(); 517 return data.ip; 518 } 519 } catch (error) { 520 if (error.name === 'AbortError') { 521 emitDevWarn('â ï¸ IP fetch from ipify.org timed out'); 522 } else { 523 emitDevWarn('â ï¸ Failed to get IP from ipify.org:', error.message); 524 } 525 } 526 527 try { 528 // Fallback to ipapi.co with timeout 529 const controller = new AbortController(); 530 const timeoutId = setTimeout(() => controller.abort(), timeout); 531 532 const response = await fetch('https://ipapi.co/ip/', { 533 signal: controller.signal 534 }); 535 clearTimeout(timeoutId); 536 537 if (response.ok) { 538 const ip = await response.text().then(t => t.trim()); 539 if (ip && ip.match(/^\d+\.\d+\.\d+\.\d+$/)) { 540 return ip; 541 } 542 } 543 } catch (error) { 544 if (error.name === 'AbortError') { 545 emitDevWarn('â ï¸ IP fetch from ipapi.co timed out'); 546 } else { 547 emitDevWarn('â ï¸ Failed to get IP from ipapi.co:', error.message); 548 } 549 } 550 551 emitDevWarn('â Could not fetch IP address from any service'); 552 return null; 553 } 554 555 // Generate browser fingerprint 556 function generateFingerprint() { 557 const canvas = document.createElement('canvas'); 558 const ctx = canvas.getContext('2d'); 559 ctx.textBaseline = 'top';
560 ctx.font = '14px Arial'; 561 ctx.fillText('fingerprint', 2, 2); 562 563 const fingerprint = [ 564 navigator.userAgent, 565 navigator.language, 566 screen.colorDepth, 567 screen.width + 'x' + screen.height, 568 new Date().getTimezoneOffset(), 569 canvas.toDataURL() 570 ].join('|'); 571 572 let hash = 0; 573 for (let i = 0; i < fingerprint.length; i++) { 574 const char = fingerprint.charCodeAt(i); 575 hash = ((hash << 5) - hash) + char; 576 hash = hash & hash; 577 } 578 579 return 'fp_' + Math.abs(hash).toString(36); 580 } 581 582 // Check if username is already taken by someone else 583 async function isUsernameTaken(username) { 584 if (!fishFoodRef || !userFingerprint) return false; 585 586 try { 587 // Search for any entry with this username 588 const snapshot = await fishFoodRef.orderByChild('name').equalTo(username).once('value'); 589 590 if (!snapshot.exists()) { 591 return false; // Username not taken 592 } 593 594 // Check if any of the entries with this name have a different fingerprint 595 let takenByOther = false; 596 snapshot.forEach((childSnapshot) => { 597 const entry = childSnapshot.val(); 598 if (entry.fingerprint && entry.fingerprint !== userFingerprint) { 599 takenByOther = true; 600 } 601 }); 602 603 return takenByOther; 604 } catch (error) { 605 console.error('Error checking username:', error); 606 return false; // On error, allow the username 607 } 608 } 609 610 // Check if we're on index.html 611 const isIndexPage = window.location.pathname === '/index.html' || 612 window.location.pathname === '/' || 613 window.location.pathname === '/index'; 614 615 // Redirect to index if first visit and not already there (skip in hub preview / capture embeds) 616 function isHubPreviewVisit() { 617 try { 618 if (window.__PUPWIFE_HUB_PREVIEW__ || window.__PUPWIFE_CAPTURE_PREVIEW__) return true; 619 var params = new URLSearchParams(window.location.search || ''); 620 var value = params.get('hubPreview'); 621 if (value === '1' || value === 'true') return true; 622 } catch (e) {} 623 return /(?:^|[?&])hubPreview=(?:1|true)(?:&|$)/i.test(window.location.search || ''); 624 } 625 626 if (!isIndexPage && !localStorage.getItem(HAS_VISITED_INDEX_KEY)) { 627 if (isHubPreviewVisit()) { 628 localStorage.setItem(HAS_VISITED_INDEX_KEY, 'true'); 629 } else { 630 window.location.href = '/index.html'; 631 return; 632 } 633 } 634 635 // If on index page, mark as visited 636 if (isIndexPage) { 637 localStorage.setItem(HAS_VISITED_INDEX_KEY, 'true'); 638 639 // Wait for Firebase to load, then check username 640 function checkUsername() { 641 if (initFirebase()) { 642 // Check if user is in guest mode 643 const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true'; 644 if (isGuestMode) { 645 // User is in guest mode, don't prompt 646 emitDevLog('ð¤ Guest mode active'); 647 return; 648 } 649 650 const existingUsername = localStorage.getItem(USERNAME_KEY); 651 if (existingUsername) { 652 // Check if we need to add password or login 653 // Use setTimeout to ensure this runs after Firebase is fully initialized 654 setTimeout(() => { 655 checkPasswordRequirement(); 656 // Also update IP address for logged-in users 657 updateIPForLoggedInUser(existingUsername); 658 }, 500); 659 } 660 // Removed automatic popup - users can click the account indicator to sign up 661 } else { 662 // Retry after 100ms 663 setTimeout(checkUsername, 100); 664 } 665 } 666 667 // Start checking after a short delay 668 setTimeout(checkUsername, 500); 669 } 670 671 function promptForUsername() { 672 const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true'; 673 const overlay = createAuthOverlay(); 674 const modal = createAuthModal(); 675 676 const guestModeMessage = isGuestMode ? 677 '<p class="pupwife-auth-success" style="margin: 16px 0;">⨠Your guest progress will be saved to your account!</p>' : ''; 678 679 modal.innerHTML = ` 680 <h2 style="color: #633aac; margin-top: 0;">${isGuestMode ? 'Create Account ð' : 'Welcome! ð'}</h2> 681 <p style="color: #555; margin: 16px 0;">Choose a username for the leaderboards!</p> 682 ${guestModeMessage} 683 <input type="text" id="username-input" placeholder="Enter your name" maxlength="20" 684 style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 685 border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;"> 686 <input type="password" id="password-input" placeholder="Enter a password (to secure your account and keep your progress through the games and systems on this website!)" 687 style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 688 border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box;"> 689 <p id="username-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p> 690 <p class="pupwife-auth-warn" style="margin: 8px 0; font-weight: bold;">â ï¸ Security Notice: The admin (me, the only developer) cannot view your password! Please still pick something you would not use elsewhere</p> 691 <p style="font-size: 12px; margin: 8px 0;">Letters and numbers only, max 20 characters for username</p> 692 <div style="display: flex; align-items: flex-start; margin: 16px 0; gap: 8px;"> 693 <input type="checkbox" id="terms-checkbox" style="margin-top: 4px; cursor: pointer; width: 18px; height: 18px; flex-shrink: 0;"> 694 <label for="terms-checkbox" style="color: #555; font-size: 13px; cursor: pointer; line-height: 1.4; flex: 1;"> 695 I agree to the <span id="terms-link" style="color: #633aac; text-decoration: underline; cursor: pointer; font-weight: 600;">Terms & Conditions</span> 696 </label> 697 </div> 698 <p id="terms-error" style="color: #ff4444; font-size: 12px; margin: 4px 0 0 0; display: none;">You must agree to the Terms & Conditions to register.</p> 699 <div style="display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap;"> 700 <button id="username-submit" 701 style="flex: 1; padding: 12px 24px; font-size: 16px; 702 background-color: #c8a8ff; color: white; border: none; 703 border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif; 704 box-shadow: 0 4px 8px rgba(200, 168, 255, 0.4); transition: all 0.3s ease; min-width: 120px;"> 705 Register ⨠706 </button> 707 <button id="login-instead-btn" 708 style="flex: 1; padding: 12px 24px; font-size: 16px; 709 background-color: rgba(200, 168, 255, 0.3); color: #633aac; border: 2px solid #c8a8ff; 710 border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif; 711 transition: all 0.3s ease; min-width: 120px;"> 712 Login Instead 713 </button> 714 </div> 715 <button id="continue-as-guest-btn" 716 style="width: 100%; padding: 12px 24px; font-size: 16px; margin-top: 12px; 717 background-color: rgba(200, 168, 255, 0.15); color: #633aac; border: 2px solid rgba(200, 168, 255, 0.5); 718 border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif; 719 transition: all 0.3s ease;"> 720 Continue As Guest 721 </button> 722 <p class="pupwife-auth-guest-note" style="margin: 8px 0 0 0;">Guest mode saves to your browser. You can register later to keep your progress!</p> 723 `; 724 725 overlay.appendChild(modal); 726 document.body.appendChild(overlay); 727 728 const input = document.getElementById('username-input'); 729 const passwordInput = document.getElementById('password-input'); 730 const submitBtn = document.getElementById('username-submit'); 731 const loginInsteadBtn = document.getElementById('login-instead-btn'); 732 const guestBtn = document.getElementById('continue-as-guest-btn'); 733 const errorMsg = document.getElementById('username-error'); 734 const termsCheckbox = document.getElementById('terms-checkbox'); 735 const termsLink = document.getElementById('terms-link'); 736 const termsError = document.getElementById('terms-error'); 737 738 // Terms & Conditions popup 739 function showTermsPopup() { 740 const termsOverlay = createAuthOverlay(true); 741 const termsModal = createAuthModal(true); 742 termsModal.style.position = 'relative'; 743 744 termsModal.innerHTML = ` 745 <button id="close-terms-btn" style="position: absolute; top: 15px; right: 15px; background: #c8a8ff; color: white; border: none; border-radius: 50%; width: 32px; height: 32px; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-weight: bold; transition: all 0.2s ease; z-index: 10;">Ã</button> 746 <h2 style="color: #633aac !important; margin-top: 0; margin-bottom: 20px; background: transparent !important;">Terms & Conditions</h2> 747 <div style="color: #222222 !important; line-height: 1.6; font-size: 14px; background: transparent !important;"> 748 <p style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Last updated:</strong> ${new Date().toLocaleDateString()}</p> 749 750 <p style="color: #222222 !important; background: transparent !important;">Welcome to pupwife.com! These Terms of Service ("Terms") govern your access to and use of our website. By accessing or using our website, you agree to be bound by these Terms.</p> 751 752 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Acceptance of Terms</h3> 753 <p style="color: #222222 !important; background: transparent !important;">By accessing or using pupwife.com, you agree to comply with and be bound by these Terms of Service. If you do not agree to these Terms, please do not use our website.</p> 754 755 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Use of the Website</h3> 756 <h4 style="color: #633aac !important; margin-top: 15px; background: transparent !important;">Permitted Use</h4> 757 <p style="color: #222222 !important; background: transparent !important;">You may use our website for personal, non-commercial purposes. You agree to use the website in a manner that is lawful and does not infringe upon the rights of others.</p> 758 759 <h4 style="color: #633aac !important; margin-top: 15px; background: transparent !important;">Prohibited Activities</h4> 760 <p style="color: #222222 !important; background: transparent !important;">You agree not to:</p> 761 <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;"> 762 <li style="color: #222222 !important; background: transparent !important;">Use the website for any illegal or unauthorized purpose</li> 763 <li style="color: #222222 !important; background: transparent !important;">Attempt to gain unauthorized access to any part of the website or its systems</li> 764 <li style="color: #222222 !important; background: transparent !important;">Interfere with or disrupt the website or servers connected to the website</li> 765 <li style="color: #222222 !important; background: transparent !important;">Transmit any viruses, malware, or other harmful code</li> 766 <li style="color: #222222 !important; background: transparent !important;">
766Harass, abuse, or harm other users</li> 767 <li style="color: #222222 !important; background: transparent !important;">Post or transmit any content that is offensive, defamatory, or violates any laws</li> 768 <li style="color: #222222 !important; background: transparent !important;">Use automated systems (bots, scrapers, etc.) to access the website without permission</li> 769 <li style="color: #222222 !important; background: transparent !important;">Attempt to reverse engineer or decompile any part of the website</li> 770 </ul> 771 772 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">User Accounts</h3> 773 <p style="color: #222222 !important; background: transparent !important;">Some features of our website may require you to create an account. When creating an account, you agree to:</p> 774 <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;"> 775 <li style="color: #222222 !important; background: transparent !important;">Provide accurate and complete information</li> 776 <li style="color: #222222 !important; background: transparent !important;">Maintain the security of your account credentials</li> 777 <li style="color: #222222 !important; background: transparent !important;">Notify us immediately of any unauthorized access to your account</li> 778 <li style="color: #222222 !important; background: transparent !important;">Be responsible for all activities that occur under your account</li> 779 </ul> 780 781 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">User-Generated Content</h3> 782 <p style="color: #222222 !important; background: transparent !important;">Our website allows you to create and share various types of user-generated content, including but not limited to:</p> 783 <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;"> 784 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Usernames:</strong> Usernames you create for your account</li> 785 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Chat messages:</strong> Messages you send in our chatroom, including text messages, direct messages, and reactions</li> 786 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Profile pictures:</strong> Images you upload as your profile picture (stored as base64-encoded data)</li> 787 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Game scores:</strong> Scores and achievements from games you play on our website</li> 788 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Voice chat participation:</strong> Your participation in voice chat channels, including audio data transmitted during voice calls</li> 789 </ul> 790 791 <p style="color: #222222 !important; background: transparent !important;">By creating, posting, or uploading any user-generated content, you agree that:</p> 792 <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;"> 793 <li style="color: #222222 !important; background: transparent !important;">You own or have the right to post the content, or you have obtained all necessary permissions</li> 794 <li style="color: #222222 !important; background: transparent !important;">The content does not violate any laws, regulations, or third-party rights (including copyright, trademark, privacy, or publicity rights)</li> 795 <li style="color: #222222 !important; background: transparent !important;">The content is not defamatory, harassing, abusive, threatening, obscene, or otherwise objectionable</li> 796 <li style="color: #222222 !important; background: transparent !important;">Profile pictures must be appropriate and may not contain explicit, violent, or illegal content</li> 797 <li style="color: #222222 !important; background: transparent !important;">You grant us a non-exclusive, worldwide, royalty-free license to use, display, reproduce, modify, and store your content on our website and in our database</li> 798 <li style="color: #222222 !important; background: transparent !important;">We may remove, edit, or moderate any content that violates these Terms or is otherwise objectionable, at our sole discretion</li> 799 <li style="color: #222222 !important; background: transparent !important;">We are not responsible for user-generated content and do not endorse any opinions expressed in such content</li> 800 </ul> 801 802 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Content Moderation</h3> 803 <p style="color: #222222 !important; background: transparent !important;">We reserve the right to monitor, review, and remove any user-generated content that violates these Terms. We may al
803so suspend or terminate accounts of users who repeatedly violate these Terms or post inappropriate content.</p> 804 805 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Intellectual Property</h3> 806 <p style="color: #222222 !important; background: transparent !important;">All content on pupwife.com, including but not limited to text, graphics, logos, images, and software, is the property of pupwife.com or its content suppliers and is protected by copyright and other intellectual property laws. You may not reproduce, distribute, or create derivative works from our content without our express written permission.</p> 807 808 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Third-Party Services</h3> 809 <p style="color: #222222 !important; background: transparent !important;">Our website uses third-party services, including:</p> 810 <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;"> 811 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Google AdSense:</strong> For displaying advertisements</li> 812 <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Google Firebase:</strong> For data storage, authentication, and real-time database services</li> 813 </ul> 814 <p style="color: #222222 !important; background: transparent !important;">Your use of these third-party services is subject to their respective terms of service and privacy policies. We are not responsible for the practices of these third-party services.</p> 815 816 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Disclaimers</h3> 817 <p style="color: #222222 !important; background: transparent !important;">Our website is provided "as is" and "as available" without warranties of any kind, either express or implied. We do not guarantee that:</p> 818 <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;"> 819 <li style="color: #222222 !important; background: transparent !important;">The website will be available at all times or free from errors</li> 820 <li style="color: #222222 !important; background: transparent !important;">The website will meet your specific requirements</li> 821 <li style="color: #222222 !important; background: transparent !important;">The website will be secure or free from viruses or other harmful components</li> 822 </ul> 823 824 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Limitation of Liability</h3> 825 <p style="color: #222222 !important; background: transparent !important;">To the maximum extent permitted by law, pupwife.com and its operators shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from your use of the website.</p> 826 827 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Indemnification</h3> 828 <p style="color: #222222 !important; background: transparent !important;">You agree to indemnify and hold harmless pupwife.com and its operators from any claims, damages, losses, liabilities, and expenses (including legal fees) arising out of or relating to your use of the website or violation of these Terms.</p> 829 830 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Termination</h3> 831 <p style="color: #222222 !important; background: transparent !important;">We reserve the right to suspend or terminate your access to the website at any time, with or without cause or notice, for any reason, including if you violate these Terms of Service.</p> 832 833 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Changes to Terms</h3> 834 <p style="color: #222222 !important; background: transparent !important;">We may update these Terms of Service from time to time. We will notify you of any changes by posting the new Terms on this page and updating the "last updated" date. Your continued use of the website after such changes constitutes acceptance of the new Terms.</p> 835 836 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Governing Law</h3> 837 <p style="color: #222222 !important; background: transparent !important;">These Terms of Service shall be governed by and construed in accordance with the laws of the jurisdiction in which the website operator is located, without regard to its conflict of law provisions.</p> 838 839 <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Contact Us</h3> 840 <p style="color: #222222 !important; background: transparent !important;">If you have any questions about these Terms of Service, please contact us through our <a href="contact.html" target="_blank" rel="noopener noreferrer" style="color: #633aac !important; text-decoration: underline;">contact page</a>.</p> 841 </div> 842 `; 843 844 termsOverlay.appendChild(termsModal); 845 document.body.appendChild(termsOverlay); 846 847 const closeBtn = document.getElementById('close-terms-btn'); 848 849 function closeTerms() { 850 termsOverlay.style.opacity = '1'; 851 termsOverlay.style.transition = 'opacity 0.3s ease'; 852 termsOverlay.style.opacity = '0'; 853 setTimeout(() => { 854 termsOverlay.remove(); 855 }, 300); 856 } 857 858 closeBtn.addEventListener('click', closeTerms); 859 closeBtn.addEventListener('mouseenter', () => { 860 closeBtn.style.backgroundColor = '#b58eff'; 861 closeBtn.style.transform = 'scale(1.1)'; 862 }); 863 closeBtn.addEventListener('mouseleave', () => { 864 closeBtn.style.backgroundColor = '#c8a8ff'; 865 closeBtn.style.transform = 'scale(1)'; 866 }); 867 868 termsOverlay.addEventListener('click', (e) => { 869 if (e.target === termsOverlay) { 870 closeTerms(); 871 } 872 }); 873 } 874 875 // Terms link click handler 876 termsLink.addEventListener('click', (e) => { 877 e.preventDefault(); 878 if (document.body && document.body.classList.contains('page-citrusos')) { 879 openInfoPage('terms.html'); 880 return; 881 } 882 showTermsPopup(); 883 }); 884 885 termsLink.addEventListener('mouseenter', () => { 886 termsLink.style.color = '#b58eff'; 887 }); 888 889 termsLink.addEventListener('mouseleave', () => { 890 termsLink.style.color = '#633aac'; 891 }); 892 893 // Continue as guest button 894 guestBtn.addEventListener('click', () => { 895 // Set guest mode flag 896 localStorage.setItem('puppy_guest_mode', 'true'); 897 // Clear any existing username/password 898 localStorage.removeItem(USERNAME_KEY); 899 localStorage.removeItem('puppy_username_password'); 900 userPassword = null; 901 902 // Animate modal out 903 overlay.style.opacity = '1'; 904 overlay.style.transition = 'opacity 0.3s ease'; 905 overlay.style.opacity = '0'; 906 907 setTimeout(() => { 908 overlay.remove(); 909 910 // Dispatch event so account indicator knows we're in guest mode 911 window.dispatchEvent(new CustomEvent('usernameSet', { 912 detail: { username: null, guestMode: true } 913 })); 914 }, 300); 915 }); 916 917 // Login instead button - switch to login mode 918 loginInsteadBtn.addEventListener('click', () => { 919 overlay.remove(); 920 const username = input.value.trim(); 921 if (username) { 922 promptForLogin(username); 923 } else { 924 // Show login prompt with username input
925 promptForLoginWithUsername(); 926 } 927 }); 928 929 // Button hover effect 930 submitBtn.addEventListener('mouseenter', () => { 931 submitBtn.style.backgroundColor = '#b58eff'; 932 submitBtn.style.transform = 'translateY(-2px)'; 933 submitBtn.style.boxShadow = '0 6px 12px rgba(200, 168, 255, 0.6)'; 934 }); 935 936 submitBtn.addEventListener('mouseleave', () => { 937 submitBtn.style.backgroundColor = '#c8a8ff'; 938 submitBtn.style.transform = 'translateY(0)'; 939 submitBtn.style.boxShadow = '0 4px 8px rgba(200, 168, 255, 0.4)'; 940 }); 941 942 // Submit handler (now async to check for duplicates) 943 async function submit() { 944 // Check if terms are accepted 945 if (!termsCheckbox.checked) { 946 termsError.style.display = 'block'; 947 termsCheckbox.style.outline = '2px solid #ff4444'; 948 termsCheckbox.style.outlineOffset = '2px'; 949 setTimeout(() => { 950 termsCheckbox.style.outline = ''; 951 termsCheckbox.style.outlineOffset = ''; 952 }, 2000); 953 return; 954 } else { 955 termsError.style.display = 'none'; 956 } 957 958 let username = input.value.trim(); 959 const password = passwordInput.value.trim(); 960 961 // Sanitize (letters, numbers, spaces only) 962 username = username.substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim(); 963 964 // Check minimum length 965 if (!username || username.length < 3) { 966 input.style.borderColor = '#ff4444'; 967 errorMsg.textContent = 'Username must be at least 3 characters long!'; 968 errorMsg.style.display = 'block'; 969 return; 970 } 971 972 // Check for banned words in username 973 const usernameBanned = containsBannedWord(username); 974 if (usernameBanned) { 975 input.style.borderColor = '#ff4444'; 976 errorMsg.textContent = `Username contains inappropriate content. Please choose a different username.`; 977 errorMsg.style.display = 'block'; 978 return; 979 } 980 981 if (!password || password.length === 0) { 982 passwordInput.style.borderColor = '#ff4444'; 983 passwordInput.placeholder = 'Please enter a password!'; 984 errorMsg.style.display = 'none'; 985 return; 986 } 987 988 // Check for banned words in password 989 const passwordBanned = containsBannedWord(password); 990 if (passwordBanned) { 991 passwordInput.style.borderColor = '#ff4444'; 992 errorMsg.textContent = `Password contains inappropriate content. Please choose a different password.`; 993 errorMsg.style.display = 'block'; 994 return; 995 } 996 997 // Show loading state 998 submitBtn.disabled = true; 999 submitBtn.textContent = 'Checking...'; 1000 submitBtn.style.opacity = '0.6'; 1001 submitBtn.style.cursor = 'wait'; 1002 errorMsg.style.display = 'none'; 1003 input.style.borderColor = '#c8a8ff'; 1004 passwordInput.style.borderColor = '#c8a8ff'; 1005 1006 // Check if username is taken 1007 const taken = await isUsernameTaken(username); 1008 1009 if (taken) { 1010 // Username is taken by someone else! 1011 errorMsg.textContent = `â ï¸ "${username}" is already taken! Please choose another name.`; 1012 errorMsg.style.display = 'block'; 1013 input.style.borderColor = '#ff4444'; 1014 input.value = ''; 1015 input.focus(); 1016 1017 // Reset button 1018 submitBtn.disabled = false; 1019 submitBtn.textContent = "Register â¨"; 1020 submitBtn.style.opacity = '1'; 1021 submitBtn.style.cursor = 'pointer'; 1022 return; 1023 } 1024 1025 // Check if migrating from guest mode 1026 const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true'; 1027 1028 // Username is available! Save it 1029 localStorage.setItem(USERNAME_KEY, username); 1030 localStorage.setItem('puppy_username_password', password); 1031 localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag 1032 userPassword = password; 1033 1034 // Register username in Firebase with password 1035 if (usernamesRef && userFingerprint) { 1036 try { 1037 // Get user's IP address 1038 const userIP = await getUserIP(); 1039 1040 const userData = { 1041 fingerprint: userFingerprint, 1042 password: password 1043 }; 1044 1045 await usernamesRef.child(username).set(userData); 1046 emitDevLog('ð Username registered:', username); 1047 1048 // Store IP address in admin-only path (admin/userIPs/$username) 1049 if (userIP && firebase && firebase.database) { 1050 try { 1051 const userIPsRef = firebase.database().ref('admin/userIPs'); 1052 const ipData = { 1053 ipAddress: userIP, 1054 ipLastUpdated: Date.now() 1055 }; 1056 await userIPsRef.child(username).set(ipData); 1057 } catch (ipError) { 1058 console.error('â Could not store IP address:', ipError); 1059 console.error(' Error details:', { 1060 code: ipError.code, 1061 message: ipError.message, 1062 username: username, 1063 ip: userIP 1064 }); 1065 } 1066 } else { 1067 if (!userIP) { 1068 emitDevWarn(`â ï¸ Could not get IP address for ${username} - IP fetch failed or was blocked`); 1069 } 1070 } 1071 1072 // If migrating from guest mode, migrate all leaderboard scores 1073 if (isGuestMode) { 1074 const migrated = await migrateGuestScoresToAccount(username, userFingerprint); 1075 if (migrated > 0) { 1076 emitDevLog(`â Migrated ${migrated} scores from guest mode to ${username}`); 1077 } 1078 } else { 1079 // Restore any deleted scores for this user 1080 const restored = await restoreDeletedScores(username, userFingerprint); 1081 if (restored > 0) { 1082 emitDevLog(`â Restored ${restored} scores for ${username}`); 1083 } 1084 } 1085 } catch (error) { 1086 console.error('Error registering username:', error); 1087 } 1088 } 1089 1090 // Animate modal out 1091 overlay.style.opacity = '1'; 1092 overlay.style.transition = 'opacity 0.3s ease'; 1093 overlay.style.opacity = '0'; 1094 1095 setTimeout(() => { 1096 overlay.remove(); 1097 1098 // Dispatch event so leaderboards know username is set 1099 window.dispatchEvent(new CustomEvent('usernameSet', { 1100 detail: { username: username } 1101 })); 1102 }, 300); 1103 } 1104 1105 submitBtn.addEventListener('click', submit); 1106 input.addEventListener('keypress', (e) => { 1107 if (e.key === 'Enter') submit(); 1108 }); 1109 1110 // Focus input 1111 setTimeout(() => input.focus(), 100); 1112 } 1113 1114 // Prompt for password (for existing users who need to add one) 1115 function promptForPassword(username, isExistingUser) {
1116 const overlay = createAuthOverlay(); 1117 const modal = createAuthModal(); 1118 1119 modal.innerHTML = ` 1120 <h2 style="color: #633aac; margin-top: 0;">ð Add Password</h2> 1121 <p style="color: #555; margin: 16px 0;">Please add a password to secure your account "${username}"</p> 1122 <input type="password" id="password-add-input" placeholder="Enter a password" 1123 style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 1124 border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;"> 1125 <p id="password-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p> 1126 <p style="color: #ff8800; font-size: 11px; margin: 8px 0; font-weight: bold;">â ï¸ Security Notice: The admin can currently see your password. Please use a unique password that you don't use elsewhere until this is fixed.</p> 1127 <button id="password-submit" 1128 style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 1129 background-color: #c8a8ff; color: white; border: none; 1130 border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;"> 1131 Save Password ⨠1132 </button> 1133 `; 1134 1135 overlay.appendChild(modal); 1136 document.body.appendChild(overlay); 1137 1138 const passwordInput = document.getElementById('password-add-input'); 1139 const submitBtn = document.getElementById('password-submit'); 1140 const errorMsg = document.getElementById('password-error'); 1141 1142 async function submit() { 1143 const password = passwordInput.value.trim(); 1144 1145 if (!password || password.length === 0) { 1146 passwordInput.style.borderColor = '#ff4444'; 1147 errorMsg.textContent = 'Please enter a password!'; 1148 errorMsg.style.display = 'block'; 1149 return; 1150 } 1151 1152 // Check for banned words in password 1153 const passwordBanned = containsBannedWord(password); 1154 if (passwordBanned) { 1155 passwordInput.style.borderColor = '#ff4444'; 1156 errorMsg.textContent = `Password contains inappropriate content. Please choose a different password.`; 1157 errorMsg.style.display = 'block'; 1158 return; 1159 } 1160 1161 submitBtn.disabled = true; 1162 submitBtn.textContent = 'Saving...'; 1163 1164 try { 1165 // Update username in Firebase with password 1166 if (usernamesRef && userFingerprint) { 1167 // Get user's IP address 1168 const userIP = await getUserIP(); 1169 1170 const userData = { 1171 fingerprint: userFingerprint, 1172 password: password 1173 }; 1174 1175 await usernamesRef.child(username).set(userData); 1176 1177 // Store IP address in admin-only path (admin/userIPs/$username) 1178 if (userIP && firebase && firebase.database) { 1179 try { 1180 const userIPsRef = firebase.database().ref('admin/userIPs'); 1181 const ipData = { 1182 ipAddress: userIP, 1183 ipLastUpdated: Date.now() 1184 }; 1185 await userIPsRef.child(username).set(ipData); 1186 } catch (ipError) { 1187 console.error('â Could not store IP address:', ipError); 1188 console.error(' Error details:', { 1189 code: ipError.code, 1190 message: ipError.message, 1191 username: username, 1192 ip: userIP 1193 }); 1194 } 1195 } else { 1196 if (!userIP) { 1197 emitDevWarn(`â ï¸ Could not get IP address for ${username} - IP fetch failed or was blocked`); 1198 } 1199 } 1200 } 1201 1202 // Save password locally 1203 localStorage.setItem('puppy_username_password', password); 1204 userPassword = password; 1205 1206 overlay.style.opacity = '0'; 1207 overlay.style.transition = 'opacity 0.3s ease'; 1208 setTimeout(() => overlay.remove(), 300); 1209 } catch (error) { 1210 console.error('Error saving password:', error); 1211 errorMsg.textContent = 'Error saving password. Please try again.'; 1212 errorMsg.style.display = 'block'; 1213 submitBtn.disabled = false;
1214 submitBtn.textContent = 'Save Password â¨'; 1215 } 1216 } 1217 1218 submitBtn.addEventListener('click', submit); 1219 passwordInput.addEventListener('keypress', (e) => { 1220 if (e.key === 'Enter') submit(); 1221 }); 1222 1223 setTimeout(() => passwordInput.focus(), 100); 1224 } 1225 1226 // Prompt for login with username input (for users who want to log in) 1227 function promptForLoginWithUsername() { 1228 const overlay = createAuthOverlay(); 1229 const modal = createAuthModal(); 1230 1231 modal.innerHTML = ` 1232 <h2 style="color: #633aac; margin-top: 0;">ð Login</h2> 1233 <p style="color: #555; margin: 16px 0;">Enter your username and password</p> 1234 <input type="text" id="login-username-input" placeholder="Enter your username" maxlength="20" 1235 style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 1236 border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;"> 1237 <input type="password" id="login-password-input" placeholder="Enter your password" 1238 style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 1239 border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;"> 1240 <p id="login-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p> 1241 <button id="login-submit-btn" 1242 style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 1243 background-color: #c8a8ff; color: white; border: none; 1244 border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;"> 1245 Login ⨠1246 </button> 1247 `; 1248 1249 overlay.appendChild(modal); 1250 document.body.appendChild(overlay); 1251 1252 const usernameInput = document.getElementById('login-username-input'); 1253 const passwordInput = document.getElementById('login-password-input'); 1254 const submitBtn = document.getElementById('login-submit-btn'); 1255 const errorMsg = document.getElementById('login-error'); 1256 1257 async function submit() { 1258 const username = usernameInput.value.trim().substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim(); 1259 const password = passwordInput.value.trim(); 1260 1261 if (!username || username.length === 0) { 1262 usernameInput.style.borderColor = '#ff4444'; 1263 errorMsg.textContent = 'Please enter your username!'; 1264 errorMsg.style.display = 'block'; 1265 return; 1266 } 1267 1268 if (!password || password.length === 0) { 1269 passwordInput.style.borderColor = '#ff4444'; 1270 errorMsg.textContent = 'Please enter your password!'; 1271 errorMsg.style.display = 'block'; 1272 return; 1273 } 1274 1275 submitBtn.disabled = true; 1276 submitBtn.textContent = 'Checking...'; 1277 errorMsg.style.display = 'none'; 1278 usernameInput.style.borderColor = '#c8a8ff'; 1279 passwordInput.style.borderColor = '#c8a8ff'; 1280 1281 try { 1282 // Check password in Firebase (NO username availability check - this is login, not registration!) 1283 if (usernamesRef) { 1284 const usernameData = await usernamesRef.child(username).once('value'); 1285 if (usernameData.exists()) { 1286 const data = usernameData.val(); 1287 const storedPassword = typeof data === 'object' ? data.password : null; 1288 1289 if (storedPassword && storedPassword === password) { 1290 // Password correct! Login successful 1291 localStorage.setItem(USERNAME_KEY, username); 1292 localStorage.setItem('puppy_username_password', password); 1293 localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag 1294 userPassword = password; 1295 1296 // Update IP address on login (admin-only readable) 1297 try { 1298 const userIP = await getUserIP(); 1299 if (userIP && firebase && firebase.database) { 1300 const userIPsRef = firebase.database().ref('admin/userIPs'); 1301 const ipData = { 1302 ipAddress: userIP, 1303 ipLastUpdated: Date.now() 1304 }; 1305 await userIPsRef.child(username).set(ipData); 1306 } else { 1307 if (!userIP) { 1308 emitDevWarn(`â ï¸ Could not get IP address for ${username} on login - IP fetch failed or was blocked`); 1309 } 1310 } 1311 } catch (error) { 1312 console.error('â Could not update IP address:', error); 1313 console.error(' Error details:', { 1314 code: error.code, 1315 message: error.message, 1316 username: username 1317 }); 1318 } 1319 1320 // Restore Firebase scores (prioritize over localStorage) 1321 restoreFirebaseScoresOnLogin(username).catch(err => { 1322 emitDevWarn('Could not restore Firebase scores:', err); 1323 }); 1324 1325 overlay.style.opacity = '0'; 1326 overlay.style.transition = 'opacity 0.3s ease'; 1327 setTimeout(() => { 1328 overlay.remove(); 1329 window.dispatchEvent(new CustomEvent('usernameSet', { 1330 detail: { username: username } 1331 })); 1332 }, 300); 1333 } else { 1334 // Wrong password or no password set 1335 if (!storedPassword) { 1336 // Legacy account - offer to add password 1337 errorMsg.textContent = 'This is a legacy account with no password. Would you like to add one?'; 1338 errorMsg.style.display = 'block'; 1339 errorMsg.style.color = '#ff9800'; 1340 1341 // Add button to add password 1342 const addPasswordBtn = document.createElement('button'); 1343 addPasswordBtn.textContent = 'Add Password'; 1344 addPasswordBtn.style.cssText = 'margin-top: 10px; padding: 8px 16px; font-size: 14px; background-color: #ff9800; color: white; border: none; border-radius: 8px; cursor: pointer; font-family: "Comic Sans MS", sans-serif;'; 1345 addPasswordBtn.onclick = () => { 1346 overlay.remove(); 1347 promptForPassword(username, true); 1348 }; 1349 1350 const errorContainer = errorMsg.parentElement; 1351 if (!errorContainer.querySelector('button')) { 1352 errorContainer.appendChild(addPasswordBtn); 1353 } 1354 } else { 1355 errorMsg.textContent = 'Incorrect password. Please try again.'; 1356 errorMsg.style.display = 'block'; 1357 } 1358 passwordInput.value = ''; 1359 passwordInput.focus(); 1360 submitBtn.disabled = false;
1361 submitBtn.textContent = 'Login â¨'; 1362 } 1363 } else { 1364 // Username doesn't exist 1365 errorMsg.textContent = 'This username does not exist! Please register instead.'; 1366 errorMsg.style.display = 'block'; 1367 usernameInput.focus(); 1368 submitBtn.disabled = false; 1369 submitBtn.textContent = 'Login â¨'; 1370 } 1371 } else { 1372 // No Firebase - can't verify 1373 errorMsg.textContent = 'Unable to connect to server. Please try again later.'; 1374 errorMsg.style.display = 'block'; 1375 submitBtn.disabled = false; 1376 submitBtn.textContent = 'Login â¨'; 1377 } 1378 } catch (error) { 1379 console.error('Error during login:', error); 1380 errorMsg.textContent = 'Error connecting to server. Please try again.'; 1381 errorMsg.style.display = 'block'; 1382 submitBtn.disabled = false; 1383 submitBtn.textContent = 'Login â¨'; 1384 } 1385 } 1386 1387 submitBtn.addEventListener('click', submit); 1388 usernameInput.addEventListener('keypress', (e) => { 1389 if (e.key === 'Enter') passwordInput.focus(); 1390 }); 1391 passwordInput.addEventListener('keypress', (e) => { 1392 if (e.key === 'Enter') submit(); 1393 }); 1394 1395 setTimeout(() => usernameInput.focus(), 100); 1396 } 1397 1398 // Prompt for login (when user needs to enter password for existing username) 1399 function promptForLogin(username) { 1400 const overlay = createAuthOverlay(); 1401 const modal = createAuthModal(); 1402 1403 modal.innerHTML = ` 1404 <h2 style="color: #633aac; margin-top: 0;">ð Login</h2> 1405 <p style="color: #555; margin: 16px 0;">Enter your password for "${username}"</p> 1406 <input type="password" id="password-login-input" placeholder="Enter your password" 1407 style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 1408 border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;"> 1409 <p id="password-login-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p> 1410 <button id="password-login-submit" 1411 style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 1412 background-color: #c8a8ff; color: white; border: none; 1413 border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;"> 1414 Login ⨠1415 </button> 1416 `; 1417 1418 overlay.appendChild(modal); 1419 document.body.appendChild(overlay); 1420 1421 const passwordInput = document.getElementById('password-login-input'); 1422 const submitBtn = document.getElementById('password-login-submit'); 1423 const errorMsg = document.getElementById('password-login-error'); 1424 1425 async function submit() { 1426 const password = passwordInput.value.trim(); 1427 1428 if (!password || password.length === 0) { 1429 passwordInput.style.borderColor = '#ff4444'; 1430 errorMsg.textContent = 'Please enter your password!'; 1431 errorMsg.style.display = 'block'; 1432 return; 1433 } 1434 1435 submitBtn.disabled = true; 1436 submitBtn.textContent = 'Checking...'; 1437 1438 try { 1439 // Check password in Firebase 1440 if (usernamesRef) { 1441 const usernameData = await usernamesRef.child(username).once('value'); 1442 if (usernameData.exists()) { 1443 const data = usernameData.val(); 1444 const storedPassword = typeof data === 'object' ? data.password : null; 1445 1446 if (storedPassword && storedPassword === password) { 1447 // Password correct! Login successful 1448 localStorage.setItem(USERNAME_KEY, username); 1449 localStorage.setItem('puppy_username_password', password); 1450 localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag 1451 userPassword = password; 1452 1453 // Update IP address on login (admin-only readable) 1454 try { 1455 const userIP = await getUserIP(); 1456 if (userIP && firebase && firebase.database) { 1457 const userIPsRef = firebase.database().ref('admin/userIPs'); 1458 const ipData = { 1459 ipAddress: userIP, 1460 ipLastUpdated: Date.now() 1461 }; 1462 await userIPsRef.child(username).set(ipData); 1463 } else { 1464 if (!userIP) { 1465 emitDevWarn(`â ï¸ Could not get IP address for ${username} on login - IP fetch failed or was blocked`); 1466 } 1467 } 1468 } catch (error) { 1469 console.error('â Could not update IP address:', error); 1470 console.error(' Error details:', { 1471 code: error.code, 1472 message: error.message, 1473 username: username 1474 }); 1475 } 1476 1477 // Restore Firebase scores (prioritize over localStorage) 1478 restoreFirebaseScoresOnLogin(username).catch(err => { 1479 emitDevWarn('Could not restore Firebase scores:', err); 1480 }); 1481 1482 overlay.style.opacity = '0'; 1483 overlay.style.transition = 'opacity 0.3s ease'; 1484 setTimeout(() => { 1485 overlay.remove(); 1486 window.dispatchEvent(new CustomEvent('usernameSet', { 1487 detail: { username: username } 1488 })); 1489 }, 300); 1490 } else { 1491 // Wrong password or no password set 1492 const storedPassword = typeof data === 'object' ? data.password : null; 1493 if (!storedPassword) { 1494 // Legacy account - offer to add password 1495 errorMsg.textContent = 'This is a legacy account with no password. Would you like to add one?'; 1496 errorMsg.style.display = 'block'; 1497 errorMsg.style.color = '#ff9800'; 1498 1499 // Add button to add password 1500 const addPasswordBtn = document.createElement('button'); 1501 addPasswordBtn.textContent = 'Add Password'; 1502 addPasswordBtn.style.cssText = 'margin-top: 10px; padding: 8px 16px; font-size: 14px; background-color: #ff9800; color: white; border: none; border-radius: 8px; cursor: pointer; font-family: "Comic Sans MS", sans-serif;'; 1503 addPasswordBtn.onclick = () => { 1504 overlay.remove(); 1505 promptForPassword(username, true); 1506 }; 1507 1508 const errorContainer = errorMsg.parentElement; 1509 if (!errorContainer.querySelector('button')) { 1510 errorContainer.appendChild(addPasswordBtn); 1511 } 1512 } else { 1513 errorMsg.textContent = 'Incorrect password. Please try again.'; 1514 errorMsg.style.display = 'block'; 1515 } 1516 passwordInput.value = ''; 1517 passwordInput.focus(); 1518 submitBtn.disabled = false;
1519 submitBtn.textContent = 'Login â¨'; 1520 } 1521 } else { 1522 // Username doesn't exist in Firebase - this shouldn't happen for login, but handle it 1523 errorMsg.textContent = 'This username does not exist! Please register instead.'; 1524 errorMsg.style.display = 'block'; 1525 passwordInput.value = ''; 1526 passwordInput.focus(); 1527 submitBtn.disabled = false; 1528 submitBtn.textContent = 'Login â¨'; 1529 } 1530 } else { 1531 // No Firebase - can't verify, but we have username so allow login 1532 localStorage.setItem(USERNAME_KEY, username); 1533 localStorage.setItem('puppy_username_password', password); 1534 localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag 1535 userPassword = password; 1536 1537 // Try to restore Firebase scores (may not work if Firebase unavailable) 1538 restoreFirebaseScoresOnLogin(username).catch(err => { 1539 emitDevWarn('Could not restore Firebase scores:', err); 1540 }); 1541 1542 overlay.style.opacity = '0'; 1543 overlay.style.transition = 'opacity 0.3s ease'; 1544 setTimeout(() => { 1545 overlay.remove(); 1546 window.dispatchEvent(new CustomEvent('usernameSet', { 1547 detail: { username: username } 1548 })); 1549 }, 300); 1550 } 1551 } catch (error) { 1552 // Firebase permission denied - can't verify, but we have username so allow login 1553 emitDevWarn('Could not verify password with Firebase (permission denied), allowing login:', error); 1554 localStorage.setItem(USERNAME_KEY, username); 1555 localStorage.setItem('puppy_username_password', password); 1556 localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag 1557 userPassword = password; 1558 1559 // Try to restore Firebase scores (may not work if Firebase unavailable) 1560 restoreFirebaseScoresOnLogin(username).catch(err => { 1561 emitDevWarn('Could not restore Firebase scores:', err); 1562 }); 1563 1564 overlay.style.opacity = '0'; 1565 overlay.style.transition = 'opacity 0.3s ease'; 1566 setTimeout(() => { 1567 overlay.remove(); 1568 window.dispatchEvent(new CustomEvent('usernameSet', { 1569 detail: { username: username } 1570 })); 1571 }, 300); 1572 } 1573 } 1574 1575 submitBtn.addEventListener('click', submit); 1576 passwordInput.addEventListener('keypress', (e) => { 1577 if (e.key === 'Enter') submit(); 1578 }); 1579 1580 setTimeout(() => passwordInput.focus(), 100); 1581 } 1582 1583 // Track active score restoration notifications 1584 let activeScoreNotifications = []; 1585 1586 // Show score restoration notification (similar to migration notifications) 1587 function showScoreRestoreNotification(leaderboardName, restoredScore, oldScore) { 1588 // Ensure body exists 1589 if (!document.body) { 1590 setTimeout(() => showScoreRestoreNotification(leaderboardName, restoredScore, oldScore), 500); 1591 return; 1592 } 1593 1594 const displayName = leaderboardName === 'fishFood' ? 'Fish Food' : leaderboardName; 1595 1596 const emoji = leaderboardName === 'fishFood' ? 'ð' : 'â¨'; 1597 1598 // Calculate position based on number of active notifications 1599 const offset = activeScoreNotifications.length * 120; 1600 const topPosition = 20 + offset; 1601 1602 const notification = document.createElement('div'); 1603 notification.className = 'score-restore-notification'; 1604 notification.style.cssText = ` 1605 position: fixed; 1606 top: ${topPosition}px; 1607 right: 20px; 1608 background: linear-gradient(135deg, rgba(139, 92, 246, 0.95), rgba(236, 72, 153, 0.95)); 1609 color: white; 1610 padding: 16px 20px; 1611 border-radius: 12px; 1612 box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4); 1613 z-index: ${10001 + activeScoreNotifications.length}; 1614 font-family: 'Comic Sans MS', sans-serif; 1615 font-size: 14px; 1616 max-width: 300px; 1617 animation: slideIn 0.3s ease-out; 1618 pointer-events: auto; 1619 `; 1620 1621 notification.innerHTML = ` 1622 <div style="font-weight: bold; margin-bottom: 8px; font-size: 16px;"> 1623 ${emoji}⨠${displayName} Restored! â¨${emoji} 1624 </div> 1625 <div> 1626 Your ${displayName.toLowerCase()} has been restored from ${oldScore} to <strong>${restoredScore}</strong>! 1627 </div> 1628 `; 1629 1630 // Add animation if not already present 1631 if (!document.getElementById('score-restore-animation-style')) { 1632 const style = document.createElement('style'); 1633 style.id = 'score-restore-animation-style'; 1634 style.textContent = ` 1635 @keyframes slideIn { 1636 from { 1637 transform: translateX(100%); 1638 opacity: 0; 1639 } 1640 to { 1641 transform: translateX(0); 1642 opacity: 1; 1643 } 1644 } 1645 `; 1646 document.head.appendChild(style); 1647 } 1648 1649 try { 1650 document.body.appendChild(notification); 1651 activeScoreNotifications.push(notification); 1652 1653 // Function to remove this notification and update positions 1654 const removeNotification = () => { 1655 const index = activeScoreNotifications.indexOf(notification); 1656 if (index > -1) { 1657 activeScoreNotifications.splice(index, 1); 1658 } 1659 1660 // Update positions of remaining notifications 1661 activeScoreNotifications.forEach((notif, idx) => { 1662 const newTop = 20 + (idx * 120); 1663 notif.style.top = `${newTop}px`; 1664 notif.style.zIndex = `${10001 + idx}`; 1665 }); 1666 1667 if (notification.parentNode) { 1668 notification.parentNode.removeChild(notification); 1669 } 1670 }; 1671 1672 // Auto-remove after 5 seconds 1673 setTimeout(() => { 1674 notification.style.animation = 'slideIn 0.3s ease-out reverse'; 1675 setTimeout(() => { 1676 removeNotification(); 1677 }, 300); 1678 }, 5000); 1679 } catch (error) { 1680 console.error('Error showing score restore notification:', error); 1681 } 1682 } 1683 1684 // Restore Firebase scores when logging into existing account (prioritize Firebase over localStorage) 1685 async function restoreFirebaseScoresOnLogin(username) { 1686 if (!usernamesRef || !userFingerprint) return; 1687 1688 try { 1689 // Get user's fingerprint from Firebase 1690 const usernameData = await usernamesRef.child(username).once('value'); 1691 if (!usernameData.exists()) return; 1692 1693 const data = usernameData.val(); 1694 const accountFingerprint = typeof data === 'object' ? data.fingerprint : data; 1695 if (!accountFingerprint) return; 1696 1697 const summaryParts = []; 1698 1699 // Restore scores from Firebase for systems that use localStorage 1700 const leaderboards = [ 1701 { 1702 name: 'fishFood', 1703 ref: fishFoodRef, 1704 localStorageKey: 'puppy_food_count', 1705 scoreField: 'score', 1706 restoreCallback: (score) => { 1707 if (window.__puppyFoodCounter && window.__puppyFoodCounter.setCount) { 1708 window.__puppyFoodCounter.setCount(score); 1709 } else {
1710 localStorage.setItem('puppy_food_count', String(score)); 1711 } 1712 } 1713 }, 1714 ]; 1715 1716 for (const lb of leaderboards) { 1717 if (!lb.ref) continue; 1718 1719 try { 1720 // Find highest score for this username in Firebase 1721 const snapshot = await lb.ref.orderByChild('name').equalTo(username).once('value'); 1722 let highestScore = 0; 1723 1724 snapshot.forEach((child) => { 1725 const entry = child.val(); 1726 const score = entry[lb.scoreField] || 0; 1727 if (score > highestScore) { 1728 highestScore = score; 1729 } 1730 }); 1731 1732 if (highestScore > 0) { 1733 // Get current localStorage score (from guest mode) 1734 const currentScore = parseInt(localStorage.getItem(lb.localStorageKey) || '0', 10); 1735 1736 // Always prioritize Firebase scores over localStorage guest scores 1737 lb.restoreCallback(highestScore); 1738 summaryParts.push(`${lb.name}: ${currentScore}â${highestScore} (cloud)`); 1739 1740 // Show notification if score changed 1741 if (highestScore !== currentScore) { 1742 setTimeout(() => { 1743 showScoreRestoreNotification(lb.name, highestScore, currentScore); 1744 }, 100); 1745 } 1746 } else { 1747 summaryParts.push(`${lb.name}: no cloud row`); 1748 } 1749 } catch (error) { 1750 console.error(`Error restoring ${lb.name} scores:`, error); 1751 } 1752 } 1753 emitDevLog('ð Firebase score sync (' + username + '): ' + summaryParts.join('; ')); 1754 } catch (error) { 1755 console.error('Error restoring Firebase scores on login:', error); 1756 } 1757 } 1758 1759 // Migrate guest mode scores to account when user registers 1760 async function migrateGuestScoresToAccount(newUsername, fingerprint) { 1761 if (!userFingerprint) return 0; 1762 1763 try { 1764 const updates = {}; 1765 let migrateCount = 0; 1766 1767 // Check all leaderboards for entries with this fingerprint 1768 const leaderboards = [ 1769 { ref: fishFoodRef, path: 'fishFoodScores' }, 1770 { ref: firebase.database().ref('plongScores'), path: 'plongScores' }, 1771 { ref: firebase.database().ref('breakoutScores'), path: 'breakoutScores' }, 1772 { ref: firebase.database().ref('puggleScores'), path: 'puggleScores' }, 1773 { ref: firebase.database().ref('puppyJumpScores'), path: 'puppyJumpScores' } 1774 ]; 1775 1776 for (const lb of leaderboards) { 1777 if (!lb.ref) continue; 1778 1779 try { 1780 const snapshot = await lb.ref.orderByChild('fingerprint').equalTo(fingerprint).once('value'); 1781 snapshot.forEach((child) => { 1782 const entry = child.val(); 1783 // Update name to new username 1784 updates[`${lb.path}/${child.key}/name`] = newUsername; 1785 migrateCount++; 1786 }); 1787 } catch (error) { 1788 console.error(`Error checking ${lb.path}:`, error); 1789 } 1790 } 1791 1792 if (Object.keys(updates).length > 0) { 1793 await firebase.database().ref().update(updates); 1794 } 1795 1796 return migrateCount; 1797 } catch (error) { 1798 console.error('Error migrating guest scores:', error); 1799 return 0; 1800 } 1801 } 1802 1803 // Restore deleted scores when user registers a new username 1804 async function restoreDeletedScores(newUsername, fingerprint) { 1805 if (!userFingerprint || !fishFoodRef) return 0; 1806 1807 try { 1808 const updates = {}; 1809 let restoreCount = 0; 1810 1811 // Check all leaderboards for placeholder names with this fingerprint 1812 const leaderboards = [ 1813 { ref: fishFoodRef, path: 'fishFoodScores' }, 1814 { ref: firebase.database().ref('plongScores'), path: 'plongScores' }, 1815 { ref: firebase.database().ref('breakoutScores'), path: 'breakoutScores' }, 1816 { ref: firebase.database().ref('puggleScores'), path: 'puggleScores' } 1817 ]; 1818 1819 for (const lb of leaderboards) { 1820 if (!lb.ref) continue; 1821 1822 try { 1823 const snapshot = await lb.ref.orderByChild('fingerprint').equalTo(fingerprint).once('value'); 1824 snapshot.forEach((child) => { 1825 const entry = child.val(); 1826 // Check if name is a placeholder (starts with [Deleted User) 1827 if (entry.name && entry.name.startsWith('[Deleted User')) { 1828 updates[`${lb.path}/${child.key}/name`] = newUsername; 1829 restoreCount++; 1830 } 1831 }); 1832 } catch (error) { 1833 console.error(`Error checking ${lb.path}:`, error); 1834 } 1835 } 1836 1837 if (Object.keys(updates).length > 0) { 1838 await firebase.database().ref().update(updates); 1839 } 1840 1841 return restoreCount; 1842 } catch (error) { 1843 console.error('Error restoring deleted scores:', error); 1844 return 0; 1845 } 1846 } 1847 1848 // Logout function 1849 function logout() { 1850 localStorage.removeItem(USERNAME_KEY); 1851 localStorage.removeItem('puppy_username_password'); 1852 localStorage.removeItem('puppy_guest_mode'); 1853 userPassword = null; 1854 location.reload(); 1855 } 1856 1857 function dispatchUsernameSet(detail) { 1858 try { 1859 window.dispatchEvent(new CustomEvent('usernameSet', { detail: detail || {} })); 1860 } catch (e) { /* ignore */ } 1861 } 1862 1863 function mountCitrusAuth(container, options) { 1864 if (!container) return; 1865 options = options || {}; 1866 1867 var activeTab = 'register'; 1868 1869 container.innerHTML = 1870 '<div class="citrus-account-auth">' + 1871 '<div class="citrus-account-auth__tabs" role="tablist" aria-label="Account sign in">' + 1872 '<button type="button" class="citrus-account-auth__tab is-active" data-citrus-auth-tab="register" role="tab" aria-selected="true">Register</button>' + 1873 '<button type="button" class="citrus-account-auth__tab" data-citrus-auth-tab="login" role="tab" aria-selected="false">Login</button>' + 1874 '</div>' + 1875 '<div class="citrus-account-auth__panel" data-citrus-auth-panel="register">' + 1876 '<p class="citrus-account-auth__lead">Choose a username for leaderboards and saved progress.</p>' + 1877 '<label class="citrus-account-field"><span class="citrus-account-label">Username</span>' + 1878 '<input type="text" class="citrus-account-input" data-citrus-auth="register-username" maxlength="20" autocomplete="username" placeholder="Letters and numbers, 3 to 20 chars"></label>' + 1879 '<label class="citrus-account-field"><span class="citrus-account-label">Password</span>' + 1880 '<input type="password" class="citrus-account-input" data-citrus-auth="register-password" autocomplete="new-password" placeholder="Pick a password"></label>' + 1881 '<label class="citrus-account-check">' + 1882 '<input type="checkbox" data-citrus-auth="terms"> ' + 1883 '<span>I agree to the <a href="/terms.html" class="citrus-account-link" data-citrus-open-info="terms.html">Terms & Conditions</a></span>' + 1884 '</label>' + 1885 '<p class="citrus-account-msg citrus-account-msg--error" data-citrus-auth="register-error" style="display: none;"></p>' + 1886 '<button type="button" class="citrus-account-btn citrus-account-btn--primary" data-citrus-auth="register-submit">Create account</button>' + 1887 '</div>' + 1888 '<div class="citrus-account-auth__panel" data-citrus-auth-panel="login" hidden>' + 1889 '<p class="citrus-account-auth__lead">Welcome back! Enter your username and password.</p>' + 1890 '<label class="citrus-account-field"><span class="citrus-account-label">Username</span>' + 1891 '<input type="text" class="citrus-account-input" data-citrus-auth="login-username" maxlength="20" autocomplete="username"></label>' + 1892 '<label class="citrus-account-field"><span class="citrus-account-label">Password</span>' + 1893 '<input type="password" class="citrus-account-input" data-citrus-auth="login-password" autocomplete="current-password"></label>' + 1894 '<p class="citrus-account-msg citrus-account-msg--error" data-citrus-auth="login-error" style="display: none;"></p>' + 1895 '<button type="button" class="citrus-account-btn citrus-account-btn--primary" data-citrus-auth="login-submit">Sign in</button>' + 1896 '</div>' + 1897 '<div class="citrus-account-auth__guest">' + 1898 '<button type="button" class="citrus-account-btn" data-citrus-auth="guest">Continue as guest</button>' + 1899 '<p class="citrus-account-auth__note">Guest progress stays in this browser. Register later to keep it.</p>' + 1900 '</div>' + 1901 '</div>'; 1902 1903 var tabButtons = container.querySelectorAll('[data-citrus-auth-tab]'); 1904 var panels = container.querySelectorAll('[data-citrus-auth-panel]'); 1905 1906 function setTab(tab) { 1907 activeTab = tab; 1908 tabButtons.forEach(function (btn) { 1909 var isActive = btn.getAttribute('data-citrus-auth-tab') === tab; 1910 btn.classList.toggle('is-active', isActive); 1911 btn.setAttribute('aria-selected', isActive ? 'true' : 'false'); 1912 });
1913 panels.forEach(function (panel) { 1914 var show = panel.getAttribute('data-citrus-auth-panel') === tab; 1915 panel.hidden = !show; 1916 }); 1917 } 1918 1919 tabButtons.forEach(function (btn) { 1920 btn.addEventListener('click', function () { 1921 setTab(btn.getAttribute('data-citrus-auth-tab')); 1922 }); 1923 }); 1924 1925 container.addEventListener('click', function (event) { 1926 var infoLink = event.target.closest('[data-citrus-open-info]'); 1927 if (!infoLink) return; 1928 event.preventDefault(); 1929 openInfoPage(infoLink.getAttribute('data-citrus-open-info')); 1930 }); 1931 1932 function showError(key, message) { 1933 var el = container.querySelector('[data-citrus-auth="' + key + '"]'); 1934 if (!el) return; 1935 el.textContent = message; 1936 el.style.display = message ? 'block' : 'none'; 1937 } 1938 1939 function finishAuth(username) { 1940 dispatchUsernameSet({ username: username }); 1941 if (typeof options.onSuccess === 'function') { 1942 options.onSuccess(username); 1943 } 1944 } 1945 1946 async function submitRegister() { 1947 var usernameInput = container.querySelector('[data-citrus-auth="register-username"]'); 1948 var passwordInput = container.querySelector('[data-citrus-auth="register-password"]'); 1949 var termsInput = container.querySelector('[data-citrus-auth="terms"]'); 1950 var submitBtn = container.querySelector('[data-citrus-auth="register-submit"]'); 1951 var username = usernameInput.value.trim().substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim(); 1952 var password = passwordInput.value.trim(); 1953 1954 showError('register-error', ''); 1955 1956 if (!termsInput.checked) { 1957 showError('register-error', 'You must agree to the Terms & Conditions to register.'); 1958 return; 1959 } 1960 if (!username || username.length < 3) { 1961 showError('register-error', 'Username must be at least 3 characters long.'); 1962 return; 1963 } 1964 if (containsBannedWord(username)) { 1965 showError('register-error', 'Username contains inappropriate content. Please choose another.'); 1966 return; 1967 } 1968 if (!password) { 1969 showError('register-error', 'Please enter a password.'); 1970 return; 1971 } 1972 if (containsBannedWord(password)) { 1973 showError('register-error', 'Password contains inappropriate content. Please choose another.'); 1974 return; 1975 } 1976 1977 submitBtn.disabled = true; 1978 submitBtn.textContent = 'Checking...'; 1979 1980 try { 1981 initFirebase(); 1982 var taken = await isUsernameTaken(username); 1983 if (taken) { 1984 showError('register-error', '"' + username + '" is already taken. Try another name.'); 1985 submitBtn.disabled = false; 1986 submitBtn.textContent = 'Create account'; 1987 return; 1988 } 1989 1990 var isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true'; 1991 localStorage.setItem(USERNAME_KEY, username); 1992 localStorage.setItem('puppy_username_password', password); 1993 localStorage.removeItem('puppy_guest_mode'); 1994 userPassword = password; 1995 1996 if (usernamesRef && userFingerprint) { 1997 var userIP = await getUserIP(); 1998 await usernamesRef.child(username).set({ 1999 fingerprint: userFingerprint, 2000 password: password 2001 }); 2002 if (userIP && firebase && firebase.database) { 2003 try { 2004 await firebase.database().ref('admin/userIPs').child(username).set({ 2005 ipAddress: userIP, 2006 ipLastUpdated: Date.now() 2007 }); 2008 } catch (ipError) { 2009 emitDevWarn('Could not store IP address:', ipError); 2010 } 2011 } 2012 if (isGuestMode) { 2013 await migrateGuestScoresToAccount(username, userFingerprint); 2014 } else { 2015 await restoreDeletedScores(username, userFingerprint); 2016 } 2017 } 2018 2019 finishAuth(username); 2020 } catch (error) { 2021 console.error('Error registering username:', error); 2022 showError('register-error', 'Could not create account. Please try again.'); 2023 submitBtn.disabled = false;
2024 submitBtn.textContent = 'Create account'; 2025 } 2026 } 2027 2028 async function submitLogin() { 2029 var usernameInput = container.querySelector('[data-citrus-auth="login-username"]'); 2030 var passwordInput = container.querySelector('[data-citrus-auth="login-password"]'); 2031 var submitBtn = container.querySelector('[data-citrus-auth="login-submit"]'); 2032 var username = usernameInput.value.trim().substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim(); 2033 var password = passwordInput.value.trim(); 2034 2035 showError('login-error', ''); 2036 2037 if (!username) { 2038 showError('login-error', 'Please enter your username.'); 2039 return; 2040 } 2041 if (!password) { 2042 showError('login-error', 'Please enter your password.'); 2043 return; 2044 } 2045 2046 submitBtn.disabled = true; 2047 submitBtn.textContent = 'Checking...'; 2048 2049 try { 2050 initFirebase(); 2051 if (usernamesRef) { 2052 var usernameData = await usernamesRef.child(username).once('value'); 2053 if (!usernameData.exists()) { 2054 showError('login-error', 'This username does not exist. Try registering instead.'); 2055 submitBtn.disabled = false; 2056 submitBtn.textContent = 'Sign in'; 2057 return; 2058 } 2059 var data = usernameData.val(); 2060 var storedPassword = typeof data === 'object' ? data.password : null; 2061 if (!storedPassword) { 2062 showError('login-error', 'Legacy account with no password. Use Register to claim it, or contact support.'); 2063 submitBtn.disabled = false; 2064 submitBtn.textContent = 'Sign in'; 2065 return; 2066 } 2067 if (storedPassword !== password) { 2068 showError('login-error', 'Incorrect password. Please try again.'); 2069 submitBtn.disabled = false; 2070 submitBtn.textContent = 'Sign in'; 2071 return; 2072 } 2073 2074 localStorage.setItem(USERNAME_KEY, username); 2075 localStorage.setItem('puppy_username_password', password); 2076 localStorage.removeItem('puppy_guest_mode'); 2077 userPassword = password; 2078 2079 try { 2080 var userIP = await getUserIP(); 2081 if (userIP && firebase && firebase.database) { 2082 await firebase.database().ref('admin/userIPs').child(username).set({ 2083 ipAddress: userIP, 2084 ipLastUpdated: Date.now() 2085 }); 2086 } 2087 } catch (ipErr) { 2088 emitDevWarn('Could not update IP address:', ipErr); 2089 } 2090 2091 restoreFirebaseScoresOnLogin(username).catch(function (err) { 2092 emitDevWarn('Could not restore Firebase scores:', err); 2093 }); 2094 finishAuth(username); 2095 return; 2096 } 2097 2098 showError('login-error', 'Login is unavailable right now. Try again in a moment.'); 2099 submitBtn.disabled = false; 2100 submitBtn.textContent = 'Sign in'; 2101 } catch (error) { 2102 console.error('Error during login:', error); 2103 showError('login-error', 'Login failed. Please try again.'); 2104 submitBtn.disabled = false; 2105 submitBtn.textContent = 'Sign in'; 2106 } 2107 } 2108 2109 container.querySelector('[data-citrus-auth="register-submit"]').addEventListener('click', submitRegister); 2110 container.querySelector('[data-citrus-auth="login-submit"]').addEventListener('click', submitLogin); 2111 container.querySelector('[data-citrus-auth="guest"]').addEventListener('click', function () { 2112 localStorage.setItem('puppy_guest_mode', 'true'); 2113 localStorage.removeItem(USERNAME_KEY); 2114 localStorage.removeItem('puppy_username_password'); 2115 userPassword = null; 2116 dispatchUsernameSet({ username: null, guestMode: true }); 2117 if (typeof options.onSuccess === 'function') { 2118 options.onSuccess(null); 2119 } 2120 }); 2121 } 2122 2123 // Expose API 2124 window.__sharedUsername = { 2125 /** 2126 * Check if text contains any banned words. Returns the banned word found, or null. 2127 * Used by chatroom message filter and username/password validation. 2128 */ 2129 containsBannedWord: containsBannedWord, 2130 get: function() { 2131 return localStorage.getItem(USERNAME_KEY) || null; 2132 }, 2133 set: function(username) { 2134 const sanitized = username.substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim(); 2135 if (sanitized) { 2136 localStorage.setItem(USERNAME_KEY, sanitized); 2137 return true; 2138 } 2139 return false;
2140 }, 2141 clear: function() { 2142 localStorage.removeItem(USERNAME_KEY); 2143 localStorage.removeItem('puppy_username_password'); 2144 localStorage.removeItem('puppy_guest_mode'); 2145 userPassword = null; 2146 }, 2147 logout: logout, 2148 promptAgain: promptForUsername, 2149 mountCitrusAuth: mountCitrusAuth 2150 }; 2151 2152 // Auto-load online indicator system (similar to notifications) 2153 function loadOnlineIndicator() { 2154 if (!window.ChatroomOnlineIndicator) { 2155 const indicatorScript = document.createElement('script'); 2156 indicatorScript.src = '/projects/chatroom/scripts/onlineIndicator.js'; 2157 indicatorScript.onerror = () => { 2158 emitDevWarn('â ï¸ Failed to load online indicator script'); 2159 }; 2160 document.body.appendChild(indicatorScript); 2161 } 2162 } 2163 2164 // Auto-load site-wide IP ban gate (fallback when loadingScreen/citrusBoot did not boot it) 2165 function loadIPBanChecker() { 2166 var path = typeof window !== 'undefined' && window.location && window.location.pathname; 2167 if (path && /\/(fuckoff|banned)\.html$/i.test(String(path).replace(/\\/g, '/'))) { 2168 return; 2169 } 2170 if (window.__PUPWIFE_IP_BAN_BOOTED__) { 2171 if (window.PupwifeIpBan && typeof window.PupwifeIpBan.start === 'function') { 2172 window.PupwifeIpBan.start(); 2173 } 2174 return; 2175 } 2176 if (document.querySelector('script[src="/scripts/dependencies/ipBanBoot.js"]')) { 2177 window.__PUPWIFE_IP_BAN_PENDING_START__ = true; 2178 return; 2179 } 2180 2181 window.__PUPWIFE_IP_BAN_PENDING_START__ = true; 2182 const banBootScript = document.createElement('script'); 2183 banBootScript.src = '/scripts/dependencies/ipBanBoot.js'; 2184 banBootScript.onerror = function() { 2185 emitDevWarn('Failed to load IP ban boot script'); 2186 window.__PUPWIFE_IP_BAN_PENDING_START__ = false; 2187 }; 2188 2189 if (document.head) { 2190 document.head.insertBefore(banBootScript, document.head.firstChild); 2191 } else if (document.body) { 2192 document.body.insertBefore(banBootScript, document.body.firstChild); 2193 } else { 2194 setTimeout(loadIPBanChecker, 100); 2195 } 2196 } 2197 2198 // Load IP ban checker after DOM is ready (pages without loadingScreen/citrusBoot) 2199 if (document.readyState === 'loading') { 2200 document.addEventListener('DOMContentLoaded', () => { 2201 loadIPBanChecker(); 2202 }); 2203 } else { 2204 loadIPBanChecker(); 2205 } 2206 2207 // Presence client must load before onlineIndicator (tab-based Firebase presence) 2208 (function injectPresenceClient() { 2209 var _p = (typeof window !== 'undefined' && window.location && window.location.pathname) || ''; 2210 if (/music\.html$/i.test(_p) || _p === '/music') return; 2211 if (document.querySelector('script[data-pupwife-presence-client]')) return; 2212 const s = document.createElement('script'); 2213 s.src = '/projects/chatroom/scripts/presenceClient.js'; 2214 s.setAttribute('data-pupwife-presence-client', '1'); 2215 s.async = false; 2216 (document.head || document.documentElement).appendChild(s); 2217 })(); 2218 2219 // Load online indicator after a delay (similar to notifications) 2220 // Wait a bit longer to ensure Firebase is loaded first 2221 (function scheduleOnlineIndicator() { 2222 var _p = (typeof window !== 'undefined' && window.location && window.location.pathname) || ''; 2223 if (/music\.html$/i.test(_p) || _p === '/music') return; 2224 setTimeout(loadOnlineIndicator, 1500); 2225 })(); 2226 2227})(); 2228
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.