} else if (res.ok && data.success) { window.location.href = '/index.php'; } else if (res.status === 403) { if (data.require_2fa_setup) { showError('⚠️ 管理者は二段階認証の設定が必要です。管理者に連絡してください。'); } else { showError('⚠️ あなたのアカウントは凍結されています。管理者に連絡してください。'); } } else if (res.status === 401) { showError('❌ 初期IDまたはパスワードが間違っています'); } else { showError(data.error || 'ログインに失敗しました'); } } catch (e) { showError('通信エラー: ' + e.message); } }); // パスキーログイン passkeyLoginBtn.addEventListener('click', async () => { if (!window.PublicKeyCredential) { showError('お使いのブラウザはパスキーに対応していません'); return; } const orgId = document.getElementById('orgId').value || 'default'; try { clearError(); // SimpleWebAuthn使用した認証 const optionsResponse = await fetch('/api_proxy.php?endpoint=passkey_login.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ action: 'start', orgId: orgId }) }); let optionsData; try { optionsData = await optionsResponse.json(); } catch (parseError) { const text = await optionsResponse.text(); console.error('Response text:', text); showError('サーバーエラー: JSONパースに失敗しました'); return; } if (!optionsResponse.ok) { console.error('Options error:', optionsData); showError(optionsData.error || 'パスキー認証の準備に失敗しました'); return; } console.log('受信したオプション:', optionsData); // SimpleWebAuthnで認証実行 let authResult; try { // SimpleWebAuthnはoptionsをネストした形式で期待している authResult = await startAuthentication({ optionsJSON: optionsData }); } catch (authError) { console.error('認証エラー:', authError); if (authError.name === 'NotAllowedError') { showError('認証がキャンセルされました'); } else { showError('パスキー認証に失敗しました: ' + authError.message); } return; } // 認証完了 console.log('認証結果:', authResult); const verifyResponse = await fetch('/api_proxy.php?endpoint=passkey_login.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ action: 'verify', credential: authResult }) }); let verifyData; try { verifyData = await verifyResponse.json(); } catch (parseError) { const text = await verifyResponse.text(); console.error('検証レスポンステキスト:', text); showError('サーバーエラー: JSONパースに失敗しました'); return; } console.log('検証レスポンス:', verifyData); if (verifyResponse.ok && verifyData.success) { window.location.href = '/index.php'; } else { showError(verifyData.error || 'パスキー認証に失敗しました'); } } catch (e) { console.error('Passkey authentication error:', e); showError('パスキー認証エラー: ' + e.message); } }); function showError(msg) { errorMessage.textContent = msg; errorMessage.classList.remove('hidden'); } function clearError() { errorMessage.classList.add('hidden'); } // 管理者二段階認証ダイアログ function show2FADialog(availableMethods) { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; justify-content: center; align-items: center; z-index: 10000; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; padding: 2rem; border-radius: 12px; max-width: 450px; width: 90%; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); `; // 両方の認証方法がある場合はタブ表示 const hasBothMethods = availableMethods.totp && availableMethods.passkey; let tabsHTML = ''; let methodsHTML = ''; if (hasBothMethods) { tabsHTML = `
生体認証または端末のロックを解除して認証します
管理者ログインには二段階認証が必要です。${hasBothMethods ? '認証方法を選択してください。' : ''}
${tabsHTML} ${methodsHTML} `; overlay.appendChild(dialog); document.body.appendChild(overlay); console.log('2FA dialog added to DOM'); console.log('Available methods:', availableMethods); // タブ切り替え機能 if (hasBothMethods) { const tabs = dialog.querySelectorAll('.auth-tab'); const totpSection = dialog.querySelector('#totp-section'); const passkeySection = dialog.querySelector('#passkey-section'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetTab = tab.dataset.tab; // すべてのタブを非アクティブに tabs.forEach(t => { t.style.borderBottomColor = 'transparent'; t.style.color = '#718096'; t.classList.remove('active'); }); // クリックされたタブをアクティブに tab.style.borderBottomColor = targetTab === 'totp' ? '#48bb78' : '#667eea'; tab.style.color = targetTab === 'totp' ? '#48bb78' : '#667eea'; tab.classList.add('active'); // セクションの表示切り替え if (targetTab === 'totp') { totpSection.style.display = 'block'; passkeySection.style.display = 'none'; // TOTPフィールドにフォーカス setTimeout(() => { dialog.querySelector('#totp-2fa-code').focus(); }, 100); } else { totpSection.style.display = 'none'; passkeySection.style.display = 'block'; } }); }); // 初期フォーカス setTimeout(() => { dialog.querySelector('#totp-2fa-code').focus(); }, 100); } // TOTP認証ハンドラー if (availableMethods.totp) { const totpForm = dialog.querySelector('#totp-2fa-form'); console.log('TOTP form found:', totpForm); if (!totpForm) { console.error('TOTP form not found in dialog!'); return; } totpForm.addEventListener('submit', async (e) => { e.preventDefault(); console.log('TOTP form submitted'); const code = dialog.querySelector('#totp-2fa-code').value; console.log('TOTP code:', code); if (!code || code.length !== 6) { alert('6桁のコードを入力してください'); return; } try { console.log('Sending TOTP verification request...'); const response = await fetch('/api_proxy.php?endpoint=login.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ method: 'complete_2fa', twofa_method: 'totp', twofa_code: code }) }); console.log('Response status:', response.status); const data = await response.json(); console.log('Response data:', data); if (data.success) { overlay.remove(); window.location.href = '/index.php'; } else { alert('認証に失敗しました: ' + (data.error || '不明なエラー')); } } catch (error) { console.error('TOTP認証エラー:', error); alert('認証処理中にエラーが発生しました: ' + error.message); } }); } // Passkey認証ハンドラー if (availableMethods.passkey) { const passkeyBtn = dialog.querySelector('#passkey-2fa-btn'); passkeyBtn.addEventListener('click', async () => { try { // Passkey認証を実行 const optionsResponse = await fetch('/api_proxy.php?endpoint=passkey_login.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ action: 'start' }) }); const optionsData = await optionsResponse.json(); if (!optionsResponse.ok) { alert('Passkey認証の準備に失敗しました'); return; } let authResult; try { authResult = await startAuthentication(optionsData); } catch (authError) { console.error('Passkey認証エラー:', authError); if (authError.name === 'NotAllowedError') { alert('認証がキャンセルされました'); } else { alert('Passkey認証に失敗しました: ' + authError.message); } return; } const verifyResponse = await fetch('/api_proxy.php?endpoint=passkey_login.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ action: 'verify', credential: authResult }) }); const verifyData = await verifyResponse.json(); if (verifyResponse.ok && verifyData.success) { overlay.remove(); window.location.href = '/index.php'; } else { alert('Passkey認証に失敗しました: ' + verifyData.error); } } catch (error) { console.error('Passkey認証エラー:', error); alert('Passkey認証中にエラーが発生しました: ' + error.message); } }); } // キャンセルボタン const cancelBtn = dialog.querySelector('#cancel-2fa-btn'); cancelBtn.addEventListener('click', () => { overlay.remove(); }); }