// a fucntion to check the role and username if exist let it in and show corressponding module for the role by save the role in localstorage/global variable function setCookie(name, value) { document.cookie = `${name}=${encodeURIComponent( value, )}; path=/; SameSite=Lax`; // console.log( // `đŸĒ Session cookie "${name}" set (no expiry, lasts until browser closes)` // ); } function getCookie(name) { return document.cookie.split("; ").reduce((r, v) => { const parts = v.split("="); return parts[0] === name ? decodeURIComponent(parts[1]) : r; }, null); } function deleteCookie(name) { document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; } function generateCodeVerifier(length = 64) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; let verifier = ""; const array = new Uint32Array(length); crypto.getRandomValues(array); for (let i = 0; i < length; i++) { verifier += chars[array[i] % chars.length]; } return verifier; } function base64urlEncode(buffer) { return btoa(String.fromCharCode(...new Uint8Array(buffer))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest("SHA-256", data); return base64urlEncode(digest); } async function startLogin() { const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); setCookie(env.FRONTEND_APP_NAME + "PKCEVerifier", verifier); const params = new URLSearchParams({ response_type: "code", client_id: env.CLIENT_ID, redirect_uri: env.FRONTEND_URL + "/" + env.FRONTEND_CALLBACK_PATH, scope: "openid profile", code_challenge: challenge, code_challenge_method: "S256", }); window.location.href = env.AUTH_ENDPOINT + "?" + params.toString(); // console.log(JSON.stringify(`${env.AUTH_ENDPOINT}?${params.toString()}`)); } async function startLoginWithMyXS() { const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); setCookie(env.FRONTEND_APP_NAME + "PKCEVerifier", verifier); const params = new URLSearchParams({ response_type: "code", client_id: env.CLIENT_ID, redirect_uri: env.FRONTEND_URL + "/" + env.FRONTEND_CALLBACK_PATH_MYXS, scope: "openid profile", code_challenge: challenge, code_challenge_method: "S256", }); window.location.href = env.AUTH_ENDPOINT_MYXS + "?" + params.toString(); } function parseJwt(token) { try { const payload = token.split(".")[1]; const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); return JSON.parse(decoded); } catch (e) { return null; } } function parseJwtAndGetProfile() { try { let idToken, accessToken, refreshToken = null; idToken = parseJwt(getCookie(env.FRONTEND_APP_NAME + "IdToken")); accessToken = parseJwt(getCookie(env.FRONTEND_APP_NAME + "AccessToken")); refreshToken = parseJwt(getCookie(env.FRONTEND_APP_NAME + "RefreshToken")); return { id_token_payload: idToken, access_token_payload: accessToken, refresh_token_payload: refreshToken, }; } catch (e) { return null; } } function isTokenExpired(token) { const payload = parseJwt(token); if (!payload || !payload.exp) return true; const now = Math.floor(Date.now() / 1000); return payload.exp < now; } let refreshInProgress = null; // shared promise across all functions function shouldRunAclGuard() { const page = window.location.pathname.split("/").pop().toLowerCase(); return ["index.html", "whitelist.html", "blacklist.html"].includes(page); } function waitForTokenThenCheckRole() { return new Promise((resolve) => { let attempts = 0; const maxAttempts = 20; const timer = setInterval(() => { const token = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); if (token) { clearInterval(timer); checkRoleOrLogout(); // đŸ”Ĩ WILL NOW LOGOUT resolve(); } attempts++; if (attempts >= maxAttempts) { clearInterval(timer); logoutNoRole(); resolve(); } }, 200); }); } async function bootstrapApp() { const params = new URLSearchParams(window.location.search); const isSsoReturn = params.has("code"); // Phase 1: authentication if (!isSsoReturn) { const ok = await checkToken(); if (!ok) return; } document.body.classList.remove("auth-pending"); displayProfileFromToken(); loadSidebar(); // ✅ ACL guard only on allowed pages if (shouldRunAclGuard()) { aclGuard(); } // Phase 2: authorization (AFTER token exists) await waitForTokenThenCheckRole(); } async function checkToken() { const token = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); if (!token) { // console.log("đŸšĢ No access token found."); // Show a short message on the page document.body.innerHTML = `

Session Expired

Redirecting to login page...

