1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> 6 <title>MistAgent â Sign In</title> 7 <link rel="icon" href="/static/images/favicon.ico" type="image/x-icon"> 8 <link rel="preconnect" href="https://fonts.googleapis.com"> 9 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 10 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> 11
11<script defer src="https://unpkg.com/[email protected]/dist/umd/lucide.min.js"></script>
11 12
12<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
12 13 <style> 14 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 15 16 :root { 17 --bg-light: #FAFAFA; 18 --bg-white: #FFFFFF; 19 --bg-subtle: #F5F5F5; 20 --text-black: #000000; 21 --text-dark: #171717; 22 --text-light: #888888; 23 --text-muted: #A1A1A1; 24 --border-light: #EAEAEA; 25 --border-medium: #D4D4D4; 26 --accent-blue: #3B82F6; 27 --danger: #EF4444; 28 } 29 30 body { 31 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 32 background: var(--bg-light); 33 color: var(--text-dark); 34 height: 100vh; height: 100dvh; 35 overflow: hidden; 36 position: relative; 37 } 38 39 /* Watermark background */ 40 .watermark { 41 position: fixed; 42 inset: 0; 43 background: url('/static/images/cglogo-large.png') center / contain no-repeat; 44 opacity: 0.08; 45 pointer-events: none; 46 z-index: 0; 47 } 48 49 /* Top navigation bar */ 50 .top-bar { 51 position: absolute; 52 top: 0; left: 0; right: 0; 53 height: 56px; 54 display: flex; 55 align-items: center; 56 justify-content: space-between; 57 padding: 0 32px; 58 z-index: 2; 59 } 60 61 .top-logo { 62 display: flex; 63 align-items: center; 64 gap: 10px; 65 } 66 67 .top-logo img { 68 height: 34px; 69 object-fit: contain; 70 filter: contrast(2.2) brightness(0.7); 71 } 72 73 .top-logo span { 74 font-size: 16px; 75 font-weight: 700; 76 color: var(--text-dark); 77 letter-spacing: -0.3px; 78 } 79 80 .top-help { 81 display: flex; 82 align-items: center; 83 gap: 6px; 84 color: var(--text-muted); 85 font-size: 13px; 86 font-weight: 500; 87 cursor: pointer; 88 transition: color 0.2s; 89 } 90 91 .top-help:hover { 92 color: var(--text-light); 93 } 94 95 .top-help i, .top-help svg { 96 width: 16px; 97 height: 16px; 98 } 99 100 /* Center container */ 101 .login-wrapper { 102 position: relative; 103 z-index: 1; 104 display: flex; 105 align-items: center; 106 justify-content: center; 107 height: 100vh; height: 100dvh; 108 } 109 110 /* Login card */ 111 .login-card { 112 width: 420px; 113 max-width: calc(100vw - 40px); 114 background: rgba(255, 255, 255, 0.25); 115 backdrop-filter: blur(6px); 116 -webkit-backdrop-filter: blur(6px); 117 border: 1px solid var(--border-light); 118 border-radius: 16px; 119 padding: 40px 36px; 120 display: flex; 121 flex-direction: column; 122 align-items: center; 123 gap: 28px; 124 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.06); 125 opacity: 0; 126 transform: translateY(20px); 127 animation: cardIn 0.6s ease-out 0.2s forwards; 128 position: relative; 129 overflow: hidden; 130 } 131 132 /* Card internal watermark â hidden on desktop, visible on mobile */ 133 .login-card::before { 134 content: ''; 135 position: absolute; 136 inset: 0; 137 background: url('/static/images/cglogo-large.png') center / 120% no-repeat; 138 opacity: 0; 139 pointer-events: none; 140 z-index: 0; 141 } 142 143 .login-card > * { 144 position: relative; 145 z-index: 1; 146 } 147 148 @keyframes cardIn { 149 to { 150 opacity: 1; 151 transform: translateY(0); 152 } 153 } 154 155 /* Header area */ 156 .header-area { 157 display: flex; 158 flex-direction: column; 159 align-items: center; 160 gap: 16px; 161 width: 100%; 162 } 163 164 .header-logo { 165 width: 48px; 166 height: 48px; 167 object-fit: contain; 168 } 169 170 .title-group { 171 display: flex; 172 flex-direction: column; 173 align-items: center; 174 gap: 6px; 175 } 176 177 .title-group h1 { 178 font-size: 22px; 179 font-weight: 700; 180 color: var(--text-black); 181 letter-spacing: -0.5px; 182 } 183 184 .title-group p { 185 font-size: 13px; 186 color: var(--text-light); 187 text-align: center; 188 } 189 190 /* Separator */ 191 .separator { 192 width: 100%; 193 height: 1px; 194 background: var(--border-light); 195 } 196 197 /* Form */ 198 .form-fields { 199 display: flex; 200 flex-direction: column; 201 gap: 18px; 202 width: 100%; 203 } 204 205 .field-group { 206 display: flex; 207 flex-direction: column; 208 gap: 6px; 209 } 210 211 .field-label-row { 212 display: flex; 213 justify-content: space-between; 214 align-items: center; 215 } 216 217 .field-label { 218 font-size: 13px; 219 font-weight: 500; 220 color: var(--text-dark); 221 } 222 223 .forgot-link { 224 font-size: 12px; 225 font-weight: 500; 226 color: var(--accent-blue); 227 text-decoration: none; 228 cursor: pointer; 229 transition: opacity 0.2s; 230 } 231 232 .forgot-link:hover { 233 opacity: 0.8; 234 } 235 236 .field-input-wrapper { 237 display: flex; 238 align-items: center; 239 gap: 10px; 240 width: 100%; 241 height: 44px; 242 background: var(--bg-subtle); 243 border: 1px solid var(--border-light); 244 border-radius: 8px; 245 padding: 0 14px; 246 transition: border-color 0.2s, box-shadow 0.2s; 247 } 248 249 .field-input-wrapper:focus-within { 250 border-color: var(--border-medium); 251 box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.04); 252 } 253 254 .field-input-wrapper i, 255 .field-input-wrapper svg { 256 width: 16px; 257 height: 16px; 258 color: var(--text-muted); 259 flex-shrink: 0; 260 } 261 262 .field-input { 263 flex: 1; 264 border: none; 265 background: transparent; 266 font-family: 'Inter', sans-serif; 267 font-size: 13px; 268 color: var(--text-dark); 269 outline: none; 270 } 271 272 .field-input::placeholder { 273 color: var(--text-muted); 274 } 275 276 /* Override browser autofill blue background */ 277 .field-input:-webkit-autofill, 278 .field-input:-webkit-autofill:hover, 279 .field-input:-webkit-autofill:focus { 280 -webkit-box-shadow: 0 0 0 1000px var(--bg-subtle) inset; 281 -webkit-text-fill-color: var(--text-dark); 282 transition: background-color 5000s ease-in-out 0s; 283 } 284 285 .toggle-pw { 286 cursor: pointer; 287 flex-shrink: 0; 288 opacity: 0.6; 289 transition: opacity 0.2s; 290 } 291 292 .toggle-pw:hover { 293 opacity: 1; 294 } 295 296 /* Error message */ 297 .error-msg { 298 display: none; 299 width: 100%; 300 padding: 10px 14px; 301 background: rgba(239, 68, 68, 0.08); 302 border: 1px solid rgba(239, 68, 68, 0.2); 303 border-radius: 8px; 304 color: var(--danger); 305 font-size: 12px; 306 text-align: center; 307 } 308 309 .error-msg.visible { 310 display: block; 311 } 312 313 /* Login button */ 314 .login-btn { 315 width: 100%; 316 height: 44px; 317 display: flex; 318 align-items: center; 319 justify-content: center; 320 gap: 8px; 321 background: var(--text-black); 322 border: none; 323 border-radius: 8px; 324 color: #FFFFFF; 325 font-family: 'Inter', sans-serif; 326 font-size: 14px; 327 font-weight: 600; 328 cursor: pointer; 329 transition: background 0.2s, transform 0.1s; 330 } 331 332 .login-btn:hover { 333 background: #1a1a1a; 334 } 335 336 .login-btn:active { 337 transform: scale(0.98); 338 } 339 340 .login-btn:disabled { 341 opacity: 0.5;
342 cursor: not-allowed; 343 } 344 345 .login-btn i, .login-btn svg { 346 width: 16px; 347 height: 16px; 348 } 349 350 /* OAuth buttons */ 351 .oauth-section { 352 display: none; 353 flex-direction: column; 354 gap: 12px; 355 width: 100%; 356 } 357 358 .oauth-section.visible { 359 display: flex; 360 } 361 362 .oauth-divider { 363 display: flex; 364 align-items: center; 365 gap: 12px; 366 width: 100%; 367 color: var(--text-muted); 368 font-size: 12px; 369 } 370 371 .oauth-divider::before, 372 .oauth-divider::after { 373 content: ''; 374 flex: 1; 375 height: 1px; 376 background: var(--border-light); 377 } 378 379 .oauth-btn { 380 width: 100%; 381 height: 44px; 382 display: flex; 383 align-items: center; 384 justify-content: center; 385 gap: 10px; 386 background: var(--bg-white); 387 border: 1px solid var(--border-light); 388 border-radius: 8px; 389 color: var(--text-dark); 390 font-family: 'Inter', sans-serif; 391 font-size: 13px; 392 font-weight: 500; 393 cursor: pointer; 394 transition: background 0.2s, border-color 0.2s; 395 } 396 397 .oauth-btn:hover:not(:disabled) { 398 background: var(--bg-subtle); 399 border-color: var(--border-medium); 400 } 401 402 .oauth-btn:disabled { 403 opacity: 0.45; 404 cursor: not-allowed; 405 } 406 407 .oauth-btn .coming-soon { 408 font-size: 11px; 409 color: var(--text-muted); 410 margin-left: 2px; 411 } 412 413 .oauth-btn svg { 414 width: 18px; 415 height: 18px; 416 } 417 418 /* Sign up row */ 419 .signup-row { 420 display: flex; 421 align-items: center; 422 justify-content: center; 423 gap: 4px; 424 font-size: 12px; 425 } 426 427 .signup-row span { 428 color: var(--text-light); 429 } 430 431 .signup-row a { 432 color: var(--text-black); 433 font-weight: 600; 434 text-decoration: none; 435 cursor: pointer; 436 } 437 438 .signup-row a:hover { 439 text-decoration: underline; 440 } 441 442 /* Footer */ 443 .footer { 444 position: absolute; 445 bottom: 16px; 446 left: 0; 447 right: 0; 448 text-align: center; 449 font-size: 11px; 450 color: var(--text-muted); 451 z-index: 2; 452 } 453 454 /* Mobile brand tagline â hidden on desktop */ 455 .mobile-brand { 456 display: none; 457 text-align: center; 458 padding: 20px 0 0; 459 } 460 461 .mobile-brand .tagline { 462 font-size: 12px; 463 color: var(--text-muted); 464 letter-spacing: 0.5px; 465 } 466 467 .mobile-brand .features { 468 display: flex; 469 justify-content: center; 470 gap: 16px; 471 margin-top: 12px; 472 flex-wrap: wrap; 473 } 474 475 .mobile-brand .feature-item { 476 display: flex; 477 align-items: center; 478 gap: 4px; 479 font-size: 11px; 480 color: var(--text-light); 481 } 482 483 .mobile-brand .feature-item i, 484 .mobile-brand .feature-item svg { 485 width: 12px; 486 height: 12px; 487 } 488 489 /* Responsive */ 490 @media (max-width: 480px) { 491 .top-bar { 492 display: none; 493 } 494 .login-card { 495 padding: 24px 20px; 496 border-radius: 12px; 497 gap: 18px; 498 } 499 .login-card::before { 500 opacity: 0.06; 501 background-size: 140%; 502 } 503 .header-area { 504 gap: 10px; 505 } 506 .header-logo { 507 width: 36px; 508 height: 36px; 509 } 510 .title-group h1 { 511 font-size: 18px; 512 } 513 .title-group p { 514 font-size: 12px; 515 } 516 .form-fields { 517 gap: 14px; 518 } 519 .field-input { 520 font-size: 16px; 521 } 522 .mobile-brand { 523 display: block; 524 padding: 12px 0 0; 525 } 526 .footer { 527 position: relative; 528 bottom: auto; 529 padding-top: 8px; 530 } 531 .login-wrapper { 532 height: auto; 533 min-height: 100vh; 534 min-height: 100dvh; 535 padding: 16px 0; 536 flex-direction: column; 537 gap: 0; 538 } 539 } 540 </style> 541</head> 542<body> 543 <!-- Watermark --> 544 <div class="watermark"></div> 545 546 <!-- Top bar --> 547 <div class="top-bar"> 548 <a class="top-logo" href="https://slowmist.ai" target="_blank"> 549 <img src="/static/images/MistAgent-logo.png" alt="MistAgent"> 550 </a> 551 <div class="top-help"> 552 <i data-lucide="life-buoy"></i> 553 Need help? 554 </div> 555 </div> 556 557 <!-- Login --> 558 <div class="login-wrapper"> 559 <div class="login-card" id="loginCard"> 560 <!-- Header --> 561 <div class="header-area"> 562 <img class="header-logo" src="/static/images/wuqiong.png" alt="MistAgent"> 563 <div class="title-group"> 564 <h1>Welcome to MistAgent</h1> 565 <p>Sign in to continue to your workspace</p> 566 </div> 567 </div> 568 569 <div class="separator"></div> 570 571 <!-- Error --> 572 <div class="error-msg" id="errorMsg"></div> 573 574 <!-- Form --> 575 <form id="loginForm" autocomplete="on" style="width:100%;display:flex;flex-direction:column;gap:20px;"> 576 <div class="form-fields"> 577 <div class="field-group"> 578 <label class="field-label">Email address</label> 579 <div class="field-input-wrapper"> 580 <i data-lucide="mail"></i> 581 <input type="email" class="field-input" id="emailInput" 582 placeholder="[email protected]" autocomplete="email" autofocus> 583 </div> 584 </div> 585 586 <div class="field-group"> 587 <div class="field-label-row"> 588 <label class="field-label">Password</label> 589 <a href="/forgot-password" class="forgot-link">Forgot?</a> 590 </div> 591 <div class="field-input-wrapper"> 592 <i data-lucide="lock"></i> 593 <input type="password" class="field-input" id="passwordInput" 594 placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" autocomplete="current-password"> 595 <i data-lucide="eye-off" class="toggle-pw" id="togglePw"></i> 596 </div> 597 </div> 598 </div> 599 600 <button type="submit" class="login-btn" id="authBtn"> 601 Sign In 602 <i data-lucide="arrow-right"></i> 603 </button> 604 </form> 605 606 <!-- OAuth --> 607 <div class="oauth-section" id="oauthSection"> 608 <div class="oauth-divider">or continue with</div> 609 <button type="button" class="oauth-btn" id="googleBtn" disabled> 610 <svg viewBox="0 0 24 24">
610<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg> 611 Google <span class="coming-soon">(Coming Soon)</span> 612 </button> 613 <button type="button" class="oauth-btn" id="githubBtn" disabled> 614 <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg> 615 GitHub <span class="coming-soon">(Coming Soon)</span> 616 </button> 617 </div> 618 619 <div class="signup-row"> 620 <span>Don't have an account?</span> 621 <a href="/register">Create one</a> 622 </div> 623 </div> 624 625 <!-- Mobile brand tagline --> 626 <div class="mobile-brand"> 627 <div class="tagline">AI-powered security analysis platform</div> 628 <div class="features"> 629 <span class="feature-item"><i data-lucide="shield-check"></i> Audit</span> 630 <span class="feature-item"><i data-lucide="code"></i> Analysis</span> 631 <span class="feature-item"><i data-lucide="brain"></i> AI Detection</span> 632 </div> 633 </div> 634 </div> 635 636 <!-- Footer --> 637 <div class="footer">© 2026 SlowMist. All rights reserved.</div> 638 639
639<script> 640 const API_BASE = '/api/v1'; 641 let supabaseClient = null; 642 643 document.addEventListener('DOMContentLoaded', async () => { 644 lucide.createIcons(); 645 setupPasswordToggle(); 646 await checkAuthAndBoot(); 647 }); 648 649 // Password visibility toggle 650 function setupPasswordToggle() { 651 const toggle = document.getElementById('togglePw'); 652 const pwInput = document.getElementById('passwordInput'); 653 if (!toggle || !pwInput) return; 654 655 toggle.addEventListener('click', () => { 656 const isPassword = pwInput.type === 'password'; 657 pwInput.type = isPassword ? 'text' : 'password'; 658 toggle.setAttribute('data-lucide', isPassword ? 'eye' : 'eye-off'); 659 lucide.createIcons({ nodes: [toggle.parentElement] }); 660 const newToggle = toggle.parentElement.querySelector('.toggle-pw'); 661 if (newToggle && newToggle !== toggle) { 662 newToggle.addEventListener('click', () => setupPasswordToggle()); 663 } 664 }); 665 } 666 667 async function checkAuthAndBoot() { 668 // Get auth status + Supabase config 669 try { 670 const statusRes = await fetch(`${API_BASE}/auth/status`); 671 if (statusRes.ok) { 672 const statusData = await statusRes.json(); 673 if (!statusData.auth_enabled) { 674 window.location.href = '/'; 675 return; 676 } 677 // Initialize Supabase client 678 if (statusData.supabase_url && statusData.supabase_anon_key && window.supabase) { 679 supabaseClient = window.supabase.createClient( 680 statusData.supabase_url, 681 statusData.supabase_anon_key 682 ); 683 // Show OAuth buttons 684 document.getElementById('oauthSection').classList.add('visible'); 685 686 // Handle OAuth redirect callback (hash fragment) 687 await handleOAuthCallback(); 688 } 689 } 690 } catch {} 691 692 // Check if already logged in 693 const token = localStorage.getItem('auth_token'); 694 if (token) { 695 try { 696 const meRes = await fetch(`${API_BASE}/auth/me`, { 697 headers: { 'Authorization': `Bearer ${token}` } 698 }); 699 if (meRes.ok) { 700 window.location.href = '/'; 701 return; 702 } 703 localStorage.removeItem('auth_token'); 704 localStorage.removeItem('refresh_token'); 705 localStorage.removeItem('user_info'); 706 } catch {} 707 } 708 } 709 710 // Handle OAuth redirect (Supabase puts tokens in URL hash) 711 async function handleOAuthCallback() { 712 if (!supabaseClient) return; 713 714 // Check for hash params from OAuth redirect 715 const hash = window.location.hash; 716 if (hash && hash.includes('access_token')) { 717 try { 718 const { data: { session }, error } = await supabaseClient.auth.getSession(); 719 if (session && !error) { 720 localStorage.setItem('auth_token', session.access_token); 721 localStorage.setItem('refresh_token', session.refresh_token || ''); 722 // Clear hash 723 window.history.replaceState(null, '', window.location.pathname); 724 window.location.href = '/'; 725 return; 726 } 727 } catch {} 728 } 729 730 // Also check URL query params (Supabase PKCE flow) 731 const params = new URLSearchParams(window.location.search); 732 if (params.has('code')) { 733 try { 734 const { data: { session }, error } = await supabaseClient.auth.exchangeCodeForSession(params.get('code')); 735 if (session && !error) { 736 localStorage.setItem('auth_token', session.access_token); 737 localStorage.setItem('refresh_token', session.refresh_token || ''); 738 window.history.replaceState(null, '', window.location.pathname); 739 window.location.href = '/'; 740 return; 741 } 742 } catch {} 743 } 744 } 745 746 // OAuth sign-in 747 async function signInWithOAuth(provider) { 748 if (!supabaseClient) { 749 showError('OAuth not available. Supabase not configured.'); 750 return; 751 } 752 try { 753 const { error } = await supabaseClient.auth.signInWithOAuth({ 754 provider: provider, 755 options: { 756 redirectTo: window.location.origin + '/login', 757 }, 758 }); 759 if (error) showError(error.message); 760 } catch (e) { 761 showError('OAuth error: ' + e.message); 762 } 763 } 764 765 // Error handling 766 function showError(msg) { 767 const el = document.getElementById('errorMsg'); 768 el.textContent = msg; 769 el.classList.add('visible'); 770 const card = document.getElementById('loginCard'); 771 // Keep card visible â cardIn animation (forwards) set opacity:1, 772 // replacing it with shake would revert to base CSS opacity:0 773 card.style.opacity = '1'; 774 card.style.transform = 'translateY(0)';
775 card.style.animation = 'none'; 776 card.offsetHeight; 777 card.style.animation = 'shake 0.4s ease-out'; 778 } 779 780 function hideError() { 781 document.getElementById('errorMsg').classList.remove('visible'); 782 } 783 784 // Shake animation 785 const shakeStyle = document.createElement('style'); 786 shakeStyle.textContent = ` 787 @keyframes shake { 788 0%, 100% { transform: translateX(0); } 789 20% { transform: translateX(-6px); } 790 40% { transform: translateX(6px); } 791 60% { transform: translateX(-4px); } 792 80% { transform: translateX(4px); } 793 } 794 `; 795 document.head.appendChild(shakeStyle); 796 797 // Login form submission (backend proxy â supports username + email) 798 document.getElementById('loginForm').addEventListener('submit', async (e) => { 799 e.preventDefault(); 800 hideError(); 801 802 const email = document.getElementById('emailInput').value.trim(); 803 const password = document.getElementById('passwordInput').value; 804 if (!email || !password) { 805 showError('Please enter both email and password.'); 806 return; 807 } 808 809 const btn = document.getElementById('authBtn'); 810 btn.disabled = true; 811 const originalHTML = btn.innerHTML; 812 btn.textContent = 'Signing in...'; 813 814 try { 815 if (!supabaseClient) { 816 showError('Authentication is not available. Supabase not configured.'); 817 return; 818 } 819 820 const { data, error } = await supabaseClient.auth.signInWithPassword({ 821 email: email, 822 password: password, 823 }); 824 825 if (error) { 826 if (error.message && error.message.toLowerCase().includes('rate limit')) { 827 showError('Too many login attempts. Please wait a few minutes before trying again.'); 828 } else { 829 showError('Invalid email or password.'); 830 } 831 return; 832 } 833 834 if (data.session) { 835 localStorage.setItem('auth_token', data.session.access_token); 836 if (data.session.refresh_token) localStorage.setItem('refresh_token', data.session.refresh_token); 837 838 btn.textContent = 'Success!'; 839 btn.style.background = '#22C55E'; 840 841 const card = document.getElementById('loginCard'); 842 card.style.transition = 'opacity 0.3s, transform 0.3s'; 843 card.style.opacity = '0'; 844 card.style.transform = 'translateY(-10px)'; 845 846 setTimeout(() => { window.location.href = '/'; }, 400); 847 return; 848 } 849 850 showError('Authentication failed.'); 851 } catch { 852 showError('Network error. Please try again.'); 853 } finally { 854 btn.disabled = false; 855 if (btn.textContent !== 'Success!') { 856 btn.innerHTML = originalHTML; 857 lucide.createIcons({ nodes: [btn] }); 858 } 859 } 860 }); 861 </script>
861 862</body> 863</html>
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.