add build profiles
This commit is contained in:
@@ -17,6 +17,14 @@ const rawServerListCheckbox = document.getElementById("raw-server-list");
|
|||||||
const serverUrlInput = document.getElementById("server-url");
|
const serverUrlInput = document.getElementById("server-url");
|
||||||
const solMemoCheckbox = document.getElementById("sol-memo");
|
const solMemoCheckbox = document.getElementById("sol-memo");
|
||||||
const solSettings = document.getElementById("sol-settings");
|
const solSettings = document.getElementById("sol-settings");
|
||||||
|
const profileSelect = document.getElementById("build-profile-select");
|
||||||
|
const profileNameInput = document.getElementById("build-profile-name");
|
||||||
|
const profileSaveBtn = document.getElementById("profile-save-btn");
|
||||||
|
const profileLoadBtn = document.getElementById("profile-load-btn");
|
||||||
|
const profileDeleteBtn = document.getElementById("profile-delete-btn");
|
||||||
|
const profileExportBtn = document.getElementById("profile-export-btn");
|
||||||
|
const profileImportBtn = document.getElementById("profile-import-btn");
|
||||||
|
const profileImportFile = document.getElementById("profile-import-file");
|
||||||
|
|
||||||
let currentServerVersion = null;
|
let currentServerVersion = null;
|
||||||
let currentUserRole = null;
|
let currentUserRole = null;
|
||||||
@@ -87,42 +95,129 @@ function updateWindowsSectionVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BUILD_SETTINGS_KEY = "overlord_build_settings";
|
const BUILD_SETTINGS_KEY = "overlord_build_settings";
|
||||||
|
const PROFILE_EXPORT_MAGIC = "overlord-build-profile";
|
||||||
|
|
||||||
|
let buildProfiles = [];
|
||||||
|
|
||||||
|
function collectFormSettings() {
|
||||||
|
return {
|
||||||
|
platforms: Array.from(document.querySelectorAll('input[name="platform"]')).map((el) => ({ value: el.value, checked: el.checked })),
|
||||||
|
serverUrl: document.getElementById("server-url")?.value ?? "",
|
||||||
|
rawServerList: document.getElementById("raw-server-list")?.checked ?? false,
|
||||||
|
solMemo: document.getElementById("sol-memo")?.checked ?? false,
|
||||||
|
solAddress: document.getElementById("sol-address")?.value ?? "",
|
||||||
|
solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value ?? "",
|
||||||
|
outputName: document.getElementById("output-name")?.value ?? "",
|
||||||
|
iosBundleId: document.getElementById("ios-bundle-id")?.value ?? "",
|
||||||
|
mutex: document.getElementById("mutex")?.value ?? "",
|
||||||
|
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
|
||||||
|
stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true,
|
||||||
|
disableCgo: document.querySelector('input[name="disable-cgo"]')?.checked ?? false,
|
||||||
|
noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false,
|
||||||
|
obfuscate: document.querySelector('input[name="obfuscate"]')?.checked ?? false,
|
||||||
|
garbleLiterals: document.querySelector('input[name="garble-literals"]')?.checked ?? false,
|
||||||
|
garbleTiny: document.querySelector('input[name="garble-tiny"]')?.checked ?? false,
|
||||||
|
garbleSeed: document.getElementById("garble-seed")?.value ?? "",
|
||||||
|
enableUpx: document.querySelector('input[name="enable-upx"]')?.checked ?? false,
|
||||||
|
upxStripHeaders: document.querySelector('input[name="upx-strip-headers"]')?.checked ?? false,
|
||||||
|
sleepSeconds: document.getElementById("sleep-seconds")?.value ?? "0",
|
||||||
|
enablePersistence: document.querySelector('input[name="enable-persistence"]')?.checked ?? false,
|
||||||
|
persistenceMethods: Array.from(document.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value),
|
||||||
|
startupName: document.getElementById("startup-name")?.value ?? "",
|
||||||
|
hideConsole: document.querySelector('input[name="hide-console"]')?.checked ?? false,
|
||||||
|
requireAdmin: document.querySelector('input[name="require-admin"]')?.checked ?? false,
|
||||||
|
criticalProcess: document.querySelector('input[name="critical-process"]')?.checked ?? false,
|
||||||
|
assemblyTitle: document.getElementById("assembly-title")?.value ?? "",
|
||||||
|
assemblyProduct: document.getElementById("assembly-product")?.value ?? "",
|
||||||
|
assemblyCompany: document.getElementById("assembly-company")?.value ?? "",
|
||||||
|
assemblyVersion: document.getElementById("assembly-version")?.value ?? "",
|
||||||
|
assemblyCopyright: document.getElementById("assembly-copyright")?.value ?? "",
|
||||||
|
outputExtension: document.getElementById("output-extension")?.value ?? ".exe",
|
||||||
|
cryptableMode: document.getElementById("cryptable-mode")?.checked ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFormSettings(settings) {
|
||||||
|
if (!settings || typeof settings !== "object") return;
|
||||||
|
|
||||||
|
const setVal = (id, val) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el && val !== undefined && val !== null) el.value = String(val);
|
||||||
|
};
|
||||||
|
const setCb = (sel, val) => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (el && val !== undefined) el.checked = !!val;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(settings.platforms)) {
|
||||||
|
settings.platforms.forEach(({ value, checked }) => {
|
||||||
|
const el = document.querySelector(`input[name="platform"][value="${value}"]`);
|
||||||
|
if (el) el.checked = !!checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (settings.serverUrl !== undefined) setVal("server-url", settings.serverUrl);
|
||||||
|
if (settings.rawServerList !== undefined) setCb("#raw-server-list", settings.rawServerList);
|
||||||
|
if (settings.solMemo !== undefined) setCb("#sol-memo", settings.solMemo);
|
||||||
|
if (settings.solAddress !== undefined) setVal("sol-address", settings.solAddress);
|
||||||
|
if (settings.solRpcEndpoints !== undefined) setVal("sol-rpc-endpoints", settings.solRpcEndpoints);
|
||||||
|
if (settings.outputName !== undefined) setVal("output-name", settings.outputName);
|
||||||
|
if (settings.iosBundleId !== undefined) setVal("ios-bundle-id", settings.iosBundleId);
|
||||||
|
if (settings.mutex !== undefined) setVal("mutex", settings.mutex);
|
||||||
|
if (settings.disableMutex !== undefined) setCb('input[name="disable-mutex"]', settings.disableMutex);
|
||||||
|
if (settings.stripDebug !== undefined) setCb('input[name="strip-debug"]', settings.stripDebug);
|
||||||
|
if (settings.disableCgo !== undefined) setCb('input[name="disable-cgo"]', settings.disableCgo);
|
||||||
|
if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting);
|
||||||
|
if (settings.obfuscate !== undefined) setCb('input[name="obfuscate"]', settings.obfuscate);
|
||||||
|
if (settings.garbleLiterals !== undefined) setCb('input[name="garble-literals"]', settings.garbleLiterals);
|
||||||
|
if (settings.garbleTiny !== undefined) setCb('input[name="garble-tiny"]', settings.garbleTiny);
|
||||||
|
if (settings.garbleSeed !== undefined) setVal("garble-seed", settings.garbleSeed);
|
||||||
|
if (settings.enableUpx !== undefined) setCb('input[name="enable-upx"]', settings.enableUpx);
|
||||||
|
if (settings.upxStripHeaders !== undefined) setCb('input[name="upx-strip-headers"]', settings.upxStripHeaders);
|
||||||
|
if (settings.sleepSeconds !== undefined) setVal("sleep-seconds", settings.sleepSeconds);
|
||||||
|
if (settings.enablePersistence !== undefined) setCb('input[name="enable-persistence"]', settings.enablePersistence);
|
||||||
|
if (Array.isArray(settings.persistenceMethods)) {
|
||||||
|
document.querySelectorAll('input[name="persistence-method"]').forEach((el) => {
|
||||||
|
el.checked = settings.persistenceMethods.includes(el.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (settings.startupName !== undefined) setVal("startup-name", settings.startupName);
|
||||||
|
if (settings.hideConsole !== undefined) setCb('input[name="hide-console"]', settings.hideConsole);
|
||||||
|
if (settings.requireAdmin !== undefined) setCb('input[name="require-admin"]', settings.requireAdmin);
|
||||||
|
if (settings.criticalProcess !== undefined) setCb('input[name="critical-process"]', settings.criticalProcess);
|
||||||
|
if (settings.assemblyTitle !== undefined) setVal("assembly-title", settings.assemblyTitle);
|
||||||
|
if (settings.assemblyProduct !== undefined) setVal("assembly-product", settings.assemblyProduct);
|
||||||
|
if (settings.assemblyCompany !== undefined) setVal("assembly-company", settings.assemblyCompany);
|
||||||
|
if (settings.assemblyVersion !== undefined) setVal("assembly-version", settings.assemblyVersion);
|
||||||
|
if (settings.assemblyCopyright !== undefined) setVal("assembly-copyright", settings.assemblyCopyright);
|
||||||
|
if (settings.outputExtension !== undefined) setVal("output-extension", settings.outputExtension);
|
||||||
|
if (settings.cryptableMode !== undefined) setCb("#cryptable-mode", settings.cryptableMode);
|
||||||
|
|
||||||
|
const restoredObfuscate = document.querySelector('input[name="obfuscate"]');
|
||||||
|
const garbleContainer = document.getElementById("garble-settings-container");
|
||||||
|
if (restoredObfuscate && garbleContainer) {
|
||||||
|
garbleContainer.classList.toggle("hidden", !restoredObfuscate.checked);
|
||||||
|
}
|
||||||
|
const restoredUpx = document.querySelector('input[name="enable-upx"]');
|
||||||
|
const upxContainer = document.getElementById("upx-settings-container");
|
||||||
|
if (restoredUpx && upxContainer) {
|
||||||
|
upxContainer.classList.toggle("hidden", !restoredUpx.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWindowsSectionVisibility();
|
||||||
|
updateIosSectionVisibility();
|
||||||
|
updatePersistenceSettingsVisibility();
|
||||||
|
if (solMemoCheckbox && solSettings) {
|
||||||
|
solSettings.classList.toggle("hidden", !solMemoCheckbox.checked);
|
||||||
|
}
|
||||||
|
if (serverUrlInput && rawServerListCheckbox) {
|
||||||
|
serverUrlInput.placeholder = getDefaultServerUrlPlaceholder(rawServerListCheckbox.checked);
|
||||||
|
}
|
||||||
|
applyCryptableMode(document.getElementById("cryptable-mode")?.checked || false);
|
||||||
|
}
|
||||||
|
|
||||||
function saveFormSettings() {
|
function saveFormSettings() {
|
||||||
try {
|
try {
|
||||||
const settings = {
|
const settings = collectFormSettings();
|
||||||
platforms: Array.from(document.querySelectorAll('input[name="platform"]')).map((el) => ({ value: el.value, checked: el.checked })),
|
|
||||||
serverUrl: document.getElementById("server-url")?.value ?? "",
|
|
||||||
rawServerList: document.getElementById("raw-server-list")?.checked ?? false,
|
|
||||||
solMemo: document.getElementById("sol-memo")?.checked ?? false,
|
|
||||||
solAddress: document.getElementById("sol-address")?.value ?? "",
|
|
||||||
solRpcEndpoints: document.getElementById("sol-rpc-endpoints")?.value ?? "",
|
|
||||||
mutex: document.getElementById("mutex")?.value ?? "",
|
|
||||||
disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false,
|
|
||||||
stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true,
|
|
||||||
disableCgo: document.querySelector('input[name="disable-cgo"]')?.checked ?? false,
|
|
||||||
noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false,
|
|
||||||
obfuscate: document.querySelector('input[name="obfuscate"]')?.checked ?? false,
|
|
||||||
garbleLiterals: document.querySelector('input[name="garble-literals"]')?.checked ?? false,
|
|
||||||
garbleTiny: document.querySelector('input[name="garble-tiny"]')?.checked ?? false,
|
|
||||||
garbleSeed: document.getElementById("garble-seed")?.value ?? "",
|
|
||||||
enableUpx: document.querySelector('input[name="enable-upx"]')?.checked ?? false,
|
|
||||||
upxStripHeaders: document.querySelector('input[name="upx-strip-headers"]')?.checked ?? false,
|
|
||||||
sleepSeconds: document.getElementById("sleep-seconds")?.value ?? "0",
|
|
||||||
enablePersistence: document.querySelector('input[name="enable-persistence"]')?.checked ?? false,
|
|
||||||
persistenceMethods: Array.from(document.querySelectorAll('input[name="persistence-method"]:checked')).map((el) => el.value),
|
|
||||||
startupName: document.getElementById("startup-name")?.value ?? "",
|
|
||||||
hideConsole: document.querySelector('input[name="hide-console"]')?.checked ?? false,
|
|
||||||
requireAdmin: document.querySelector('input[name="require-admin"]')?.checked ?? false,
|
|
||||||
criticalProcess: document.querySelector('input[name="critical-process"]')?.checked ?? false,
|
|
||||||
assemblyTitle: document.getElementById("assembly-title")?.value ?? "",
|
|
||||||
assemblyProduct: document.getElementById("assembly-product")?.value ?? "",
|
|
||||||
assemblyCompany: document.getElementById("assembly-company")?.value ?? "",
|
|
||||||
assemblyVersion: document.getElementById("assembly-version")?.value ?? "",
|
|
||||||
assemblyCopyright: document.getElementById("assembly-copyright")?.value ?? "",
|
|
||||||
outputExtension: document.getElementById("output-extension")?.value ?? ".exe",
|
|
||||||
cryptableMode: document.getElementById("cryptable-mode")?.checked ?? false,
|
|
||||||
};
|
|
||||||
localStorage.setItem(BUILD_SETTINGS_KEY, JSON.stringify(settings));
|
localStorage.setItem(BUILD_SETTINGS_KEY, JSON.stringify(settings));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to save form settings:", err);
|
console.error("Failed to save form settings:", err);
|
||||||
@@ -133,67 +228,138 @@ function restoreFormSettings() {
|
|||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(BUILD_SETTINGS_KEY);
|
const raw = localStorage.getItem(BUILD_SETTINGS_KEY);
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
const s = JSON.parse(raw);
|
const settings = JSON.parse(raw);
|
||||||
|
applyFormSettings(settings);
|
||||||
const setVal = (id, val) => { const el = document.getElementById(id); if (el && val !== undefined) el.value = val; };
|
|
||||||
const setCb = (sel, val) => { const el = document.querySelector(sel); if (el && val !== undefined) el.checked = val; };
|
|
||||||
|
|
||||||
if (Array.isArray(s.platforms)) {
|
|
||||||
s.platforms.forEach(({ value, checked }) => {
|
|
||||||
const el = document.querySelector(`input[name="platform"][value="${value}"]`);
|
|
||||||
if (el) el.checked = checked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (s.serverUrl !== undefined) setVal("server-url", s.serverUrl);
|
|
||||||
if (s.rawServerList !== undefined) setCb("#raw-server-list", s.rawServerList);
|
|
||||||
if (s.solMemo !== undefined) setCb("#sol-memo", s.solMemo);
|
|
||||||
if (s.solAddress !== undefined) setVal("sol-address", s.solAddress);
|
|
||||||
if (s.solRpcEndpoints !== undefined) setVal("sol-rpc-endpoints", s.solRpcEndpoints);
|
|
||||||
if (s.mutex !== undefined) setVal("mutex", s.mutex);
|
|
||||||
if (s.disableMutex !== undefined) setCb('input[name="disable-mutex"]', s.disableMutex);
|
|
||||||
if (s.stripDebug !== undefined) setCb('input[name="strip-debug"]', s.stripDebug);
|
|
||||||
if (s.disableCgo !== undefined) setCb('input[name="disable-cgo"]', s.disableCgo);
|
|
||||||
if (s.noPrinting !== undefined) setCb('input[name="no-printing"]', s.noPrinting);
|
|
||||||
if (s.obfuscate !== undefined) setCb('input[name="obfuscate"]', s.obfuscate);
|
|
||||||
if (s.garbleLiterals !== undefined) setCb('input[name="garble-literals"]', s.garbleLiterals);
|
|
||||||
if (s.garbleTiny !== undefined) setCb('input[name="garble-tiny"]', s.garbleTiny);
|
|
||||||
if (s.garbleSeed !== undefined) setVal("garble-seed", s.garbleSeed);
|
|
||||||
if (s.enableUpx !== undefined) setCb('input[name="enable-upx"]', s.enableUpx);
|
|
||||||
if (s.upxStripHeaders !== undefined) setCb('input[name="upx-strip-headers"]', s.upxStripHeaders);
|
|
||||||
if (s.sleepSeconds !== undefined) setVal("sleep-seconds", s.sleepSeconds);
|
|
||||||
if (s.enablePersistence !== undefined) setCb('input[name="enable-persistence"]', s.enablePersistence);
|
|
||||||
if (Array.isArray(s.persistenceMethods)) {
|
|
||||||
document.querySelectorAll('input[name="persistence-method"]').forEach((el) => {
|
|
||||||
el.checked = s.persistenceMethods.includes(el.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (s.startupName !== undefined) setVal("startup-name", s.startupName);
|
|
||||||
if (s.hideConsole !== undefined) setCb('input[name="hide-console"]', s.hideConsole);
|
|
||||||
if (s.requireAdmin !== undefined) setCb('input[name="require-admin"]', s.requireAdmin);
|
|
||||||
if (s.criticalProcess !== undefined) setCb('input[name="critical-process"]', s.criticalProcess);
|
|
||||||
if (s.assemblyTitle !== undefined) setVal("assembly-title", s.assemblyTitle);
|
|
||||||
if (s.assemblyProduct !== undefined) setVal("assembly-product", s.assemblyProduct);
|
|
||||||
if (s.assemblyCompany !== undefined) setVal("assembly-company", s.assemblyCompany);
|
|
||||||
if (s.assemblyVersion !== undefined) setVal("assembly-version", s.assemblyVersion);
|
|
||||||
if (s.assemblyCopyright !== undefined) setVal("assembly-copyright", s.assemblyCopyright);
|
|
||||||
if (s.outputExtension !== undefined) setVal("output-extension", s.outputExtension);
|
|
||||||
if (s.cryptableMode !== undefined) setCb("#cryptable-mode", s.cryptableMode);
|
|
||||||
|
|
||||||
const restoredObfuscate = document.querySelector('input[name="obfuscate"]');
|
|
||||||
const garbleContainer = document.getElementById("garble-settings-container");
|
|
||||||
if (restoredObfuscate && garbleContainer) {
|
|
||||||
garbleContainer.classList.toggle("hidden", !restoredObfuscate.checked);
|
|
||||||
}
|
|
||||||
const restoredUpx = document.querySelector('input[name="enable-upx"]');
|
|
||||||
const upxContainer = document.getElementById("upx-settings-container");
|
|
||||||
if (restoredUpx && upxContainer) {
|
|
||||||
upxContainer.classList.toggle("hidden", !restoredUpx.checked);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to restore form settings:", err);
|
console.error("Failed to restore form settings:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeProfileName(name) {
|
||||||
|
if (typeof name !== "string") return "";
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!trimmed || trimmed.length > 64) return "";
|
||||||
|
if (!/^[A-Za-z0-9 _.-]+$/.test(trimmed)) return "";
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProfileOptions(selectedName = "") {
|
||||||
|
if (!profileSelect) return;
|
||||||
|
profileSelect.innerHTML = "";
|
||||||
|
|
||||||
|
const placeholder = document.createElement("option");
|
||||||
|
placeholder.value = "";
|
||||||
|
placeholder.textContent = "Select a saved profile...";
|
||||||
|
profileSelect.appendChild(placeholder);
|
||||||
|
|
||||||
|
buildProfiles.forEach((profile) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = profile.name;
|
||||||
|
option.textContent = profile.name;
|
||||||
|
if (selectedName && selectedName === profile.name) option.selected = true;
|
||||||
|
profileSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedProfile() {
|
||||||
|
if (!profileSelect || !profileSelect.value) return null;
|
||||||
|
return buildProfiles.find((profile) => profile.name === profileSelect.value) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBuildProfiles(selectedName = "") {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/build/profiles", { credentials: "include" });
|
||||||
|
if (!res.ok) {
|
||||||
|
buildProfiles = [];
|
||||||
|
renderProfileOptions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
buildProfiles = Array.isArray(data?.profiles) ? data.profiles : [];
|
||||||
|
renderProfileOptions(selectedName || profileSelect?.value || "");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load build profiles:", err);
|
||||||
|
buildProfiles = [];
|
||||||
|
renderProfileOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCurrentProfile() {
|
||||||
|
const inputName = profileNameInput?.value || "";
|
||||||
|
const selectedName = profileSelect?.value || "";
|
||||||
|
const profileName = sanitizeProfileName(inputName || selectedName);
|
||||||
|
if (!profileName) {
|
||||||
|
alert("Enter a valid profile name (1-64 chars; letters, numbers, spaces, . _ -)");
|
||||||
|
profileNameInput?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = collectFormSettings();
|
||||||
|
|
||||||
|
const res = await fetch("/api/build/profiles", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ name: profileName, config }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || "Failed to save profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileNameInput) profileNameInput.value = profileName;
|
||||||
|
await loadBuildProfiles(profileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportProfileToFile(name, config) {
|
||||||
|
const payload = {
|
||||||
|
type: PROFILE_EXPORT_MAGIC,
|
||||||
|
version: 1,
|
||||||
|
name,
|
||||||
|
exportedAt: Date.now(),
|
||||||
|
config,
|
||||||
|
};
|
||||||
|
const json = JSON.stringify(payload, null, 2);
|
||||||
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const safeName = (name || "build-profile").replace(/[^A-Za-z0-9._-]/g, "_");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${safeName}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importProfileFromFile(file) {
|
||||||
|
const text = await file.text();
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackName = file.name.replace(/\.json$/i, "");
|
||||||
|
const importedName = sanitizeProfileName(parsed?.name || fallbackName);
|
||||||
|
if (!importedName) {
|
||||||
|
throw new Error("Invalid profile name in imported file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedConfig = parsed?.config;
|
||||||
|
if (!importedConfig || typeof importedConfig !== "object" || Array.isArray(importedConfig)) {
|
||||||
|
throw new Error("Imported profile is missing a valid config object");
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFormSettings(importedConfig);
|
||||||
|
saveFormSettings();
|
||||||
|
|
||||||
|
if (profileNameInput) profileNameInput.value = importedName;
|
||||||
|
await saveCurrentProfile();
|
||||||
|
}
|
||||||
|
|
||||||
const CRYPTABLE_DISABLE_TARGETS = [
|
const CRYPTABLE_DISABLE_TARGETS = [
|
||||||
'input[name="enable-persistence"]',
|
'input[name="enable-persistence"]',
|
||||||
'input[name="enable-upx"]',
|
'input[name="enable-upx"]',
|
||||||
@@ -999,10 +1165,16 @@ async function init() {
|
|||||||
buildBtn.disabled = true;
|
buildBtn.disabled = true;
|
||||||
buildBtn.innerHTML =
|
buildBtn.innerHTML =
|
||||||
'<i class="fa-solid fa-lock"></i> <span>Build requires permission</span>';
|
'<i class="fa-solid fa-lock"></i> <span>Build requires permission</span>';
|
||||||
|
if (profileSaveBtn) profileSaveBtn.disabled = true;
|
||||||
|
if (profileLoadBtn) profileLoadBtn.disabled = true;
|
||||||
|
if (profileDeleteBtn) profileDeleteBtn.disabled = true;
|
||||||
|
if (profileExportBtn) profileExportBtn.disabled = true;
|
||||||
|
if (profileImportBtn) profileImportBtn.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadServerVersion();
|
await loadServerVersion();
|
||||||
await loadSavedBuilds();
|
await loadSavedBuilds();
|
||||||
|
await loadBuildProfiles();
|
||||||
|
|
||||||
const toggleAllBuildsBtn = document.getElementById("toggle-all-builds-btn");
|
const toggleAllBuildsBtn = document.getElementById("toggle-all-builds-btn");
|
||||||
const toggleAllBuildsLabel = document.getElementById("toggle-all-builds-label");
|
const toggleAllBuildsLabel = document.getElementById("toggle-all-builds-label");
|
||||||
@@ -1038,6 +1210,95 @@ if (logoutBtn && !logoutBtn.dataset.boundLogout) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileSelect) {
|
||||||
|
profileSelect.addEventListener("change", () => {
|
||||||
|
const profile = getSelectedProfile();
|
||||||
|
if (profileNameInput) {
|
||||||
|
profileNameInput.value = profile?.name || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileSaveBtn) {
|
||||||
|
profileSaveBtn.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await saveCurrentProfile();
|
||||||
|
alert("Profile saved.");
|
||||||
|
} catch (err) {
|
||||||
|
alert(err?.message || "Failed to save profile");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileLoadBtn) {
|
||||||
|
profileLoadBtn.addEventListener("click", () => {
|
||||||
|
const profile = getSelectedProfile();
|
||||||
|
if (!profile) {
|
||||||
|
alert("Select a profile to load.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyFormSettings(profile.config || {});
|
||||||
|
saveFormSettings();
|
||||||
|
if (profileNameInput) profileNameInput.value = profile.name;
|
||||||
|
alert(`Loaded profile: ${profile.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileDeleteBtn) {
|
||||||
|
profileDeleteBtn.addEventListener("click", async () => {
|
||||||
|
const profile = getSelectedProfile();
|
||||||
|
if (!profile) {
|
||||||
|
alert("Select a profile to delete.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`Delete profile \"${profile.name}\"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/build/profiles/${encodeURIComponent(profile.name)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || "Failed to delete profile");
|
||||||
|
}
|
||||||
|
await loadBuildProfiles();
|
||||||
|
if (profileNameInput) profileNameInput.value = "";
|
||||||
|
alert("Profile deleted.");
|
||||||
|
} catch (err) {
|
||||||
|
alert(err?.message || "Failed to delete profile");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileExportBtn) {
|
||||||
|
profileExportBtn.addEventListener("click", () => {
|
||||||
|
const profile = getSelectedProfile();
|
||||||
|
if (profile) {
|
||||||
|
exportProfileToFile(profile.name, profile.config || {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fallbackName = sanitizeProfileName(profileNameInput?.value || "") || "current-build-profile";
|
||||||
|
exportProfileToFile(fallbackName, collectFormSettings());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileImportBtn && profileImportFile) {
|
||||||
|
profileImportBtn.addEventListener("click", () => profileImportFile.click());
|
||||||
|
profileImportFile.addEventListener("change", async () => {
|
||||||
|
const file = profileImportFile.files?.[0];
|
||||||
|
profileImportFile.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
await importProfileFromFile(file);
|
||||||
|
alert("Profile imported and saved.");
|
||||||
|
} catch (err) {
|
||||||
|
alert(err?.message || "Failed to import profile");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
form?.addEventListener("submit", async (e) => {
|
form?.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,45 @@
|
|||||||
|
|
||||||
<form id="build-form" class="flex flex-col gap-3">
|
<form id="build-form" class="flex flex-col gap-3">
|
||||||
|
|
||||||
|
<div class="border border-slate-800 rounded-lg p-3 bg-slate-800/40 flex flex-col gap-3">
|
||||||
|
<div class="flex items-center gap-2 text-sm font-medium text-slate-200">
|
||||||
|
<i class="fa-solid fa-bookmark text-blue-400"></i>
|
||||||
|
Build Profiles
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<select id="build-profile-select"
|
||||||
|
class="w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors">
|
||||||
|
<option value="">Select a saved profile...</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="build-profile-name" maxlength="64" placeholder="Profile name"
|
||||||
|
class="w-full px-3 py-2 bg-slate-800/70 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
<button type="button" id="profile-save-btn"
|
||||||
|
class="px-3 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium transition-colors">
|
||||||
|
<i class="fa-solid fa-floppy-disk mr-1"></i> Save
|
||||||
|
</button>
|
||||||
|
<button type="button" id="profile-load-btn"
|
||||||
|
class="px-3 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium transition-colors">
|
||||||
|
<i class="fa-solid fa-folder-open mr-1"></i> Load
|
||||||
|
</button>
|
||||||
|
<button type="button" id="profile-delete-btn"
|
||||||
|
class="px-3 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium transition-colors">
|
||||||
|
<i class="fa-solid fa-trash mr-1"></i> Delete
|
||||||
|
</button>
|
||||||
|
<button type="button" id="profile-export-btn"
|
||||||
|
class="px-3 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-700 text-white text-sm font-medium transition-colors">
|
||||||
|
<i class="fa-solid fa-file-export mr-1"></i> Export
|
||||||
|
</button>
|
||||||
|
<button type="button" id="profile-import-btn"
|
||||||
|
class="px-3 py-2 rounded-lg bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium transition-colors">
|
||||||
|
<i class="fa-solid fa-file-import mr-1"></i> Import
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="profile-import-file" accept="application/json,.json" class="hidden" />
|
||||||
|
<p class="text-xs text-slate-500">Profiles are saved per user on the server and can be exported/imported as JSON files.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- == Section 1: Target Platforms (open) == -->
|
<!-- == Section 1: Target Platforms (open) == -->
|
||||||
<div class="accordion-section border border-slate-800 rounded-lg overflow-hidden" data-open="true">
|
<div class="accordion-section border border-slate-800 rounded-lg overflow-hidden" data-open="true">
|
||||||
<button type="button" class="accordion-header w-full flex items-center justify-between px-4 py-3 bg-slate-800/50 hover:bg-slate-800/80 transition-colors text-left">
|
<button type="button" class="accordion-header w-full flex items-center justify-between px-4 py-3 bg-slate-800/50 hover:bg-slate-800/80 transition-colors text-left">
|
||||||
|
|||||||
@@ -193,6 +193,18 @@ try { db.run(`ALTER TABLE builds ADD COLUMN build_tag TEXT`); } catch {}
|
|||||||
try { db.run(`ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER`); } catch {}
|
try { db.run(`ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER`); } catch {}
|
||||||
db.run(`CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);`);
|
db.run(`CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);`);
|
||||||
|
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS build_profiles (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
profile_json TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, name)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
db.run(`CREATE INDEX IF NOT EXISTS idx_build_profiles_user_updated ON build_profiles(user_id, updated_at DESC);`);
|
||||||
|
|
||||||
db.run(`
|
db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS notification_screenshots (
|
CREATE TABLE IF NOT EXISTS notification_screenshots (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -1460,6 +1472,57 @@ export function getClientBookmark(id: string): boolean {
|
|||||||
return row?.bookmarked === 1;
|
return row?.bookmarked === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BuildProfileRecord {
|
||||||
|
userId: number;
|
||||||
|
name: string;
|
||||||
|
profileJson: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listBuildProfilesForUser(userId: number): BuildProfileRecord[] {
|
||||||
|
return db
|
||||||
|
.query<any>(
|
||||||
|
`SELECT user_id, name, profile_json, created_at, updated_at
|
||||||
|
FROM build_profiles
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY updated_at DESC, name ASC`,
|
||||||
|
)
|
||||||
|
.all(userId)
|
||||||
|
.map((row: any) => ({
|
||||||
|
userId: row.user_id,
|
||||||
|
name: row.name,
|
||||||
|
profileJson: row.profile_json,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveBuildProfileForUser(userId: number, name: string, profileJson: string): void {
|
||||||
|
const now = Date.now();
|
||||||
|
db.run(
|
||||||
|
`INSERT INTO build_profiles (user_id, name, profile_json, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(user_id, name) DO UPDATE SET
|
||||||
|
profile_json = excluded.profile_json,
|
||||||
|
updated_at = excluded.updated_at`,
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
profileJson,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteBuildProfileForUser(userId: number, name: string): boolean {
|
||||||
|
const result = db.run(
|
||||||
|
`DELETE FROM build_profiles WHERE user_id = ? AND name = ?`,
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
return ((result as any)?.changes || 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BuildRecord {
|
export interface BuildRecord {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import { authenticateRequest } from "../../auth";
|
|||||||
import { AuditAction, logAudit } from "../../auditLog";
|
import { AuditAction, logAudit } from "../../auditLog";
|
||||||
import * as buildManager from "../../build/buildManager";
|
import * as buildManager from "../../build/buildManager";
|
||||||
import * as clientManager from "../../clientManager";
|
import * as clientManager from "../../clientManager";
|
||||||
import { deleteBuild, getAllBuilds, getBuild, countBuildsForUser, getOldestBuildForUser } from "../../db";
|
import {
|
||||||
|
deleteBuild,
|
||||||
|
getAllBuilds,
|
||||||
|
getBuild,
|
||||||
|
countBuildsForUser,
|
||||||
|
getOldestBuildForUser,
|
||||||
|
listBuildProfilesForUser,
|
||||||
|
saveBuildProfileForUser,
|
||||||
|
deleteBuildProfileForUser,
|
||||||
|
} from "../../db";
|
||||||
import { encodeMessage } from "../../protocol";
|
import { encodeMessage } from "../../protocol";
|
||||||
import { metrics } from "../../metrics";
|
import { metrics } from "../../metrics";
|
||||||
import { requirePermission } from "../../rbac";
|
import { requirePermission } from "../../rbac";
|
||||||
@@ -380,6 +389,90 @@ export async function handleBuildRoutes(
|
|||||||
return Response.json({ builds });
|
return Response.json({ builds });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && url.pathname === "/api/build/profiles") {
|
||||||
|
requirePermission(user, "clients:build");
|
||||||
|
|
||||||
|
const profiles = listBuildProfilesForUser(user.userId).map((p) => {
|
||||||
|
let parsedConfig: any = {};
|
||||||
|
try {
|
||||||
|
parsedConfig = JSON.parse(p.profileJson);
|
||||||
|
} catch {
|
||||||
|
parsedConfig = {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: p.name,
|
||||||
|
config: parsedConfig,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
updatedAt: p.updatedAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Response.json({ profiles });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && url.pathname === "/api/build/profiles") {
|
||||||
|
requirePermission(user, "clients:build");
|
||||||
|
|
||||||
|
const body = await req.json();
|
||||||
|
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||||
|
const config = body?.config;
|
||||||
|
|
||||||
|
if (!name || name.length > 64 || !/^[A-Za-z0-9 _.-]+$/.test(name)) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Invalid profile name (1-64 chars, letters/numbers/space/._-)" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
||||||
|
return Response.json({ error: "Invalid profile config" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_PROFILES_PER_USER = 50;
|
||||||
|
const existingProfiles = listBuildProfilesForUser(user.userId);
|
||||||
|
const hasExistingWithSameName = existingProfiles.some((p) => p.name === name);
|
||||||
|
if (!hasExistingWithSameName && existingProfiles.length >= MAX_PROFILES_PER_USER) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: `Profile limit reached (${MAX_PROFILES_PER_USER} max)` },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let profileJson = "{}";
|
||||||
|
try {
|
||||||
|
profileJson = JSON.stringify(config);
|
||||||
|
} catch {
|
||||||
|
return Response.json({ error: "Profile config must be JSON serializable" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileJson.length > 512 * 1024) {
|
||||||
|
return Response.json({ error: "Profile config too large (max 512KB)" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBuildProfileForUser(user.userId, name, profileJson);
|
||||||
|
return Response.json({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/profiles\/.+$/)) {
|
||||||
|
requirePermission(user, "clients:build");
|
||||||
|
|
||||||
|
const encodedName = url.pathname.split("/api/build/profiles/")[1] || "";
|
||||||
|
let name = "";
|
||||||
|
try {
|
||||||
|
name = decodeURIComponent(encodedName).trim();
|
||||||
|
} catch {
|
||||||
|
return Response.json({ error: "Invalid profile name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
return Response.json({ error: "Invalid profile name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = deleteBuildProfileForUser(user.userId, name);
|
||||||
|
if (!deleted) {
|
||||||
|
return Response.json({ error: "Profile not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
return Response.json({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/(.+)\/delete$/)) {
|
if (req.method === "DELETE" && url.pathname.match(/^\/api\/build\/(.+)\/delete$/)) {
|
||||||
requirePermission(user, "clients:build");
|
requirePermission(user, "clients:build");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user