1// Inline login for the public map pages. Renders a modal over the map, posts 2// to the editor auth endpoint, and on success swaps the "ÐойÑи" button for the 3// avatar badge in place (no page reload). On the home gate page, reloads so 4// the server can render the full map. Does nothing when the visitor is already 5// authenticated (no #login-btn and no #home-login-btn present). 6(function () { 7 const loginBtn = document.getElementById('login-btn'); 8 const homeLoginBtn = document.getElementById('home-login-btn'); 9 if (!loginBtn && !homeLoginBtn) return; 10 11 const modal = document.getElementById('login-modal'); 12 const form = document.getElementById('login-form'); 13 const emailInput = document.getElementById('login-email'); 14 const passwordInput = document.getElementById('login-password'); 15 const passwordWrap = document.getElementById('login-password-wrap'); 16 const passwordLabel = document.getElementById('login-password-label'); 17 const emailTip = document.getElementById('login-email-tip'); 18 const passwordTip = document.getElementById('login-password-tip'); 19 const formTip = document.getElementById('login-form-tip'); 20 const successBox = document.getElementById('login-success'); 21 const cancelBtn = document.getElementById('login-cancel'); 22 const submitBtn = document.getElementById('login-submit'); 23 const titleEl = document.getElementById('login-title'); 24 const oauthBox = document.getElementById('login-oauth'); 25 const captchaBox = document.getElementById('login-captcha'); 26 const forgotLink = document.getElementById('login-forgot-link'); 27 const registerLink = document.getElementById('login-register-link'); 28 const backLink = document.getElementById('login-back-link'); 29 const rememberInput = document.getElementById('login-remember'); 30 const rememberWrap = document.getElementById('login-remember-wrap'); 31 if (!modal || !form) return; 32 33 const authOptions = window.AUTH_OPTIONS || { 34 registration_enabled: false, 35 oauth_providers: [], 36 smartcaptcha: { enabled: false } 37 }; 38 const captchaOpts = authOptions.smartcaptcha || { enabled: false }; 39 const policy = (window.Mapus && window.Mapus.passwordPolicy) || {}; 40 const setTip = 41 typeof policy.setFieldTip === 'function' 42 ? policy.setFieldTip 43 : function (input, tipEl, message) { 44 if (!tipEl) return; 45 tipEl.textContent = message || ''; 46 tipEl.hidden = !message; 47 if (input) { 48 input.classList.toggle('field-invalid', Boolean(message)); 49 input.setAttribute('aria-invalid', message ? 'true' : 'false'); 50 } 51 }; 52 53 // login | register | forgot 54 let mode = 'login'; 55 let captchaToken = ''; 56 let captchaWidgetId = null; 57 let captchaScriptPromise = null; 58 59 const ERROR_MESSAGES = { 60 'invalid credentials': 'ÐевеÑнÑй email или паÑолÑ', 61 'email and password are required': 'ÐведиÑе email и паÑолÑ', 62 'registration is disabled': 'РегиÑÑÑаÑÐ¸Ñ Ð¾ÑклÑÑена', 63 'not a valid email address': 'ÐведиÑе коÑÑекÑнÑй адÑÐµÑ ÑлекÑÑонной поÑÑÑ', 64 'captcha failed': 'ÐодÑвеÑдиÑе, ÑÑо Ð²Ñ Ð½Ðµ ÑобоÑ', 65 forbidden: 'ÐапÑÐ¾Ñ Ð¾ÑклонÑн' 66 }; 67 68 function emailError(value) { 69 if (typeof policy.validateEmail === 'function') { 70 return policy.validateEmail(value); 71 } 72 const trimmed = (value || '').trim(); 73 if (!trimmed) return 'ÐведиÑе адÑÐµÑ ÑлекÑÑонной поÑÑÑ'; 74 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { 75 return 'ÐведиÑе коÑÑекÑнÑй адÑÐµÑ ÑлекÑÑонной поÑÑÑ'; 76 } 77 return null; 78 } 79 80 function loginPasswordError(value) { 81 if (typeof policy.validateLoginPassword === 'function') { 82 return policy.validateLoginPassword(value); 83 } 84 if (!(value || '')) return 'ÐведиÑе паÑолÑ'; 85 return null; 86 } 87 88 /** @returns {{target: string, message: string}|null} */ 89 function validateFields() { 90 const mailErr = emailError(emailInput.value); 91 if (mailErr) return { target: 'email', message: mailErr }; 92 if (mode === 'login') { 93 const pwErr = loginPasswordError(passwordInput.value); 94 if (pwErr) return { target: 'password', message: pwErr }; 95 } 96 return null; 97 } 98 99 const OAUTH_LABELS = { 100 google: 'Google', 101 yandex: 'ЯндекÑ', 102 vk: 'ÐÐонÑакÑе', 103 github: 'GitHub' 104 }; 105 106 // Stable display order (API returns a sorted set). Not alphabetical: the two 107 // providers that survive the GeoIP filter in Russia lead, so the list does not 108 // start with a gap for most visitors. 109 const OAUTH_ORDER = ['yandex', 'vk', 'google', 'github']; 110 111 function clearFieldTips() { 112 setTip(emailInput, emailTip, null); 113 setTip(passwordInput, passwordTip, null); 114 if (formTip) { 115 formTip.textContent = ''; 116 formTip.hidden = true; 117 } 118 } 119 120 function showFieldError(target, message) { 121 if (successBox) { 122 successBox.textContent = ''; 123 successBox.hidden = true; 124 } 125 clearFieldTips(); 126 if (target === 'email') setTip(emailInput, emailTip, message); 127 else if (target === 'password') setTip(passwordInput, passwordTip, message); 128 else if (formTip) { 129 formTip.textContent = message; 130 formTip.hidden = false;
131 } 132 } 133 134 function showError(message) { 135 // Form-level (captcha / network); credentials â password tip. 136 showFieldError('form', message); 137 } 138 139 function showSuccess(message) { 140 clearFieldTips(); 141 if (!successBox) return; 142 successBox.textContent = message; 143 successBox.hidden = false; 144 } 145 146 function clearMessages() { 147 clearFieldTips(); 148 if (successBox) { 149 successBox.textContent = ''; 150 successBox.hidden = true; 151 } 152 } 153 154 function captchaNeeded() { 155 return Boolean( 156 captchaOpts.enabled && 157 captchaOpts.client_key && 158 (mode === 'register' || mode === 'forgot') 159 ); 160 } 161 162 function loadCaptchaScript() { 163 if (window.smartCaptcha) return Promise.resolve(); 164 if (captchaScriptPromise) return captchaScriptPromise; 165 captchaScriptPromise = new Promise((resolve, reject) => { 166 const existing = document.querySelector( 167 'script[data-mapus-smartcaptcha="1"]' 168 ); 169 if (existing) { 170 existing.addEventListener('load', () => resolve()); 171 existing.addEventListener('error', () => 172 reject(new Error('smartcaptcha script failed')) 173 ); 174 return; 175 } 176 const script = document.createElement('script'); 177 script.src = 'https://smartcaptcha.cloud.yandex.ru/captcha.js'; 178 script.defer = true; 179 script.dataset.mapusSmartcaptcha = '1'; 180 script.onload = () => resolve(); 181 script.onerror = () => reject(new Error('smartcaptcha script failed')); 182 document.head.appendChild(script); 183 }); 184 return captchaScriptPromise; 185 } 186 187 function destroyCaptcha() { 188 captchaToken = ''; 189 if ( 190 captchaWidgetId !== null && 191 window.smartCaptcha && 192 typeof window.smartCaptcha.destroy === 'function' 193 ) { 194 try { 195 window.smartCaptcha.destroy(captchaWidgetId); 196 } catch (err) { 197 /* ignore */ 198 } 199 } 200 captchaWidgetId = null; 201 if (captchaBox) { 202 captchaBox.replaceChildren(); 203 captchaBox.hidden = true; 204 } 205 } 206 207 function resetCaptcha() { 208 captchaToken = ''; 209 if ( 210 captchaWidgetId !== null && 211 window.smartCaptcha && 212 typeof window.smartCaptcha.reset === 'function' 213 ) { 214 try { 215 window.smartCaptcha.reset(captchaWidgetId); 216 return; 217 } catch (err) { 218 /* fall through to re-render */ 219 } 220 } 221 if (captchaNeeded()) { 222 destroyCaptcha(); 223 syncCaptcha(); 224 } 225 } 226 227 async function syncCaptcha() { 228 if (!captchaBox) return; 229 if (!captchaNeeded()) { 230 destroyCaptcha(); 231 return; 232 } 233 captchaBox.hidden = false; 234 if (captchaWidgetId !== null) return; 235 try { 236 await loadCaptchaScript(); 237 } catch (err) { 238 console.error(err); 239 showError('Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ ÐºÐ°Ð¿ÑÑ'); 240 return; 241 } 242 if (!window.smartCaptcha || captchaWidgetId !== null) return; 243 captchaBox.replaceChildren(); 244 captchaWidgetId = window.smartCaptcha.render(captchaBox, { 245 sitekey: captchaOpts.client_key, 246 hl: 'ru', 247 callback: function (token) { 248 captchaToken = token || ''; 249 } 250 }); 251 } 252 253 function setMode(next) { 254 mode = next; 255 clearMessages(); 256 const isLogin = mode === 'login'; 257 const isRegister = mode === 'register'; 258 const isForgot = mode === 'forgot'; 259 260 titleEl.textContent = isRegister 261 ? 'РегиÑÑÑаÑиÑ' 262 : isForgot 263 ? 'СбÑÐ¾Ñ Ð¿Ð°ÑолÑ' 264 : 'ÐÑ Ð¾Ð´'; 265 266 if (passwordWrap) passwordWrap.hidden = !isLogin; 267 if (passwordLabel) passwordLabel.hidden = !isLogin; 268 passwordInput.hidden = !isLogin; 269 if (!isLogin) passwordInput.value = ''; 270 271 submitBtn.textContent = isRegister 272 ? 'ÐаÑегиÑÑÑиÑоваÑÑÑÑ' 273 : isForgot 274 ? 'ÐÑпÑавиÑÑ ÑÑÑлкÑ' 275 : 'ÐойÑи'; 276 277 if (oauthBox) oauthBox.hidden = !isLogin || !oauthBox.children.length; 278 if (forgotLink) forgotLink.hidden = !isLogin; 279 if (registerLink) { 280 registerLink.hidden = !isLogin || !authOptions.registration_enabled; 281 } 282 if (backLink) backLink.hidden = isLogin; 283 // Registration and password reset have no session to keep, so the choice 284 // would be an inert control sitting under a form it does not affect. 285 if (rememberWrap) rememberWrap.hidden = !isLogin; 286 syncCaptcha(); 287 } 288 289 function renderOauth() { 290 if (!oauthBox) return; 291 oauthBox.replaceChildren(); 292 const enabled = new Set(authOptions.oauth_providers || []); 293 const providers = OAUTH_ORDER.filter((name) => enabled.has(name)); 294 if (!authOptions.registration_enabled || !providers.length) { 295 oauthBox.hidden = true; 296 return; 297 } 298 const startBase = String(authOptions.oauth_start_base || '').replace(/\/$/, '');
299 providers.forEach((name) => { 300 const a = document.createElement('a'); 301 a.className = 'login-oauth-btn login-oauth-' + name; 302 a.href = 303 startBase + '/auth/' + encodeURIComponent(name) + '/start'; 304 a.title = 'ÐойÑи ÑеÑез ' + (OAUTH_LABELS[name] || name); 305 306 const icon = document.createElement('span'); 307 icon.className = 'login-oauth-icon'; 308 icon.setAttribute('aria-hidden', 'true'); 309 310 const label = document.createElement('span'); 311 label.className = 'login-oauth-label'; 312 label.textContent = OAUTH_LABELS[name] || name; 313 314 a.append(icon, label); 315 // ÐÑоÑой ÑелÑок, пока пеÑвÑй еÑÑ ÑÐ²Ð¾Ð´Ð¸Ñ Ðº пÑовайдеÑÑ, -- вÑоÑой ÑÑаÑÑ: 316 // он заводил новÑй `state`, и возвÑÐ°Ñ Ð¾Ñ Ð¿ÐµÑвого не ÑÑ Ð¾Ð´Ð¸Ð»ÑÑ (issue 317 // #227; ÑеÑÐ²ÐµÑ ÑепеÑÑ Ð¿ÑÐ¸Ð½Ð¸Ð¼Ð°ÐµÑ Ð¾Ð±Ð°, но лиÑний кÑÑг к пÑовайдеÑÑ Ð½Ð¸ к 318 // ÑемÑ). Ðнопка Ð³Ð»Ð¾Ñ Ð½ÐµÑ Ð½Ð° вÑÐµÐ¼Ñ Ð¿ÐµÑÐµÑ Ð¾Ð´Ð° и оживаеÑ, еÑли пеÑÐµÑ Ð¾Ð´ не 319 // ÑлÑÑилÑÑ Ð¸Ð»Ð¸ Ñо ÑÑÑаниÑÑ Ð¿ÑовайдеÑа веÑнÑлиÑÑ Â«Ðазад». 320 a.addEventListener('click', (ev) => { 321 if (a.dataset.busy) { 322 ev.preventDefault(); 323 return; 324 } 325 a.dataset.busy = '1'; 326 a.setAttribute('aria-disabled', 'true'); 327 a.classList.add('is-busy'); 328 setTimeout(() => releaseOauth(a), OAUTH_BUSY_MS); 329 }); 330 oauthBox.appendChild(a); 331 }); 332 oauthBox.hidden = mode !== 'login'; 333 } 334 335 //: СколÑко кнопка Ð²Ñ Ð¾Ð´Ð° ÑеÑез пÑовайдеÑа глÑÑ Ð° поÑле ÑелÑка. 336 const OAUTH_BUSY_MS = 8000; 337 338 function releaseOauth(a) { 339 delete a.dataset.busy; 340 a.removeAttribute('aria-disabled'); 341 a.classList.remove('is-busy'); 342 } 343 344 // ÐозвÑÐ°Ñ Â«Ðазад» Ñо ÑÑÑаниÑÑ Ð¿ÑовайдеÑа Ð¿Ð¾Ð´Ð½Ð¸Ð¼Ð°ÐµÑ ÑÑÑаниÑÑ Ð¸Ð· кÑÑа 345 // пеÑÐµÑ Ð¾Ð´Ð¾Ð² вмеÑÑе Ñ Ð·Ð°Ð³Ð»Ð¾Ñ Ñими кнопками -- оживиÑÑ. 346 window.addEventListener('pageshow', (ev) => { 347 if (ev.persisted && oauthBox) oauthBox.querySelectorAll('.login-oauth-btn').forEach(releaseOauth); 348 }); 349 350 function openModal() { 351 setMode('login'); 352 renderOauth(); 353 modal.classList.add('open'); 354 emailInput.focus(); 355 } 356 357 function closeModal() { 358 modal.classList.remove('open'); 359 destroyCaptcha(); 360 } 361 362 // Replace the badge contents with the authenticated avatar using data from 363 // the login response, then drop the now-unneeded modal. 364 function renderBadge(badge) { 365 const wrap = document.querySelector('.user-badge'); 366 if (wrap && badge) { 367 const link = document.createElement('a'); 368 link.className = 'user-badge-avatar'; 369 link.href = badge.profile_url || '/editor/'; 370 link.title = 'ÐÑоÑилÑ'; 371 link.rel = 'author'; 372 if (badge.avatar_url) { 373 const img = document.createElement('img'); 374 img.src = badge.avatar_url; 375 img.alt = 'ÐÑоÑилÑ'; 376 link.appendChild(img); 377 } else { 378 const span = document.createElement('span'); 379 span.className = 'user-badge-initial'; 380 span.textContent = badge.initial || '?'; 381 link.appendChild(span); 382 } 383 wrap.replaceChildren(link); 384 } 385 modal.remove(); 386 } 387 388 function bindLoginTrigger(el) { 389 el.addEventListener('click', (e) => { 390 e.preventDefault(); 391 openModal(); 392 }); 393 } 394 395 if (loginBtn) bindLoginTrigger(loginBtn); 396 if (homeLoginBtn) bindLoginTrigger(homeLoginBtn); 397 // The landing repeats the call to action at the foot of a long scroll, where 398 // sending the reader back up to the hero button would be the whole point 399 // missed. A class rather than an id: there may be more than one. 400 document.querySelectorAll('.js-open-login').forEach(bindLoginTrigger); 401 402 // ?login=1 opens the form on arrival. The editor has no login page of its own 403 // any more, so an unauthenticated visitor is sent here with this parameter -- 404 // landing on a page where they still have to find the button would be sending 405 // them somewhere rather than letting them in. 406 if (new URLSearchParams(window.location.search).has('login')) { 407 openModal(); 408 // Drop it from the address bar: reloading or sharing the URL should not 409 // reopen a form the visitor may have deliberately closed. 410 const url = new URL(window.location.href); 411 url.searchParams.delete('login'); 412 window.history.replaceState({}, '', url.pathname + url.search + url.hash); 413 } 414 415 cancelBtn.addEventListener('click', closeModal); 416 // mousedown (not click/mouseup): press on the dimmed overlay closes; releasing 417 // outside after a press that started on the card must not dismiss. 418 modal.addEventListener('mousedown', (e) => { 419 if (e.target === modal) closeModal(); 420 }); 421 document.addEventListener('keydown', (e) => { 422 if (e.key === 'Escape') closeModal(); 423 }); 424 425 if (forgotLink) { 426 forgotLink.addEventListener('click', () => setMode('forgot')); 427 } 428 if (registerLink) { 429 registerLink.addEventListener('click', () => setMode('register')); 430 } 431 if (backLink) { 432 backLink.addEventListener('click', () => setMode('login')); 433 } 434 435 async function postJson(url, body) { 436 const res = await fetch(url, { 437 method: 'POST', 438 credentials: 'same-origin', 439 headers: { 440 'Content-Type': 'application/json', 441 'X-Requested-With': 'XMLHttpRequest' 442 }, 443 body: JSON.stringify(body) 444 }); 445 let data = null; 446 const contentType = res.headers.get('content-type') || ''; 447 if (contentType.includes('application/json')) { 448 data = await res.json(); 449 } 450 return { res, data }; 451 } 452 453 function clearFormBanner() { 454 if (formTip && !formTip.hidden) { 455 formTip.textContent = ''; 456 formTip.hidden = true; 457 } 458 if (successBox && !successBox.hidden) { 459 successBox.textContent = ''; 460 successBox.hidden = true; 461 } 462 } 463 464 // Form-level banners sit above the form;
464 dismiss on any field interaction. 465 form.addEventListener('focusin', (e) => { 466 if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { 467 clearFormBanner(); 468 } 469 }); 470 471 // Clicking an OAuth button blurs whichever field had focus, and the blur 472 // validators below would flash "введиÑе адÑеÑ" at somebody already on their 473 // way to VK -- a complaint about a form they have chosen not to fill in. 474 // 475 // pointerdown is dispatched before the focus moves, so the flag is set in 476 // time; it is cleared on the next tick so that a click released outside the 477 // button does not go on silencing the validator. relatedTarget covers 478 // reaching the button by keyboard, where no pointer event happens at all. 479 let leavingToOauth = false; 480 if (oauthBox) { 481 oauthBox.addEventListener('pointerdown', () => { 482 leavingToOauth = true; 483 setTimeout(() => { 484 leavingToOauth = false; 485 }, 0); 486 }); 487 } 488 489 function blurGoesToOauth(event) { 490 if (leavingToOauth) return true; 491 const to = event && event.relatedTarget; 492 return Boolean(to && oauthBox && oauthBox.contains(to)); 493 } 494 495 // Blur checks the shape of what was typed, never the fact that nothing was. 496 // Tabbing through a form you have not filled in yet is not a mistake, and 497 // «ÐведиÑе адÑеÑ» said while the cursor is still on its way to the next field 498 // is an accusation. Emptiness is the submit handler's business. 499 emailInput.addEventListener('blur', (e) => { 500 if (blurGoesToOauth(e)) return; 501 if (!emailInput.value.trim()) { 502 setTip(emailInput, emailTip, null); 503 return; 504 } 505 const err = emailError(emailInput.value); 506 if (err) showFieldError('email', err); 507 else setTip(emailInput, emailTip, null); 508 }); 509 510 emailInput.addEventListener('input', () => { 511 if (emailTip && !emailTip.hidden) setTip(emailInput, emailTip, null); 512 }); 513 514 passwordInput.addEventListener('blur', (e) => { 515 if (blurGoesToOauth(e)) return; 516 if (mode !== 'login' || passwordInput.hidden) return; 517 if (!passwordInput.value) { 518 setTip(passwordInput, passwordTip, null); 519 return; 520 } 521 const err = loginPasswordError(passwordInput.value); 522 if (err) showFieldError('password', err); 523 else setTip(passwordInput, passwordTip, null); 524 }); 525 526 passwordInput.addEventListener('input', () => { 527 if (passwordTip && !passwordTip.hidden) setTip(passwordInput, passwordTip, null); 528 }); 529 530 form.addEventListener('submit', async (e) => { 531 e.preventDefault(); 532 clearMessages(); 533 534 const fieldError = validateFields(); 535 if (fieldError) { 536 showFieldError(fieldError.target, fieldError.message); 537 return; 538 } 539 if (captchaNeeded() && !captchaToken) { 540 showFieldError('form', 'ÐодÑвеÑдиÑе, ÑÑо Ð²Ñ Ð½Ðµ ÑобоÑ'); 541 return; 542 } 543 544 const email = emailInput.value.trim(); 545 const password = passwordInput.value; 546 547 submitBtn.disabled = true; 548 try { 549 if (mode === 'register') { 550 const body = { email }; 551 if (captchaNeeded()) body.captcha_token = captchaToken; 552 const { res, data } = await postJson('/api/auth/register', body); 553 if (res.ok) { 554 showSuccess( 555 'ÐÑли адÑÐµÑ Ñвободен, Ð¼Ñ Ð¾ÑпÑавили пиÑÑмо Ñо ÑÑÑлкой Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ð¿Ð°ÑолÑ.' 556 ); 557 resetCaptcha(); 558 return; 559 } 560 resetCaptcha(); 561 const serverError = data && data.error; 562 const msg = 563 ERROR_MESSAGES[serverError] || 564 serverError || 565 'Ðе ÑдалоÑÑ Ð·Ð°ÑегиÑÑÑиÑоваÑÑÑÑ'; 566 showFieldError( 567 serverError === 'not a valid email address' ? 'email' : 'form', 568 msg 569 ); 570 return; 571 } 572 573 if (mode === 'forgot') { 574 const body = { email }; 575 if (captchaNeeded()) body.captcha_token = captchaToken; 576 const { res, data } = await postJson('/api/auth/forgot-password', body); 577 if (res.ok) { 578 showSuccess( 579 'ÐÑли аккаÑÐ½Ñ ÑÑÑеÑÑвÑеÑ, Ð¼Ñ Ð¾ÑпÑавили пиÑÑмо Ñо ÑÑÑлкой Ð´Ð»Ñ ÑбÑоÑа паÑолÑ.' 580 ); 581 resetCaptcha(); 582 return; 583 } 584 resetCaptcha(); 585 const serverError = data && data.error; 586 const msg = 587 ERROR_MESSAGES[serverError] || 588 serverError || 589 'Ðе ÑдалоÑÑ Ð¾ÑпÑавиÑÑ Ð¿Ð¸ÑÑмо'; 590 showFieldError( 591 serverError === 'not a valid email address' ? 'email' : 'form', 592 msg 593 ); 594 return; 595 } 596 597 const { res, data } = await postJson('/editor/api/auth/login', { 598 email, 599 password, 600 // Absent or false keeps the session in the browser's memory, which is 601 // the safer default on a shared machine; true asks for the longest 602 // cookie a browser will actually honour. 603 remember: Boolean(rememberInput && rememberInput.checked) 604 }); 605 606 if (res.ok) { 607 if (document.getElementById('home-gate')) { 608 window.location.reload(); 609 return; 610 } 611 renderBadge(data && data.badge); 612 document.dispatchEvent(new CustomEvent('mapus:auth-changed')); 613 return; 614 } 615 616 const serverError = data && data.error; 617 const msg = 618 ERROR_MESSAGES[serverError] || serverError || 'Ðе ÑдалоÑÑ Ð²Ð¾Ð¹Ñи'; 619 if (serverError === 'not a valid email address') { 620 showFieldError('email', msg); 621 } else { 622 // Login failures (credentials, network-mapped, unknown) sit above 623 // the whole form â not as a field bubble. 624 showFieldError('form', msg); 625 } 626 } catch (err) { 627 console.error('auth failed', err); 628 showFieldError('form', 'Ðе ÑдалоÑÑ Ð²ÑполниÑÑ Ð·Ð°Ð¿ÑоÑ'); 629 } finally { 630 submitBtn.disabled = false;
631 } 632 }); 633 634 renderOauth(); 635 if (registerLink) { 636 registerLink.hidden = !authOptions.registration_enabled; 637 } 638})(); 639 640// Logout button on the owner's profile page: clears the session, then goes home. 641(function () { 642 const logoutBtn = document.getElementById('logout-btn'); 643 if (!logoutBtn) return; 644 645 logoutBtn.addEventListener('click', async () => { 646 logoutBtn.disabled = true; 647 try { 648 await fetch('/editor/api/auth/logout', { 649 method: 'POST', 650 credentials: 'same-origin' 651 }); 652 } catch (err) { 653 console.error('logout failed', err); 654 } 655 window.location.href = '/'; 656 }); 657})();
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.