1/* 2 TGSL Page Management Runtime 3 4 - Reads /api/settings/page-management 5 - For non-staff users: hides links to disabled pages and blocks access 6 via redirect when a disabled route is opened. 7 8 Notes: 9 - The app uses HashRouter, but also mirrors hash routes into clean paths. 10 - This script is intentionally framework-agnostic (DOM + URL based). 11*/ 12 13(function () { 14 "use strict"; 15 16 const API_BASE = "/api"; 17 18 const DEFAULT_CFG = { 19 home: true, 20 destinations: true, 21 blog: true, 22 vehicles: true, 23 hotels: true, 24 guides: true, 25 drivers: true, 26 planner: true, 27 about: true, 28 contact: true, 29 faq: true, 30 privacy: true, 31 terms: true, 32 register: true, 33 }; 34 35 function normalizeCfg(cfg) { 36 const out = Object.assign({}, DEFAULT_CFG); 37 if (!cfg || typeof cfg !== "object") return out; 38 Object.keys(DEFAULT_CFG).forEach((k) => { 39 if (Object.prototype.hasOwnProperty.call(cfg, k)) { 40 out[k] = !!cfg[k]; 41 } 42 }); 43 return out; 44 } 45 46 function hasLoginHintCookie() { 47 try { 48 return document.cookie.indexOf("tgsl_logged_in=1") !== -1; 49 } catch (e) { 50 return false; 51 } 52 } 53 54 async function fetchJson(path, options) { 55 const res = await fetch(API_BASE + path, Object.assign({ 56 method: "GET", 57 credentials: "include", 58 headers: { "Content-Type": "application/json" }, 59 }, options || {})); 60 61 const text = await res.text(); 62 let data = null; 63 try { data = JSON.parse(text); } catch (e) { data = null; } 64 65 if (!res.ok) { 66 const msg = (data && data.error) ? data.error : (text || ("HTTP " + res.status)); 67 throw new Error(msg); 68 } 69 return data; 70 } 71 72 function getRoutePath() { 73 try { 74 const h = String(window.location.hash || ""); 75 if (h.startsWith("#/")) { 76 const p = h.slice(1); // "/guides?x=1" 77 return p.split("?")[0] || "/"; 78 } 79 // Fallback: clean path 80 return String(window.location.pathname || "/") || "/"; 81 } catch (e) { 82 return "/"; 83 } 84 } 85 86 function pickFallbackRoute(cfg) { 87 const priority = [ 88 ["home", "/"], 89 ["destinations", "/destinations"], 90 ["vehicles", "/vehicles"], 91 ["hotels", "/hotels"], 92 ["guides", "/guides"], 93 ["drivers", "/drivers"], 94 ["blog", "/blog"], 95 ["planner", "/planner"], 96 ["about", "/about"], 97 ["contact", "/contact"], 98 ["faq", "/faq"], 99 ["privacy", "/privacy"], 100 ["terms", "/terms"], 101 // If everything is disabled, keep the user on login (always available) 102 ["_always", "/login"], 103 ]; 104 105 for (const [k, path] of priority) { 106 if (k === "_always") return path; 107 if (cfg && cfg[k]) return path; 108 } 109 return "/login"; 110 } 111 112 function disabledKeyForPath(path, cfg) { 113 const p = String(path || "/"); 114 if (p === "/" || p === "") { 115 return cfg.home ? null : "home"; 116 } 117 118 // Listing section pages + detail pages 119 if (p.startsWith("/destinations") || p.startsWith("/destination/")) return cfg.destinations ? null : "destinations"; 120 if (p.startsWith("/blog") || p.startsWith("/blog/")) return cfg.blog ? null : "blog"; 121 if (p.startsWith("/vehicles") || p.startsWith("/vehicle/")) return cfg.vehicles ? null : "vehicles"; 122 if (p.startsWith("/hotels") || p.startsWith("/hotel/")) return cfg.hotels ? null : "hotels"; 123 if (p.startsWith("/guides") || p.startsWith("/guide/")) return cfg.guides ? null : "guides"; 124 if (p.startsWith("/drivers") || p.startsWith("/driver/")) return cfg.drivers ? null : "drivers"; 125 126 // Static pages 127 if (p.startsWith("/planner")) return cfg.planner ? null : "planner"; 128 if (p.startsWith("/about")) return cfg.about ? null : "about"; 129 if (p.startsWith("/contact")) return cfg.contact ? null : "contact"; 130 if (p.startsWith("/faq")) return cfg.faq ? null : "faq"; 131 if (p.startsWith("/privacy")) return cfg.privacy ? null : "privacy"; 132 if (p.startsWith("/terms")) return cfg.terms ? null : "terms"; 133 if (p.startsWith("/register")) return cfg.register ? null : "register"; 134 135 return null; 136 } 137 138 function redirectTo(path) { 139 try { 140 const p = String(path || "/"); 141 const target = window.location.origin + "/#" + (p.startsWith("/") ? p : ("/" + p)); 142 // Prevent redirect loops 143 if (window.location.href === target) return; 144 window.location.replace(target); 145 } catch (e) {
146 try { 147 window.location.hash = "#" + path; 148 } catch (e2) {} 149 } 150 } 151 152 function hrefAttr(el) { 153 if (!el) return ""; 154 try { 155 return String(el.getAttribute("href") || ""); 156 } catch (e) { 157 return ""; 158 } 159 } 160 161 function markHidden(el) { 162 try { 163 if (!el || el.nodeType !== 1) return; 164 if (el.getAttribute("data-tgsl-page-hidden") === "1") return; 165 el.setAttribute("data-tgsl-page-hidden", "1"); 166 el.style.display = "none"; 167 } catch (e) {} 168 } 169 170 function rewriteHref(el, newPath) { 171 try { 172 if (!el || el.nodeType !== 1) return; 173 const p = String(newPath || "/"); 174 el.setAttribute("href", p); 175 } catch (e) {} 176 } 177 178 function applyDomVisibility(cfg) { 179 const fallback = pickFallbackRoute(cfg); 180 181 // Exact home links: rewrite to fallback when home is disabled. 182 if (!cfg.home) { 183 const homeLinks = document.querySelectorAll('a[href="/"], a[href="#/"], a[href="/#/"]'); 184 homeLinks.forEach((a) => rewriteHref(a, fallback)); 185 } 186 187 // Helper: hide all anchors matching any of the provided patterns. 188 function hideByPatterns(patterns, exactOnly) { 189 const anchors = document.querySelectorAll("a[href]"); 190 anchors.forEach((a) => { 191 const h = hrefAttr(a); 192 if (!h) return; 193 194 for (const p of patterns) { 195 if (exactOnly) { 196 if (h === p) { markHidden(a); return; } 197 } else { 198 if (h.indexOf(p) !== -1) { markHidden(a); return; } 199 } 200 } 201 }); 202 } 203 204 // Section pages + their detail pages 205 if (!cfg.destinations) hideByPatterns(["/destinations", "#/destinations", "/destination/", "#/destination/"], false); 206 if (!cfg.blog) hideByPatterns(["/blog", "#/blog"], false); 207 if (!cfg.vehicles) hideByPatterns(["/vehicles", "#/vehicles", "/vehicle/", "#/vehicle/"], false); 208 if (!cfg.hotels) hideByPatterns(["/hotels", "#/hotels", "/hotel/", "#/hotel/"], false); 209 if (!cfg.guides) hideByPatterns(["/guides", "#/guides", "/guide/", "#/guide/"], false); 210 if (!cfg.drivers) hideByPatterns(["/drivers", "#/drivers", "/driver/", "#/driver/"], false); 211 212 // Static pages 213 if (!cfg.planner) hideByPatterns(["/planner", "#/planner"], false); 214 if (!cfg.about) hideByPatterns(["/about", "#/about"], false); 215 if (!cfg.contact) hideByPatterns(["/contact", "#/contact"], false); 216 if (!cfg.faq) hideByPatterns(["/faq", "#/faq"], false); 217 if (!cfg.privacy) hideByPatterns(["/privacy", "#/privacy"], false); 218 if (!cfg.terms) hideByPatterns(["/terms", "#/terms"], false); 219 if (!cfg.register) hideByPatterns(["/register", "#/register"], false); 220 } 221 222 function enforceCurrentRoute(cfg) { 223 const p = getRoutePath(); 224 const disabledKey = disabledKeyForPath(p, cfg); 225 if (!disabledKey) return; 226 const fallback = pickFallbackRoute(cfg); 227 redirectTo(fallback); 228 } 229 230 let state = { 231 cfg: normalizeCfg(null), 232 isStaff: false, 233 started: false, 234 }; 235 236 function applyAll() { 237 if (state.isStaff) return; 238 enforceCurrentRoute(state.cfg); 239 applyDomVisibility(state.cfg); 240 } 241 242 function startObservers() { 243 if (state.started) return; 244 state.started = true; 245 246 // Apply immediately (best-effort) 247 applyAll(); 248 249 // Re-apply on route changes 250 window.addEventListener("hashchange", applyAll); 251 window.addEventListener("popstate", applyAll); 252 253 // Re-apply when React updates DOM 254 try { 255 const obs = new MutationObserver(() => { 256 applyDomVisibility(state.cfg); 257 }); 258 obs.observe(document.documentElement, { childList: true, subtree: true }); 259 } catch (e) {} 260 } 261 262 async function init() { 263 // 1) Load page management config (public endpoint) 264 try { 265 const data = await fetchJson("/settings/page-management"); 266 state.cfg = normalizeCfg((data && data.pageManagement) ? data.pageManagement : data); 267 } catch (e) { 268 state.cfg = normalizeCfg(null); 269 } 270 271 // 2) Determine if current user is staff (admin/editor) 272 if (hasLoginHintCookie()) { 273 try { 274 const me = await fetchJson("/auth/me"); 275 const u = (me && me.user) ? me.user : me; 276 state.isStaff = !!(u && (u.isAdmin || u.role === "admin" || u.role === "editor")); 277 } catch (e) { 278 state.isStaff = false;
279 } 280 } 281 282 // Expose for debugging 283 try { 284 window.__tgsl_page_management = { cfg: state.cfg, isStaff: state.isStaff }; 285 } catch (e) {} 286 287 startObservers(); 288 } 289 290 // Allow live updates (admin panel can dispatch this event) 291 window.addEventListener("tgsl-page-management-updated", function (ev) { 292 try { 293 if (state.isStaff) return; 294 const next = ev && ev.detail && ev.detail.pageManagement; 295 if (!next) return; 296 state.cfg = normalizeCfg(next); 297 applyAll(); 298 } catch (e) {} 299 }); 300 301 // Boot 302 init(); 303})();
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.