`; const idToken = getCookie(env.FRONTEND_APP_NAME + "IdToken"); setTimeout(() => { if (idToken) { // console.log( // "🔑 ID token hint found — logging out via OpenID logout flow." // ); logout(); // assumes your logout() handles id_token_hint redirect } else { // console.log( // "â†Šī¸ No ID token hint — redirecting directly to login page." // ); const loginUrl = `${env.FRONTEND_URL}/${env.FRONTEND_LOGIN_PATH}`; window.location.href = loginUrl; } }, 1000); return false; } if (isTokenExpired(token)) { // console.log("âš ī¸ Access token expired — attempting refresh..."); const result = await refreshAccessToken(); // ✅ <-- FIXED if (!result || !result.access_token) { console.error("❌ Token refresh failed — logging out."); // alert("❌ Token refresh failed — logging out."); logout(); return false; } // ✅ Wait for cookie write await new Promise((resolve) => setTimeout(resolve, 300)); const newToken = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); if (!newToken) { console.error("❌ New access token missing after refresh. Logging out."); // alert("❌ New access token missing after refresh. Logging out."); logout(); return false; } // console.log("✅ Token refresh successful and stored."); } else { console.log("✅ Token is still valid."); } return true; } async function refreshAccessToken() { if (refreshInProgress) { // console.log("âŗ Refresh already in progress — waiting..."); return refreshInProgress; // ✅ Wait for the ongoing one } refreshInProgress = (async () => { // console.log("🚀 Entered refreshAccessToken()"); try { const refreshToken = getCookie(env.FRONTEND_APP_NAME + "RefreshToken"); // console.log("🔍 Refresh token:", refreshToken ? "[found]" : "[missing]"); if (!refreshToken) { console.warn("âš ī¸ No refresh token found. Logging out..."); // alert("âš ī¸ No refresh token found. Logging out..."); logout(); return null; } const accessToken = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); let tokenEndpoint = env.TOKEN_ENDPOINT; if (accessToken) { try { const payload = parseJwt(accessToken); if (payload?.iss?.includes("/myxs")) { tokenEndpoint = env.TOKEN_ENDPOINT_MYXS; } } catch (err) { console.warn("âš ī¸ Could not parse old access token:", err); } } // console.log("🌐 Using token endpoint:", tokenEndpoint); const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: env.CLIENT_ID, }); // console.log("📤 Sending refresh request..."); const response = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString(), }); // console.log("📡 Refresh response:", response.status, response.statusText); if (!response.ok) { const text = await response.text(); console.error("❌ Refresh token failed:", response.status, text); // alert("❌ Refresh token failed:", response.status, text); logout(); return null; } const data = await response.json(); // ✅ Save tokens if (data.access_token) setCookie(env.FRONTEND_APP_NAME + "AccessToken", data.access_token); if (data.id_token) setCookie(env.FRONTEND_APP_NAME + "IdToken", data.id_token); if (data.refresh_token) setCookie(env.FRONTEND_APP_NAME + "RefreshToken", data.refresh_token); // 🕒 Log Malaysia timestamp const nowMY = new Date(Date.now() + 8 * 60 * 60 * 1000); const malaysiaTimestamp = nowMY.toISOString().replace("Z", "+08:00"); localStorage.setItem( env.FRONTEND_APP_NAME + "LastTokenRefresh", malaysiaTimestamp, ); // console.log(`✅ Refreshed at (MYT): ${malaysiaTimestamp}`); // 🕓 Log token expiry if (data.access_token) { const payload = parseJwt(data.access_token); if (payload?.exp) { const expMYT = new Date(payload.exp * 1000 + 8 * 60 * 60 * 1000); // console.log( // `🕓 Token expires at (MYT): ${expMYT // .toISOString() // .replace("Z", "+08:00")}` // ); } } // console.log("🏁 refreshAccessToken() completed"); return data; } catch (err) { console.error("đŸ’Ĩ Error refreshing token:", err); // alert("đŸ’Ĩ Error refreshing token:", err); logout(); return null; } finally { refreshInProgress = null; // ✅ Release the lock } })(); return refreshInProgress; } function logout() { localStorage.removeItem("currentClient"); const idToken = getCookie(env.FRONTEND_APP_NAME + "IdToken"); const accessToken = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); let logoutEndpoint = env.LOGOUT_ENDPOINT_MYXS; // Determine logout endpoint based on iss claim in access token if (accessToken) { const payload = parseJwt(accessToken); if (payload && payload.iss) { if (payload.iss.includes("/digital-id")) { logoutEndpoint = env.LOGOUT_ENDPOINT; } else if (payload.iss.includes("/myxs")) { logoutEndpoint = env.LOGOUT_ENDPOINT_MYXS; } } } // ⛔ Show which endpoint is used BEFORE redirect // alert("Logging out using endpoint:\n" + logoutEndpoint); const logoutURL = logoutEndpoint + "?post_logout_redirect_uri=" + encodeURIComponent(env.FRONTEND_URL + "/login.html") + "&id_token_hint=" + encodeURIComponent(idToken); // Do redirect window.location.href = logoutURL; deleteCookie(env.FRONTEND_APP_NAME + "AccessToken"); deleteCookie(env.FRONTEND_APP_NAME + "IdToken"); deleteCookie(env.FRONTEND_APP_NAME + "RefreshToken"); deleteCookie(env.FRONTEND_APP_NAME + "PKCEVerifier"); } function checkTokenIssuers() { const idToken = getCookie(env.FRONTEND_APP_NAME + "IdToken"); const accessToken = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); const parse = (token) => { try { const base64Url = token.split(".")[1]; return JSON.parse(atob(base64Url.replace(/-/g, "+").replace(/_/g, "/"))); } catch { return null; } }; const idPayload = idToken ? parse(idToken) : null; const accessPayload = accessToken ? parse(accessToken) : null; console.log("=== Token ISS Check ==="); console.log("ID Token:"); console.log(idPayload ? idPayload.iss : "❌ No ID Token or invalid token"); console.log("Access Token:"); console.log( accessPayload ? accessPayload.iss : "❌ No Access Token or invalid token", ); return { idIss: idPayload?.iss || null, accessIss: accessPayload?.iss || null, }; } async function exchangeCodeForToken() { const params = new URLSearchParams(window.location.search); const code = params.get("code"); const verifier = getCookie(env.FRONTEND_APP_NAME + "PKCEVerifier"); if (!code || !verifier) { document.body.innerHTML = `

Missing code or verifier

Please try logging in again.

`; return; } const body = new URLSearchParams({ grant_type: "authorization_code", client_id: env.CLIENT_ID, code: code, redirect_uri: env.FRONTEND_URL + "/" + env.FRONTEND_CALLBACK_PATH, code_verifier: verifier, }); try { // console.log(env.TOKEN_ENDPOINT); // console.log(body.toString()); const response = await fetch(env.TOKEN_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); const data = await response.json(); if (data.http_code && data.http_code >= 400 && data.http_code <= 500) { //data.url contains httpcode, errTitle, errDesc window.location.href = data.url; return; } if (data.access_token) setCookie(env.FRONTEND_APP_NAME + "AccessToken", data.access_token); if (data.id_token) setCookie(env.FRONTEND_APP_NAME + "IdToken", data.id_token); if (data.refresh_token) setCookie(env.FRONTEND_APP_NAME + "RefreshToken", data.refresh_token); document.body.innerHTML = `

Login successful

Redirecting you to the dashboard...

`; setTimeout(() => { window.location.href = env.FRONTEND_URL + "/" + env.FRONTEND_HOME_PATH; }, 1000); } catch (err) { document.body.innerHTML = `

Error during token exchange

${err}
`; } } async function exchangeCodeForTokenMYXS() { const params = new URLSearchParams(window.location.search); const code = params.get("code"); const verifier = getCookie(env.FRONTEND_APP_NAME + "PKCEVerifier"); if (!code || !verifier) { document.body.innerHTML = `

Missing code or verifier

Please try logging in again.

`; return; } const body = new URLSearchParams({ grant_type: "authorization_code", client_id: env.CLIENT_ID, code: code, redirect_uri: env.FRONTEND_URL + "/" + env.FRONTEND_CALLBACK_PATH_MYXS, code_verifier: verifier, }); try { // console.log(env.TOKEN_ENDPOINT); // console.log(body.toString()); const response = await fetch(env.TOKEN_ENDPOINT_MYXS, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: body.toString(), }); const data = await response.json(); if (data.http_code && data.http_code >= 400 && data.http_code <= 500) { //data.url contains httpcode, errTitle, errDesc window.location.href = data.url; return; } if (data.access_token) setCookie(env.FRONTEND_APP_NAME + "AccessToken", data.access_token); if (data.id_token) setCookie(env.FRONTEND_APP_NAME + "IdToken", data.id_token); if (data.refresh_token) setCookie(env.FRONTEND_APP_NAME + "RefreshToken", data.refresh_token); document.body.innerHTML = `

Login successful

Redirecting you to the dashboard...

`; setTimeout(() => { window.location.href = env.FRONTEND_URL + "/" + env.FRONTEND_HOME_PATH; }, 1000); } catch (err) { document.body.innerHTML = `

Error during token exchange

${err}
`; } } async function displayProfileFromToken() { try { const ok = await checkToken(); if (!ok) return; // ⛔ prevent API call when refresh failed if (typeof env === "undefined" || !env.FRONTEND_APP_NAME) { console.error("env or FRONTEND_APP_NAME is undefined"); return; } if (typeof getCookie !== "function") { console.error("getCookie function not found"); return; } const token = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); if (!token) { console.error("No access token found."); return; } if (!env.USERINFO_ENDPOINT) { console.error("USERINFO_ENDPOINT is undefined in env"); return; } // checkToken(); const response = await fetch(env.USERINFO_ENDPOINT, { method: "GET", headers: { Accept: "*/*", "Content-Type": "application/json", Authorization: "Bearer " + token, }, }); if (!response.ok) { console.error("Failed to fetch user info. Status:", response.status); return; } const payload = await response.json(); // console.log("Userinfo payload:", payload); const legalName = payload.legal_name || payload.sub; const username = payload.preferred_username; if (!legalName || !username) { console.error("Missing required fields in userinfo response:", payload); return; } const nameElement = document.getElementById("lblDisplayName"); const nameElement2 = document.getElementById("lblDisplayName2"); const nameElement3 = document.getElementById("lblDisplayName3"); if (!nameElement && !nameElement2 && !nameElement3) { console.warn("No HTML elements found for displaying name."); } if (nameElement) nameElement.innerText = legalName; if (nameElement2) nameElement2.innerText = legalName; if (nameElement3) nameElement3.innerText = username; } catch (err) { console.error("Error in displayProfileFromToken:", err); } } async function fetchUserInfo() { const ok = await checkToken(); if (!ok) return; // ⛔ prevent API call when refresh failed const token = getCookie(env.FRONTEND_APP_NAME + "AccessToken"); if (!token) { throw new Error("Access token not found in cookies"); } const url = env.USERINFO_ENDPOINT; try { const response = await fetch(url, { method: "GET", headers: { Accept: "*/*", "Accept-Language": "en-US,en;q=0.9", Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, // credentials: "include", // keep PHPSESSID etc }); if (!response.ok) { throw new Error(`Failed: ${response.status} ${response.statusText}`); } return await response.json(); } catch (err) { console.error("Error fetching user info:", err); throw err; } } // ==================================================== // Inactivity Logout Timer (No Token Refresh) // ==================================================== // 🔧 Timeout (30 minutes) const INACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 mins in ms let inactivityTimer; let countdownInterval; // ---------- Helper: Format Time ---------- function formatTime(seconds) { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}m ${s < 10 ? "0" : ""}${s}s`; } // ------------------------------ // Toast Notification Function // ------------------------------ function showToastMessage(msg, type = "success", timeout = 3000) { let toast = document.getElementById("toast"); // Create toast if not already present if (!toast) { toast = document.createElement("div"); toast.id = "toast"; document.body.appendChild(toast); } // Reset and apply classes toast.className = `toast ${type}`; toast.textContent = msg; // Force reflow to restart animation void toast.offsetWidth; // Show toast toast.classList.add("show"); // Clear previous timeout if (toast.hideTimeout) clearTimeout(toast.hideTimeout); // Hide after timeout toast.hideTimeout = setTimeout(() => { toast.classList.remove("show"); }, timeout); } // Run inactivity only if NOT on login.html if (!window.location.pathname.endsWith("login.html")) { // ---------- Reset Inactivity Timer ---------- function resetInactivityTimer() { if (inactivityTimer) clearTimeout(inactivityTimer); if (countdownInterval) clearInterval(countdownInterval); let remaining = INACTIVITY_TIMEOUT / 1000; countdownInterval = setInterval(() => { remaining--; if (remaining <= 0) clearInterval(countdownInterval); }, 1000); inactivityTimer = setTimeout(() => { console.warn("âš ī¸ User inactive for 30 minutes — logging out."); clearInterval(countdownInterval); logout(); }, INACTIVITY_TIMEOUT); } // ---------- Watch for User Activity ---------- ["click", "mousemove", "keydown", "scroll", "touchstart"].forEach((event) => { window.addEventListener(event, resetInactivityTimer); }); // ---------- Start Immediately ---------- document.addEventListener("DOMContentLoaded", resetInactivityTimer); }