Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/raw-mirror-paths.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ rss.xml
images/branding/logo-light.png
images/branding/logo-dark.png
icons/favicon.ico
icons/favicon.svg
4 changes: 2 additions & 2 deletions static/icons/site.webmanifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "/?source=pwa",
"name": "dhanur.me",
"short_name": "dhanur",
"short_name": "dhanur.me",
"description": "Unified progressive web application for all that is dhanur.me",
"start_url": "/?source=pwa",
"scope": "/",
Expand All @@ -18,7 +18,7 @@
"dir": "ltr",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"orientation": "any",
"orientation": "portrait",
"prefer_related_applications": false,
"related_applications": [],
"icons": [
Expand Down
58 changes: 37 additions & 21 deletions static/js/core/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,19 @@ function getShellRuntimeConfig() {
return merged;
}

// Determines if relative paths work (primary domain context)
function isSameOriginHost() {
return window.location.origin === BASE_URL;
}

// Authorizes execution across all controlled top-level and subdomains
function isTrustedHost() {
const host = window.location.hostname;
return (
host === "dhanur.me" || host.endsWith(".dhanur.me") || host === "localhost"
);
}

function maybeRegisterServiceWorker(config) {
if (config.enablePwa === false) return;
if (!("serviceWorker" in navigator) || !window.isSecureContext) return;
Expand Down Expand Up @@ -210,9 +219,6 @@ function safeIconClass(value) {
}

function renderAppsGrid(shellRoot, apps) {
// Backward compatibility:
// - New markup uses: [data-app-menu-grid="desktop"|"mobile"]
// - Older injected markup used: [data-apps-grid] and [data-apps-grid-mobile]
const grids = shellRoot.querySelectorAll(
"[data-app-menu-grid], [data-apps-grid], [data-apps-grid-mobile]",
);
Expand Down Expand Up @@ -260,12 +266,10 @@ function renderAppsGrid(shellRoot, apps) {
}

async function updateAppsGridForRole(shellRoot, role) {
// Render cached/fallback immediately (fast paint).
const cached = getManifestSync();
const cachedVisible = filterAppsByRole(cached.apps, role);
renderAppsGrid(shellRoot, cachedVisible);

// Then refresh from the live manifest source of truth.
const fresh = await fetchManifest(role);
const freshVisible = filterAppsByRole(fresh.apps, role);
renderAppsGrid(shellRoot, freshVisible);
Expand All @@ -284,25 +288,36 @@ function initMobilePanel(shellRoot) {

function openPanel() {
panel.classList.remove("hidden");
// Force reflow before adding transition class
void drawer.offsetWidth;
drawer.style.transform = "translateX(0)";
// Force DOM reflow to trigger slide animation cleanly
void panel.offsetWidth;
if (drawer) drawer.style.transform = "translateX(0)";
document.body.style.overflow = "hidden";
}

function closePanel() {
drawer.style.transform = "translateX(-100%)";
if (drawer) drawer.style.transform = "translateX(-100%)";
document.body.style.overflow = "";
setTimeout(() => {
panel.classList.add("hidden");
}, 300);
}

if (toggle) toggle.addEventListener("click", openPanel);
if (backdrop) backdrop.addEventListener("click", closePanel);
if (closeBtn) closeBtn.addEventListener("click", closePanel);
// Prevent duplicate event binding if hydrated multiple times
if (toggle && !toggle.hasAttribute("data-shell-bound")) {
toggle.setAttribute("data-shell-bound", "true");
toggle.addEventListener("click", openPanel);
}

if (backdrop && !backdrop.hasAttribute("data-shell-bound")) {
backdrop.setAttribute("data-shell-bound", "true");
backdrop.addEventListener("click", closePanel);
}

if (closeBtn && !closeBtn.hasAttribute("data-shell-bound")) {
closeBtn.setAttribute("data-shell-bound", "true");
closeBtn.addEventListener("click", closePanel);
}

// Close on escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !panel.classList.contains("hidden")) {
closePanel();
Expand Down Expand Up @@ -357,7 +372,6 @@ function syncMobileAuth(shellRoot, authStatus) {
if (accountBtn) accountBtn.classList.add("hidden");
}

// Wire up mobile logout
if (logoutBtn) {
logoutBtn.onclick = (e) => {
e.preventDefault();
Expand All @@ -375,7 +389,6 @@ function syncMobileAuth(shellRoot, authStatus) {
};
}

// Wire up mobile login
if (loginBtn) {
loginBtn.onclick = (e) => {
e.preventDefault();
Expand All @@ -400,8 +413,6 @@ function hydrate(shellRoot) {
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]',
);

// On subdomains always inject shared assets.
// On same-origin shell test pages, inject only if missing.
if (!sameOrigin || !hasMainStyles) {
injectCSS(sameOrigin, config);
}
Expand Down Expand Up @@ -448,21 +459,26 @@ async function bootstrapShell() {
const config = getShellRuntimeConfig();
if (config.showNavbar === false) return;

// Enforce execution mapping strictly to authorized domain zones
if (!isTrustedHost()) {
console.warn("[shell.js] Execution blocked on unauthorized origin.");
return;
}

maybeRegisterServiceWorker(config);

const sameOrigin = isSameOriginHost();

const existingNavbar = document.querySelector(".navbar");

if (existingNavbar) {
// If shell is loaded onto the main site, skip mutations; if external and pre-rendered, hydrate.
// Subdomains bypass primary origin scripts, requiring shell hydration
if (!sameOrigin) {
hydrate(document.body);
}
return;
}

// Security hardening: do not inject cross-origin HTML into this document.
// External consumers should ship a local shell markup and then call hydrate().
// Inject underlying platform CSS assets cleanly if targeting missing navigation blocks
if (!sameOrigin) {
injectCSS(false, config);
injectFavicons(false, config);
Expand Down
Loading
Loading