// マジックリンク認証後のリダイレクト処理 function handleAuthRedirect() { const hash = window.location.hash; const search = window.location.search; let accessToken = ""; let refreshToken = ""; if (hash) { const params = new URLSearchParams(hash.substring(1)); accessToken = params.get("access_token"); refreshToken = params.get("refresh_token"); } if (!accessToken && search) { const params = new URLSearchParams(search); accessToken = params.get("access_token"); refreshToken = params.get("refresh_token"); } if (accessToken) { // トークンをローカルストレージに保存 localStorage.setItem("sb_access_token", accessToken); if (refreshToken) { localStorage.setItem("sb_refresh_token", refreshToken); } localStorage.setItem("sb_login_time", Date.now().toString()); const targetUrl = `${WATCHER_APP_URL}?access_token=${encodeURIComponent(accessToken)}` + (refreshToken ? `&refresh_token=${encodeURIComponent(refreshToken)}` : ""); window.location.href = targetUrl; } } // --- 2週間自動ログイン(セッション保持)の追加機能 --- function clearSavedSession() { localStorage.removeItem("sb_access_token"); localStorage.removeItem("sb_refresh_token"); localStorage.removeItem("sb_login_time"); } async function validateToken(token) { try { const response = await fetch("https://rciadogpebxrulmvpymp.supabase.co/auth/v1/user", { method: "GET", headers: { "Authorization": `Bearer ${token}`, "apikey": "sb_publishable_Z7o5-eVVhkbqITSHfQ-2-w_v_CqctYk" } }); return response.ok; } catch (e) { return false; } } async function refreshSession(refreshToken) { try { const response = await fetch("https://rciadogpebxrulmvpymp.supabase.co/auth/v1/token?grant_type=refresh_token", { method: "POST", headers: { "Content-Type": "application/json", "apikey": "sb_publishable_Z7o5-eVVhkbqITSHfQ-2-w_v_CqctYk" }, body: JSON.stringify({ refresh_token: refreshToken }) }); if (response.ok) { const data = await response.json(); localStorage.setItem("sb_access_token", data.access_token); localStorage.setItem("sb_refresh_token", data.refresh_token); localStorage.setItem("sb_login_time", Date.now().toString()); return data; } } catch (e) {} return null; } function redirectToApp(accessToken, refreshToken) { const targetUrl = `${WATCHER_APP_URL}?access_token=${encodeURIComponent(accessToken)}` + (refreshToken ? `&refresh_token=${encodeURIComponent(refreshToken)}` : ""); window.location.href = targetUrl; } async function checkAutoLogin() { const accessToken = localStorage.getItem("sb_access_token"); const refreshToken = localStorage.getItem("sb_refresh_token"); const loginTime = localStorage.getItem("sb_login_time"); if (!accessToken) return; // 2週間の期限チェック (14日) const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000; if (loginTime && (Date.now() - parseInt(loginTime) > TWO_WEEKS_MS)) { clearSavedSession(); return; } const msg = document.getElementById("message"); if (msg) { msg.className = "text-emerald-400 text-sm mt-2"; msg.textContent = "自動ログイン中..."; } const isValid = await validateToken(accessToken); if (isValid) { redirectToApp(accessToken, refreshToken); } else if (refreshToken) { const newTokens = await refreshSession(refreshToken); if (newTokens) { redirectToApp(newTokens.access_token, newTokens.refresh_token); } else { clearSavedSession(); if (msg) { msg.className = "text-red-400 text-sm mt-2"; msg.textContent = "セッションの有効期限が切れました。再度ログインしてください。"; } } } } window.addEventListener("DOMContentLoaded", () => { handleAuthRedirect(); if (!window.location.hash && !window.location.search) { checkAutoLogin(); } }); // OTPコード送信処理 async function sendOtpCode() { const email = document.getElementById("email").value.trim(); const btn = document.getElementById("btn-send-code"); const msg = document.getElementById("message"); if (!email || !email.includes("@")) { alert("正しいメールアドレスを入力してください。"); return; } try { btn.disabled = true; btn.textContent = "送信中..."; msg.textContent = ""; const response = await fetch("https://rciadogpebxrulmvpymp.supabase.co/auth/v1/otp", { method: "POST", headers: { "Content-Type": "application/json", "apikey": "sb_publishable_Z7o5-eVVhkbqITSHfQ-2-w_v_CqctYk" }, body: JSON.stringify({ email: email }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.msg || err.message || "コード送信に失敗しました"); } msg.className = "text-emerald-400 text-sm mt-2"; msg.textContent = "メールで6桁の認証コードを送信しました!"; // 次のステップのUIを展開 document.getElementById("email").disabled = true; btn.classList.add("hidden"); document.getElementById("otp-step").classList.remove("hidden"); } catch (err) { msg.className = "text-red-400 text-sm mt-2"; msg.textContent = "エラー: " + err.message; btn.disabled = false; btn.textContent = "認証コードを送信"; } } // OTP検証&ログイン完了処理 async function verifyOtpCode(e) { e.preventDefault(); const email = document.getElementById("email").value.trim(); const code = document.getElementById("otp-code").value.trim(); const btn = document.getElementById("btn-verify"); const msg = document.getElementById("message"); if (!code || code.length !== 6) { alert("6桁の認証コードを入力してください"); return; } try { btn.disabled = true; btn.textContent = "認証中..."; msg.textContent = ""; const response = await fetch("https://rciadogpebxrulmvpymp.supabase.co/auth/v1/verify", { method: "POST", headers: { "Content-Type": "application/json", "apikey": "sb_publishable_Z7o5-eVVhkbqITSHfQ-2-w_v_CqctYk" }, body: JSON.stringify({ type: "email", email: email, token: code }) }); if (!response.ok) { const err = await response.json(); throw new Error(err.msg || err.message || "認証コードが正しくありません"); } const data = await response.json(); const accessToken = data.access_token; const refreshToken = data.refresh_token; // ログイン成功時にローカルストレージへ保存 localStorage.setItem("sb_access_token", accessToken); if (refreshToken) { localStorage.setItem("sb_refresh_token", refreshToken); } localStorage.setItem("sb_login_time", Date.now().toString()); msg.className = "text-emerald-400 text-sm mt-2"; msg.textContent = "ログイン成功!アプリへリダイレクトします..."; // アプリ本体へ転送 setTimeout(() => { const targetUrl = `${WATCHER_APP_URL}?access_token=${encodeURIComponent(accessToken)}` + (refreshToken ? `&refresh_token=${encodeURIComponent(refreshToken)}` : ""); window.location.href = targetUrl; }, 800); } catch (err) { msg.className = "text-red-400 text-sm mt-2"; msg.textContent = "エラー: " + err.message; btn.disabled = false; btn.textContent = "ログインを完了する"; } } The above content does NOT show the entire file contents. If you need to view any lines of the file which were not shown to complete your task, call this tool again to view those lines.