security update + make easter egg optional.
This commit is contained in:
@@ -3,7 +3,7 @@ import { checkFeatureAccess } from "./feature-gate.js";
|
||||
import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
import { WhepClient } from "./whep.js";
|
||||
import { P2PClient } from "./webrtc-p2p.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/shared-ui-settings.js";
|
||||
|
||||
(async function () {
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const EASTER_EGG_ENABLED_KEY = "overlord_easter_egg_enabled";
|
||||
const EASTER_EGG_SHOWN_KEY = "overlord_easter_egg_shown";
|
||||
const EASTER_EGG_URL = "/assets/console.gif";
|
||||
|
||||
let activeRender = null;
|
||||
|
||||
export function isEasterEggEnabled() {
|
||||
try {
|
||||
return localStorage.getItem(EASTER_EGG_ENABLED_KEY) === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function setEasterEggEnabled(enabled) {
|
||||
try {
|
||||
localStorage.setItem(EASTER_EGG_ENABLED_KEY, String(Boolean(enabled)));
|
||||
} catch {}
|
||||
|
||||
if (!enabled) {
|
||||
try {
|
||||
sessionStorage.removeItem(EASTER_EGG_SHOWN_KEY);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function wasShownThisSession() {
|
||||
try {
|
||||
return sessionStorage.getItem(EASTER_EGG_SHOWN_KEY) === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function markShownThisSession() {
|
||||
try {
|
||||
sessionStorage.setItem(EASTER_EGG_SHOWN_KEY, "true");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function showEnabledEasterEgg({ force = false } = {}) {
|
||||
if (!isEasterEggEnabled() || (!force && wasShownThisSession())) return false;
|
||||
if (activeRender) return activeRender;
|
||||
|
||||
activeRender = (async () => {
|
||||
const response = await fetch(EASTER_EGG_URL);
|
||||
if (!response.ok) throw new Error(`Easter egg failed to load (${response.status})`);
|
||||
|
||||
const blob = await response.blob();
|
||||
if (!blob.type.startsWith("image/")) throw new Error("Easter egg response was not an image");
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
console.log(
|
||||
"%c ",
|
||||
`background:url("${objectUrl}") left top/contain no-repeat;font-size:320px;background-color:transparent`,
|
||||
);
|
||||
markShownThisSession();
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
|
||||
return true;
|
||||
})().finally(() => {
|
||||
activeRender = null;
|
||||
});
|
||||
|
||||
return activeRender;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "./data.js";
|
||||
import { ThumbnailLoader } from "./thumbnail-loader.js";
|
||||
import { initDashboardStats, updateDashboardStatsFromClients } from "./dashboard-stats.js";
|
||||
import { showEnabledEasterEgg } from "./easter-egg.js";
|
||||
|
||||
const grid = document.getElementById("grid");
|
||||
const totalPill = document.getElementById("total-pill");
|
||||
@@ -2243,40 +2244,5 @@ menu.addEventListener("click", async (e) => {
|
||||
|
||||
loadCurrentUser();
|
||||
|
||||
// console easter egg
|
||||
(function () {
|
||||
const runWhenIdle = (fn) => {
|
||||
if ("requestIdleCallback" in window) {
|
||||
window.requestIdleCallback(fn, { timeout: 5000 });
|
||||
return;
|
||||
}
|
||||
window.addEventListener("load", () => setTimeout(fn, 1500), { once: true });
|
||||
};
|
||||
|
||||
const consoleimg = {
|
||||
load: function (src, { size = 320, color = "transparent" } = {}) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", function () {
|
||||
const style =
|
||||
"background: url('" + reader.result + "') left top no-repeat; font-size: " +
|
||||
size +
|
||||
"px; background-size: contain; background-color:" +
|
||||
color;
|
||||
console.log("%c ", style);
|
||||
}, false);
|
||||
fetch(src)
|
||||
.then((r) => r.blob())
|
||||
.then((blob) => {
|
||||
if (blob.type.indexOf("image") === 0) {
|
||||
if (blob.size > 8192 && navigator.userAgent.indexOf("Firefox") > 0)
|
||||
throw new Error("Image size too big to be displayed in Firefox.");
|
||||
return blob;
|
||||
}
|
||||
throw new Error("Valid image not found.");
|
||||
})
|
||||
.then((blob) => reader.readAsDataURL(blob))
|
||||
.catch((err) => console.warn(err.message));
|
||||
},
|
||||
};
|
||||
runWhenIdle(() => consoleimg.load("/assets/console.gif", { size: 320, color: "transparent" }));
|
||||
})();
|
||||
// Opt-in console easter egg. Disabled users never request the large image.
|
||||
showEnabledEasterEgg().catch((err) => console.warn(err.message));
|
||||
|
||||
@@ -4,7 +4,7 @@ import { WhepClient } from "./whep.js";
|
||||
import { P2PClient } from "./webrtc-p2p.js";
|
||||
import { AdaptiveDesktopQuality, normalizeAdaptiveProfiles } from "./rd-adaptive-quality.js";
|
||||
import { createKeyboardCapture } from "./keyboard-capture.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/shared-ui-settings.js";
|
||||
|
||||
(async function () {
|
||||
const clientId = new URLSearchParams(location.search).get("clientId");
|
||||
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
} from "./notify-client.js";
|
||||
import { escapeHtml, formatBytes as formatSharedBytes, formatDate as formatSharedDate } from "./format.js";
|
||||
import { getCertificateTrustGuide } from "./cert-banner.js";
|
||||
import {
|
||||
isEasterEggEnabled,
|
||||
setEasterEggEnabled,
|
||||
showEnabledEasterEgg,
|
||||
} from "./easter-egg.js";
|
||||
|
||||
const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds";
|
||||
const NAV_MODE_KEY = "sb_mode";
|
||||
@@ -40,6 +45,8 @@ const prefNotificationsInput = document.getElementById("pref-notifications");
|
||||
const prefDesktopNotificationsInput = document.getElementById("pref-desktop-notifications");
|
||||
const prefDesktopNotificationsHint = document.getElementById("pref-desktop-notifications-hint");
|
||||
const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds");
|
||||
const easterEggEnabledInput = document.getElementById("easter-egg-enabled");
|
||||
const easterEggStatus = document.getElementById("easter-egg-status");
|
||||
|
||||
const inputArchiveUserForm = document.getElementById("input-archive-user-form");
|
||||
const inputArchiveMyEnabledInput = document.getElementById("input-archive-my-enabled");
|
||||
@@ -656,6 +663,38 @@ function loadPrefs() {
|
||||
}
|
||||
}
|
||||
|
||||
function initEasterEggPreference() {
|
||||
if (!easterEggEnabledInput) return;
|
||||
easterEggEnabledInput.checked = isEasterEggEnabled();
|
||||
easterEggEnabledInput.addEventListener("change", async () => {
|
||||
const enabled = easterEggEnabledInput.checked;
|
||||
setEasterEggEnabled(enabled);
|
||||
if (!easterEggStatus) return;
|
||||
|
||||
if (!enabled) {
|
||||
easterEggStatus.textContent = "Disabled. The image will not be requested.";
|
||||
easterEggStatus.className = "text-xs text-slate-500";
|
||||
return;
|
||||
}
|
||||
|
||||
easterEggEnabledInput.disabled = true;
|
||||
easterEggStatus.textContent = "Loading the easter egg…";
|
||||
easterEggStatus.className = "text-xs text-violet-300";
|
||||
try {
|
||||
await showEnabledEasterEgg({ force: true });
|
||||
easterEggStatus.textContent = "Enabled and shown in the browser console.";
|
||||
easterEggStatus.className = "text-xs text-emerald-400";
|
||||
} catch (error) {
|
||||
setEasterEggEnabled(false);
|
||||
easterEggEnabledInput.checked = false;
|
||||
easterEggStatus.textContent = `Could not load the easter egg: ${error.message}`;
|
||||
easterEggStatus.className = "text-xs text-rose-400";
|
||||
} finally {
|
||||
easterEggEnabledInput.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
@@ -2926,6 +2965,7 @@ async function init() {
|
||||
await loadCurrentUser();
|
||||
applyPermissionVisibility();
|
||||
loadPrefs();
|
||||
initEasterEggPreference();
|
||||
await loadMfaStatus();
|
||||
|
||||
if (userHas("clients:build")) {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
export async function loadSharedUiSettings(scope) {
|
||||
try {
|
||||
const res = await fetch(`/api/ui-settings/${encodeURIComponent(scope)}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) return {};
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return data && typeof data.settings === "object" && data.settings ? data.settings : {};
|
||||
} catch (err) {
|
||||
console.warn(`ui-settings: failed to load ${scope}`, err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function createSharedUiSettingsSaver(scope, readSettings, delayMs = 350) {
|
||||
let timer = null;
|
||||
let lastPayload = "";
|
||||
|
||||
async function saveNow() {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
let settings = {};
|
||||
try {
|
||||
settings = readSettings() || {};
|
||||
} catch (err) {
|
||||
console.warn(`ui-settings: failed to read ${scope}`, err);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(settings);
|
||||
if (payload === lastPayload) return;
|
||||
lastPayload = payload;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/ui-settings/${encodeURIComponent(scope)}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
keepalive: true,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ settings }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
lastPayload = "";
|
||||
}
|
||||
} catch (err) {
|
||||
lastPayload = "";
|
||||
console.warn(`ui-settings: failed to save ${scope}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSave() {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(saveNow, delayMs);
|
||||
}
|
||||
|
||||
return { scheduleSave, saveNow };
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
import { checkFeatureAccess } from "./feature-gate.js";
|
||||
import { WhepClient } from "./whep.js";
|
||||
import { P2PClient } from "./webrtc-p2p.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-settings.js";
|
||||
import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./generated/shared-ui-settings.js";
|
||||
|
||||
(async function () {
|
||||
const clientId = new URLSearchParams(location.search).get("clientId");
|
||||
|
||||
Reference in New Issue
Block a user