PageSourceSearch

https://longevityandcare.cz/assets/js/admin-auth.js

js longevityandcare.cz collected 2026-10-09 23:17:30 UTC 10,177 bytes, 240 lines download raw bytes

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.