1/* 2 * CELLO WEB â reálná admin brána (nahrazuje 2026-09-12 dÅÃvÄjšà DEMO 3 * gate se sessionStorage a heslem "1234", viz DECISION-2026-09-12-05). 4 * 5 * Uživatel výslovnÄ potvrdil: "login do administrace pouze pÅes 6 * Oauth2" â tenhle soubor teÄ kontroluje SKUTEÄNOU Supabase Auth 7 * session (Google OAuth), ne sessionStorage flag. SkuteÄné vynucenà 8 * pÅÃstupu je pÅes RLS (is_admin()/has_module_access() v 9 * supabase/migrations/20260912000605_admin_profiles_and_rls.sql) â 10 * tenhle soubor jen ÅÃdà UI (skrytà odkazů, pÅesmÄrovánà na login), 11 * stejnÄ jako pÅedtÃm dÄlal demo gate jen na klientovi. 12 * 13 * NaÄÃtá se jako klasický <script src="..."> na ~20 admin stránkách 14 * beze zmÄny (ne <script type="module">), proto Supabase JS SDK 15 * naÄÃtáme pÅes dynamický import() sdÃleného klienta (jeden GoTrue 16 * klient pro celou administraci, ne jeden per stránka). 17 */ 18(function () { 19 "use strict"; 20 21 // "domu" (HP), "uzivatele" a "gdpr" jsou vždy jen pro Admina â nejsou 22 // grantovatelné, viz architektura sekce 5/6. Tohle je jen UI seznam 23 // pro schovánà odkazů/obsahu; skuteÄné vynucenà dÄlá RLS, ne tohle pole. 24 var ADMIN_ONLY_MODULES = ["domu", "uzivatele", "gdpr", "logy", "rozesilka", "vzhled", "seo", "cron", "integrace", "cookies", "gema-klienti", "gema-mailing"]; 25 26 var supabasePromise = null; 27 function getSupabase() { 28 if (!supabasePromise) { 29 supabasePromise = import("./supabase-client.js").then(function (mod) { 30 return mod.supabase; 31 }); 32 } 33 return supabasePromise; 34 } 35 36 // Session + profil aktuálnÄ pÅihlášeného admina â jednotlivé moduly 37 // (napÅ. aktuality.html) si tohle nemusà zjišťovat znovu, guardPage() 38 // to už jednou zjistilo. Vracà null pÅed dokonÄenÃm guardPage(). 39 var currentContext = { session: null, profile: null }; 40 function getCurrentContext() { 41 return currentContext; 42 } 43 44 // 2026-10-01 oprava: `getCurrentContext().session.access_token` je 45 // "vyfocený" JEDNOU pÅi guardPage() a dál se nikdy needosvÄžuje â na 46 // dlouho otevÅené stránce (hodiny, jako tahle session) token vypršà a 47 // GemaApi volánà pak padajà na 401, i když je uživatel poÅád reálnÄ 48 // pÅihlášený (Supabase SDK token na pozadà tiÅ¡e obnovuje, ale nikdo tu 49 // obnovenou hodnotu nikam nezapisuje). `getSession()` vracà vždy 50 // AKTUÃLNà (podle potÅeby ÄerstvÄ obnovený) token, proto ho API klienti 51 // (gema-api.js) majà volat tÄsnÄ pÅed každým requestem, ne spoléhat na 52 // snapshot z guardPage(). 53 async function getFreshAccessToken() { 54 var supabase = await getSupabase(); 55 var sessionResult = await supabase.auth.getSession(); 56 var session = sessionResult.data && sessionResult.data.session; 57 return session && session.access_token; 58 } 59 60 function logout() { 61 return getSupabase().then(function (supabase) { 62 return supabase.auth.signOut(); 63 }); 64 } 65 66 // ZamÄená položka zůstává focusovatelná (viz guardPage nÞe) â tenhle 67 // listener jen zabránà samotné navigaci, aÅ¥ se nestane dojem "nic se 68 // nedÄje" u myÅ¡i i klávesnice (Enter na <a> vyvolá "click" stejnÄ). 69 function preventNavigationIfLocked(e) { 70 e.preventDefault(); 71 } 72 73 function moduleAllowedFactory(role, grantedModules) { 74 return function moduleAllowed(moduleKey) { 75 if (ADMIN_ONLY_MODULES.indexOf(moduleKey) !== -1) return role === "admin"; 76 if (role === "admin") return true; 77 return grantedModules.indexOf(moduleKey) !== -1; 78 }; 79 } 80 81 // Voláno na zaÄátku každé admin/*.html stránky. Asynchronnà â volajÃcà 82 // stránky výsledek neÄekajà (fire-and-forget stejnÄ jako dÅÃv), DOM 83 // úpravy se prostÄ promÃtnou, jakmile session/profil dorazÃ. 84 async function guardPage(currentModuleKey) { 85 var supabase = await getSupabase(); 86 var sessionResult = await supabase.auth.getSession(); 87 var session = sessionResult.data && sessionResult.data.session; 88 if (!session) { 89 window.location.href = "../login.html"; 90 return; 91 } 92 93 var profileResult = await supabase 94 .from("admin_profiles") 95 .select("role, is_active, display_name") 96 .eq("id", session.user.id) 97 .maybeSingle(); 98 99 var profile = profileResult.data; 100 if (profileResult.error || !profile || !profile.is_active) { 101 // PÅihlášený Google úÄet, ale bez platného/aktivnÃho admin 102 // profilu -> odhlásit a poslat zpÄt na login s vysvÄtlenÃm 103 // (login.html samo zobrazà důvod, viz jeho ?denied= parametr). 104 await supabase.auth.signOut(); 105 window.location.href = "../login.html?denied=1"; 106 return; 107 } 108 109 currentContext.session = session; 110 currentContext.profile = profile; 111 112 var role = profile.role; // "admin" | "editor" 113 var grantedModules = []; 114 if (role !== "admin") { 115 var grantsResult = await supabase 116 .from("admin_module_access") 117 .select("module") 118 .eq("admin_id", session.user.id); 119 grantedModules = (grantsResult.data || []).map(function (row) { return row.module; }); 120 } 121 var moduleAllowed = moduleAllowedFactory(role, grantedModules); 122
123 // Hlavnà menu: uzamkni položky, na které aktuálnà role nedosáhne. 124 // `href`/fokusovatelnost se NEODEBÃRà (2026-10-01 oprava) â dÅÃv 125 // `removeAttribute("href")` vyÅadilo zamÄenou položku z tab-poÅadà 126 // úplnÄ, takže o nà klávesnicový/screen-reader uživatel vůbec 127 // nevÄdÄl. TeÄ zůstává fokusovatelná, `aria-disabled` oznamuje 128 // stav a klik/Enter se zablokuje v JS (preventNavigationIfLocked), 129 // ne pÅes CSS `pointer-events:none`. 130 document.querySelectorAll("[data-module]").forEach(function (el) { 131 var mod = el.getAttribute("data-module"); 132 var allowed = moduleAllowed(mod); 133 var li = el.closest("li"); 134 if (li) li.classList.toggle("is-locked", !allowed); 135 136 var existingBadge = el.querySelector(".subnav-lock-badge"); 137 if (existingBadge) existingBadge.remove(); 138 139 if (!allowed) { 140 el.setAttribute("aria-disabled", "true"); 141 el.addEventListener("click", preventNavigationIfLocked); 142 var badge = document.createElement("span"); 143 badge.className = "subnav-lock-badge"; 144 badge.setAttribute("aria-hidden", "true"); 145 badge.textContent = "ð"; 146 var srText = document.createElement("span"); 147 srText.className = "visually-hidden"; 148 srText.textContent = " â zamÄeno, vyhrazeno roli Admin"; 149 el.appendChild(badge); 150 el.appendChild(srText); 151 } else { 152 el.removeAttribute("aria-disabled"); 153 el.removeEventListener("click", preventNavigationIfLocked); 154 } 155 }); 156 157 // Jméno/e-mail pÅihlášeného admina, pokud na stránce existuje 158 // odpovÃdajÃcà placeholder. 159 document.querySelectorAll("[data-current-admin-name]").forEach(function (el) { 160 el.textContent = profile.display_name || session.user.email; 161 }); 162 163 // Odhlásit â skuteÄné Supabase signOut, ne jen smazánà sessionStorage. 164 var signOut = document.querySelector("[data-action='sign-out']"); 165 if (signOut) { 166 signOut.addEventListener("click", function (e) { 167 e.preventDefault(); 168 logout().then(function () { 169 window.location.href = "../login.html"; 170 }); 171 }); 172 } 173 174 // Prvky viditelné jen roli Admin. 175 document.querySelectorAll("[data-admin-only]").forEach(function (el) { 176 el.hidden = role !== "admin"; 177 }); 178 179 // Sekce vázané na konkrétnà modul mimo aktuálnà stránku. 180 document.querySelectorAll("[data-module-section]").forEach(function (el) { 181 var mod = el.getAttribute("data-module-section"); 182 el.hidden = !moduleAllowed(mod); 183 }); 184 185 // Ochrana obsahu aktuálnà stránky. 186 if (currentModuleKey && !moduleAllowed(currentModuleKey)) { 187 var content = document.getElementById("protected-content"); 188 var notice = document.getElementById("locked-notice"); 189 if (content) content.hidden = true; 190 if (notice) notice.hidden = false; 191 } 192 } 193 194 // guardPage() vracà svůj Promise, ale volajÃcà stránky ho dnes 195 // neÄekajà (fire-and-forget, viz komentáŠu guardPage). Moduly, které 196 // potÅebujà znát pÅihlášeného admina PÅED vlastnà pracà (napÅ. 197 // aktuality.html kvůli created_by), zavolajà guardReady() a poÄkajà 198 // na nÄj â nespouÅ¡tà druhé kolo kontroly, jen zabalà poslednà 199 // guardPage() volánà do Promise, na který jde Äekat. 200 var guardPromise = null; 201 function guardReady(currentModuleKey) { 202 if (!guardPromise) guardPromise = guardPage(currentModuleKey); 203 return guardPromise.then(function () { return currentContext; }); 204 } 205 206 // Pro VEÅEJNà stránky (index.html a dalÅ¡Ã), které chtÄjà jen tiÅ¡e 207 // zjistit "je tenhle návÅ¡tÄvnÃk pÅihlášený aktivnà admin?" (kvůli 208 // liÅ¡tÄ "â ZpÄt do administrace") â na rozdÃl od guardPage()/ 209 // guardReady() NIKDY nepÅesmÄruje, jen vrátà true/false. Nahrazuje 210 // dÅÃvÄjšà synchronnà demo isAuthed()/getRole() (sessionStorage), 211 // které pÅestalo existovat pÅi pÅechodu na skuteÄné OAuth 212 // (DECISION-2026-09-12-05) â 6 veÅejných stránek na nÄj jeÅ¡tÄ 213 // odkazovalo a padalo s TypeError, nalezeno a opraveno 2026-09-12. 214 async function checkAdminSession() { 215 try { 216 var supabase = await getSupabase(); 217 var sessionResult = await supabase.auth.getSession(); 218 var session = sessionResult.data && sessionResult.data.session; 219 if (!session) return false;
220 var profileResult = await supabase 221 .from("admin_profiles") 222 .select("is_active") 223 .eq("id", session.user.id) 224 .maybeSingle(); 225 return !!(profileResult.data && profileResult.data.is_active); 226 } catch (e) { 227 return false; 228 } 229 } 230 231 window.CelloAdminDemo = { 232 getSupabase: getSupabase, 233 getCurrentContext: getCurrentContext, 234 getFreshAccessToken: getFreshAccessToken, 235 logout: logout, 236 guardPage: guardPage, 237 guardReady: guardReady, 238 checkAdminSession: checkAdminSession, 239 }; 240})();
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.