diff --git a/src/app_logic.js b/src/app_logic.js index 22d0eee..a97d717 100644 --- a/src/app_logic.js +++ b/src/app_logic.js @@ -8,7 +8,7 @@ function tableScroll(query) { //var secretTokenEl = document.getElementById("secretToken"); var container = document.getElementById('container'); -function open_page(params) { +function open_page(params, retryAfterExternalLoad = false) { // Clear stored event listeners and timers EventListeners.GunJS = []; EventListeners.Timeout.forEach((ev) => clearTimeout(ev)); @@ -31,6 +31,22 @@ function open_page(params) { } var path = params.split(','); var app = path[0]; + if (!PAGES[app]) { + if (!retryAfterExternalLoad && typeof TS_loadExternalAppsFromDB === 'function') { + TS_loadExternalAppsFromDB().then(() => { + open_page(params, true); + }); + return; + } + toastr.error('La app solicitada no existe.'); + setUrlHash('index'); + return; + } + if (typeof TS_isAppInstalled === 'function' && !TS_isAppInstalled(app)) { + toastr.error('Esta app no está instalada. Instálala desde la Tienda de apps.'); + setUrlHash('tienda_apps'); + return; + } if (path[1] == undefined) { PAGES[app].index(); return; diff --git a/src/app_modules.js b/src/app_modules.js index ccc543d..680ef20 100644 --- a/src/app_modules.js +++ b/src/app_modules.js @@ -1725,6 +1725,531 @@ var PAGES = {}; var PERMS = { ADMIN: 'Administrador', }; + +var TS_INSTALLED_APPS_CACHE = null; +var TS_INSTALLED_APPS_CACHE_KEY = ''; +var TS_INSTALLED_APPS_LOADING = false; +var TS_ESSENTIAL_APPS = new Set(['index', 'personas', 'dataman']); +var TS_LOCKED_APPS = new Set(['index', 'personas', 'dataman']); + +function TS_getAppInstallStorageKey() { + var dbName = 'telesec'; + try { + dbName = typeof getDBName === 'function' ? getDBName() : 'telesec'; + } catch (e) { + dbName = 'telesec'; + } + var personaId = SUB_LOGGED_IN_ID || 'anon'; + return 'TELESEC_INSTALLED_APPS::' + dbName + '::' + personaId; +} + +function TS_getAppInstallDocId() { + var dbName = 'telesec'; + try { + dbName = typeof getDBName === 'function' ? getDBName() : 'telesec'; + } catch (e) { + dbName = 'telesec'; + } + var personaId = SUB_LOGGED_IN_ID || 'anon'; + return 'installed_apps::' + dbName + '::' + personaId; +} + +function TS_buildDefaultInstalledSet() { + var installedSet = new Set(); + TS_ESSENTIAL_APPS.forEach((key) => { + if (!PAGES[key]) return; + if (TS_isSystemApp(key)) return; + if (PAGES[key].ExternalApp === true) return; + if (PAGES[key].Esconder === true) return; + installedSet.add(key); + }); + TS_LOCKED_APPS.forEach((key) => { + if (PAGES[key] && PAGES[key].Esconder !== true) { + installedSet.add(key); + } + }); + return installedSet; +} + +function TS_parseInstalledAppsPayload(payload) { + if (!payload || !Array.isArray(payload.apps)) return null; + var set = new Set(); + payload.apps.forEach((appKey) => { + if (typeof appKey === 'string' && appKey.trim() !== '') { + set.add(appKey); + } + }); + return set; +} + +function TS_readInstalledAppsFallback() { + try { + var raw = localStorage.getItem(TS_getAppInstallStorageKey()); + if (!raw) return null; + var parsed = JSON.parse(raw); + return TS_parseInstalledAppsPayload(parsed); + } catch (e) { + return null; + } +} + +function TS_writeInstalledAppsFallback(appSet) { + var apps = []; + if (appSet && typeof appSet.forEach === 'function') { + appSet.forEach((appKey) => { + if (typeof appKey === 'string' && appKey.trim() !== '') { + apps.push(appKey); + } + }); + } + localStorage.setItem( + TS_getAppInstallStorageKey(), + JSON.stringify({ version: 1, apps: apps.sort(), updatedAt: CurrentISOTime() }) + ); +} + +function TS_setInstalledAppsCache(appSet) { + TS_LOCKED_APPS.forEach((key) => { + if (PAGES[key] && PAGES[key].Esconder !== true) { + appSet.add(key); + } + }); + TS_INSTALLED_APPS_CACHE = appSet; + TS_INSTALLED_APPS_CACHE_KEY = TS_getAppInstallDocId(); + TS_writeInstalledAppsFallback(appSet); +} + +function TS_persistInstalledAppsToDB(appSet) { + if (!window.DB || typeof DB.put !== 'function') { + return Promise.resolve(false); + } + var apps = []; + appSet.forEach((appKey) => { + if (typeof appKey === 'string' && appKey.trim() !== '') { + apps.push(appKey); + } + }); + return DB.put('config', TS_getAppInstallDocId(), { + version: 1, + apps: apps.sort(), + updatedAt: CurrentISOTime(), + }) + .then(() => true) + .catch((e) => { + console.warn('Error saving installed apps in DB', e); + return false; + }); +} + +function TS_loadInstalledAppsFromDB(forceReload = false) { + var expectedKey = TS_getAppInstallDocId(); + if (!forceReload && TS_INSTALLED_APPS_CACHE && TS_INSTALLED_APPS_CACHE_KEY === expectedKey) { + return Promise.resolve(TS_INSTALLED_APPS_CACHE); + } + if (TS_INSTALLED_APPS_LOADING) { + return Promise.resolve(TS_INSTALLED_APPS_CACHE); + } + TS_INSTALLED_APPS_LOADING = true; + + var fallbackSet = TS_readInstalledAppsFallback() || TS_buildDefaultInstalledSet(); + TS_setInstalledAppsCache(fallbackSet); + + if (!window.DB || typeof DB.get !== 'function') { + TS_INSTALLED_APPS_LOADING = false; + return Promise.resolve(TS_INSTALLED_APPS_CACHE); + } + + return DB.get('config', expectedKey) + .then((raw) => { + if (raw == null) { + TS_INSTALLED_APPS_LOADING = false; + return TS_persistInstalledAppsToDB(TS_INSTALLED_APPS_CACHE).then(() => TS_INSTALLED_APPS_CACHE); + } + + return new Promise((resolve) => { + if (typeof raw === 'string') { + TS_decrypt( + raw, + SECRET, + (decrypted) => { + var parsed = TS_parseInstalledAppsPayload(decrypted); + if (parsed) { + TS_setInstalledAppsCache(parsed); + } + TS_INSTALLED_APPS_LOADING = false; + resolve(TS_INSTALLED_APPS_CACHE); + }, + 'config', + expectedKey + ); + } else { + var parsed = TS_parseInstalledAppsPayload(raw); + if (parsed) { + TS_setInstalledAppsCache(parsed); + } + TS_INSTALLED_APPS_LOADING = false; + resolve(TS_INSTALLED_APPS_CACHE); + } + }); + }) + .catch((e) => { + console.warn('Error loading installed apps from DB', e); + TS_INSTALLED_APPS_LOADING = false; + return TS_INSTALLED_APPS_CACHE; + }); +} + +function TS_getInstalledAppsSet() { + var expectedKey = TS_getAppInstallDocId(); + if (!TS_INSTALLED_APPS_CACHE || TS_INSTALLED_APPS_CACHE_KEY !== expectedKey) { + TS_loadInstalledAppsFromDB().then(() => { + SetPages(); + }); + return null; + } + return TS_INSTALLED_APPS_CACHE; +} + +function TS_setInstalledAppsSet(appSet) { + TS_setInstalledAppsCache(appSet); + TS_persistInstalledAppsToDB(appSet); +} + +function TS_isSystemApp(appKey) { + if (appKey === 'login') return true; + if (!PAGES[appKey]) return false; + return PAGES[appKey].SystemApp === true; +} + +function TS_isLockedApp(appKey) { + return TS_LOCKED_APPS.has(appKey); +} + +function TS_isMandatoryApp(appKey) { + return TS_isSystemApp(appKey) || TS_isLockedApp(appKey); +} + +function TS_isAppInstalled(appKey) { + if (TS_isMandatoryApp(appKey)) return true; + var installedSet = TS_getInstalledAppsSet(); + if (installedSet == null) { + return true; + } + return installedSet.has(appKey); +} + +function TS_installApp(appKey) { + if (!PAGES[appKey] || TS_isSystemApp(appKey)) return; + var installedSet = TS_getInstalledAppsSet(); + if (installedSet == null) { + installedSet = TS_buildDefaultInstalledSet(); + } + installedSet.add(appKey); + if (appKey === 'supercafe' && PAGES.pagos) { + installedSet.add('pagos'); + } + TS_setInstalledAppsSet(installedSet); +} + +function TS_uninstallApp(appKey) { + if (!PAGES[appKey] || TS_isMandatoryApp(appKey)) return; + var installedSet = TS_getInstalledAppsSet(); + if (installedSet == null) { + installedSet = TS_buildDefaultInstalledSet(); + } + installedSet.delete(appKey); + TS_setInstalledAppsSet(installedSet); +} + +function TS_resetInstalledApps() { + var defaults = TS_buildDefaultInstalledSet(); + TS_setInstalledAppsSet(defaults); +} + +function TS_getAppCatalog() { + return Object.keys(PAGES) + .filter((key) => PAGES[key].Esconder !== true) + .map((key) => { + return { + key: key, + title: PAGES[key].Title || key, + icon: PAGES[key].icon || 'static/appico/application_enterprise.png', + installed: TS_isAppInstalled(key), + system: TS_isSystemApp(key), + canAccess: !PAGES[key].AccessControl || checkRole(key), + requiresRole: PAGES[key].AccessControl === true, + }; + }) + .sort((a, b) => a.title.localeCompare(b.title, 'es')); +} + +var TS_EXTERNAL_APPS_CACHE = []; +var TS_EXTERNAL_APPS_CACHE_KEY = ''; +var TS_EXTERNAL_APPS_LOADING = false; +var TS_EXTERNAL_APPS_READY = false; + +function TS_getExternalAppsStorageKey() { + var dbName = 'telesec'; + try { + dbName = typeof getDBName === 'function' ? getDBName() : 'telesec'; + } catch (e) { + dbName = 'telesec'; + } + var personaId = SUB_LOGGED_IN_ID || 'anon'; + return 'TELESEC_EXTERNAL_APPS::' + dbName + '::' + personaId; +} + +function TS_getExternalAppsDocId() { + var dbName = 'telesec'; + try { + dbName = typeof getDBName === 'function' ? getDBName() : 'telesec'; + } catch (e) { + dbName = 'telesec'; + } + var personaId = SUB_LOGGED_IN_ID || 'anon'; + return 'external_apps::' + dbName + '::' + personaId; +} + +function TS_parseExternalAppsPayload(payload) { + if (!payload || !Array.isArray(payload.apps)) return []; + return payload.apps.filter((entry) => { + return ( + entry && + typeof entry.appKey === 'string' && + entry.appKey.trim() !== '' && + typeof entry.code === 'string' && + entry.code.trim() !== '' + ); + }); +} + +function TS_readExternalAppsFallback() { + try { + var raw = localStorage.getItem(TS_getExternalAppsStorageKey()); + if (!raw) return []; + return TS_parseExternalAppsPayload(JSON.parse(raw)); + } catch (e) { + return []; + } +} + +function TS_writeExternalAppsFallback(appsList) { + localStorage.setItem( + TS_getExternalAppsStorageKey(), + JSON.stringify({ + version: 1, + apps: appsList, + updatedAt: CurrentISOTime(), + }) + ); +} + +function TS_setExternalAppsCache(appsList) { + TS_EXTERNAL_APPS_CACHE = Array.isArray(appsList) ? appsList : []; + TS_EXTERNAL_APPS_CACHE_KEY = TS_getExternalAppsDocId(); + TS_EXTERNAL_APPS_READY = true; + TS_writeExternalAppsFallback(TS_EXTERNAL_APPS_CACHE); +} + +function TS_saveExternalAppsToDB(appsList) { + if (!window.DB || typeof DB.put !== 'function') { + return Promise.resolve(false); + } + return DB.put('config', TS_getExternalAppsDocId(), { + version: 1, + apps: appsList, + updatedAt: CurrentISOTime(), + }) + .then(() => true) + .catch((e) => { + console.warn('Error saving external apps in DB', e); + return false; + }); +} + +function TS_evalExternalAppCode(code, expectedAppKey = '') { + if (typeof code !== 'string' || code.trim() === '') { + throw new Error('Código vacío en app externa.'); + } + + var beforeKeys = Object.keys(PAGES); + var beforeSet = new Set(beforeKeys); + try { + new Function(code)(); + } catch (e) { + throw new Error('Error ejecutando app externa: ' + (e && e.message ? e.message : e)); + } + + var afterKeys = Object.keys(PAGES); + var newKeys = afterKeys.filter((key) => !beforeSet.has(key)); + var appKey = expectedAppKey || ''; + + if (!appKey) { + if (newKeys.length === 0) { + throw new Error('La app externa no registró ninguna entrada en PAGES.'); + } + appKey = newKeys[0]; + } + + if (!PAGES[appKey]) { + throw new Error('No se pudo detectar la app externa cargada.'); + } + + if (beforeSet.has(appKey) && PAGES[appKey].ExternalApp !== true) { + throw new Error('La app externa intenta sobrescribir una app interna: ' + appKey); + } + + PAGES[appKey].ExternalApp = true; + PAGES[appKey].SystemApp = false; + + return { + appKey: appKey, + title: PAGES[appKey].Title || appKey, + icon: PAGES[appKey].icon || 'static/appico/application_enterprise.png', + }; +} + +function TS_applyExternalAppsRegistry(appsList) { + appsList.forEach((entry) => { + try { + if (entry.appKey && PAGES[entry.appKey] && PAGES[entry.appKey].ExternalApp === true) { + return; + } + TS_evalExternalAppCode(entry.code, entry.appKey || ''); + } catch (e) { + console.warn('Error loading external app', entry && entry.appKey, e); + } + }); +} + +function TS_loadExternalAppsFromDB(forceReload = false) { + var expectedKey = TS_getExternalAppsDocId(); + if (!forceReload && TS_EXTERNAL_APPS_READY && TS_EXTERNAL_APPS_CACHE_KEY === expectedKey) { + return Promise.resolve(TS_EXTERNAL_APPS_CACHE); + } + if (TS_EXTERNAL_APPS_LOADING) { + return Promise.resolve(TS_EXTERNAL_APPS_CACHE); + } + TS_EXTERNAL_APPS_LOADING = true; + + var fallback = TS_readExternalAppsFallback(); + TS_setExternalAppsCache(fallback); + TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE); + + if (!window.DB || typeof DB.get !== 'function') { + TS_EXTERNAL_APPS_LOADING = false; + return Promise.resolve(TS_EXTERNAL_APPS_CACHE); + } + + return DB.get('config', expectedKey) + .then((raw) => { + if (raw == null) { + TS_EXTERNAL_APPS_LOADING = false; + return TS_saveExternalAppsToDB(TS_EXTERNAL_APPS_CACHE).then(() => TS_EXTERNAL_APPS_CACHE); + } + + return new Promise((resolve) => { + if (typeof raw === 'string') { + TS_decrypt( + raw, + SECRET, + (decrypted) => { + var parsed = TS_parseExternalAppsPayload(decrypted); + TS_setExternalAppsCache(parsed); + TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE); + TS_EXTERNAL_APPS_LOADING = false; + resolve(TS_EXTERNAL_APPS_CACHE); + }, + 'config', + expectedKey + ); + } else { + var parsed = TS_parseExternalAppsPayload(raw); + TS_setExternalAppsCache(parsed); + TS_applyExternalAppsRegistry(TS_EXTERNAL_APPS_CACHE); + TS_EXTERNAL_APPS_LOADING = false; + resolve(TS_EXTERNAL_APPS_CACHE); + } + }); + }) + .catch((e) => { + console.warn('Error loading external apps from DB', e); + TS_EXTERNAL_APPS_LOADING = false; + return TS_EXTERNAL_APPS_CACHE; + }); +} + +function TS_getExternalAppsCatalog() { + var expectedKey = TS_getExternalAppsDocId(); + if (!TS_EXTERNAL_APPS_READY || TS_EXTERNAL_APPS_CACHE_KEY !== expectedKey) { + TS_loadExternalAppsFromDB().then(() => { + SetPages(); + }); + return []; + } + return TS_EXTERNAL_APPS_CACHE.map((entry) => { + return { + appKey: entry.appKey, + title: entry.title || entry.appKey, + sourceType: entry.sourceType || 'file', + source: entry.source || '', + installedAt: entry.installedAt || '', + installed: TS_isAppInstalled(entry.appKey), + }; + }); +} + +function TS_installExternalAppFromCode(code, sourceType = 'file', sourceRef = '') { + return TS_loadExternalAppsFromDB().then(() => { + var info = TS_evalExternalAppCode(code, ''); + var next = TS_EXTERNAL_APPS_CACHE.filter((entry) => entry.appKey !== info.appKey); + var item = { + id: safeuuid('extapp-'), + appKey: info.appKey, + title: info.title, + icon: info.icon, + sourceType: sourceType, + source: sourceRef, + code: code, + installedAt: CurrentISOTime(), + updatedAt: CurrentISOTime(), + }; + next.push(item); + TS_setExternalAppsCache(next); + TS_saveExternalAppsToDB(next); + TS_installApp(info.appKey); + return item; + }); +} + +function TS_uninstallExternalApp(appKey) { + return TS_loadExternalAppsFromDB().then(() => { + var next = TS_EXTERNAL_APPS_CACHE.filter((entry) => entry.appKey !== appKey); + TS_setExternalAppsCache(next); + TS_saveExternalAppsToDB(next); + TS_uninstallApp(appKey); + if (PAGES[appKey] && PAGES[appKey].ExternalApp === true) { + delete PAGES[appKey]; + } + return true; + }); +} + +function TS_resetAppsToDefault() { + return TS_loadExternalAppsFromDB().then(() => { + TS_EXTERNAL_APPS_CACHE.forEach((entry) => { + var appKey = entry.appKey; + if (PAGES[appKey] && PAGES[appKey].ExternalApp === true) { + delete PAGES[appKey]; + } + }); + TS_setExternalAppsCache([]); + TS_saveExternalAppsToDB([]); + TS_resetInstalledApps(); + return true; + }); +} + function checkRole(role) { var roles = SUB_LOGGED_IN_DETAILS.Roles || ''; var rolesArr = roles.split(','); @@ -1735,15 +2260,24 @@ function checkRole(role) { } } function SetPages() { + var expectedExternalKey = TS_getExternalAppsDocId(); + if (!TS_EXTERNAL_APPS_READY || TS_EXTERNAL_APPS_CACHE_KEY !== expectedExternalKey) { + TS_loadExternalAppsFromDB().then(() => { + SetPages(); + }); + } document.getElementById('appendApps2').innerHTML = ''; Object.keys(PAGES).forEach((key) => { if (PAGES[key].Esconder == true) { return; } + if (!TS_isAppInstalled(key)) { + return; + } if (PAGES[key].AccessControl == true) { var roles = SUB_LOGGED_IN_DETAILS.Roles || ''; var rolesArr = roles.split(','); - if (rolesArr.includes('ADMIN') || rolesArr.includes(key) || AC_BYPASS) { + if (rolesArr.includes('ADMIN') || rolesArr.includes(PAGES[key].AccessControlRole || key) || AC_BYPASS) { } else { return; } diff --git a/src/index.html b/src/index.html index eac39b6..5df1051 100644 --- a/src/index.html +++ b/src/index.html @@ -94,6 +94,7 @@ + \ No newline at end of file diff --git a/src/page/tienda_apps.js b/src/page/tienda_apps.js new file mode 100644 index 0000000..45a1d31 --- /dev/null +++ b/src/page/tienda_apps.js @@ -0,0 +1,176 @@ +PAGES.tienda_apps = { + Title: 'Tienda de apps', + icon: 'static/appico/application_enterprise.png', + SystemApp: true, + AccessControl: true, + AccessControlRole: "admin", + index: function () { + if (!checkRole('admin')) { + setUrlHash('index'); + toastr.error('No tienes permiso para acceder a la tienda de apps.'); + return; + } + var appsContainerId = safeuuid(); + var externalContainerId = safeuuid(); + var fieldFileId = safeuuid(); + var btnFileInstallId = safeuuid(); + var btnResetId = safeuuid(); + + container.innerHTML = html` +

Tienda de apps

+

Instala o desinstala módulos personalizados para tu cuenta actual.

+ + +

Apps oficiales

+
+

Apps externas instaladas

+
+
+ Instalar app externa + + +
+ ⚠️ Solo instala apps de fuentes de confianza. +
+ `; + + var render = () => { + var catalog = TS_getAppCatalog().filter((app) => app.key !== 'index' && app.key !== 'tienda_apps'); + if (catalog.length === 0) { + document.getElementById(appsContainerId).innerHTML = 'No hay apps disponibles en el catálogo.'; + return; + } + + var htmlCards = catalog + .map((app) => { + var roleInfo = app.requiresRole + ? app.canAccess + ? 'Permiso: OK' + : 'Sin permiso de acceso' + : 'Permiso: no requerido'; + + var actionBtn = app.installed + ? TS_isMandatoryApp(app.key) + ? `` + : `` + : ``; + + return ` +
+ ${app.title} +
+ ${app.title} + ${app.title} + ${app.installed ? '✅ Instalada' : '⬜ No instalada'} +
+
+ ${actionBtn} + ${roleInfo} +
+
+ `; + }) + .join(''); + + var target = document.getElementById(appsContainerId); + target.innerHTML = htmlCards; + + target.querySelectorAll('button[data-action]').forEach((button) => { + button.onclick = () => { + var appKey = button.getAttribute('data-app'); + var action = button.getAttribute('data-action'); + if (action === 'install') { + TS_installApp(appKey); + toastr.success('App instalada: ' + appKey); + } else { + TS_uninstallApp(appKey); + toastr.info('App desinstalada: ' + appKey); + } + SetPages(); + render(); + }; + }); + + var external = TS_getExternalAppsCatalog(); + var externalTarget = document.getElementById(externalContainerId); + if (!external.length) { + externalTarget.innerHTML = 'No hay apps externas instaladas.'; + } else { + externalTarget.innerHTML = external + .map((entry) => { + return ` +
+ ${entry.title} +
Clave: ${entry.appKey}
+
Origen: ${entry.sourceType} ${entry.source ? '- ' + entry.source : ''}
+
+ +
+
+ `; + }) + .join(''); + + externalTarget.querySelectorAll('button[data-external-action]').forEach((button) => { + button.onclick = async () => { + var appKey = button.getAttribute('data-external-app'); + await TS_uninstallExternalApp(appKey); + toastr.info('App externa desinstalada: ' + appKey); + SetPages(); + render(); + }; + }); + } + }; + + document.getElementById(btnResetId).onclick = async () => { + await TS_resetAppsToDefault(); + SetPages(); + render(); + toastr.success('Apps reestablecidas al estado por defecto.'); + }; + + document.getElementById(btnFileInstallId).onclick = async () => { + var input = document.getElementById(fieldFileId); + if (!input.files || !input.files[0]) { + toastr.error('Selecciona un archivo .telejs'); + return; + } + var file = input.files[0]; + try { + var code = await new Promise((resolve, reject) => { + var reader = new FileReader(); + reader.onload = (ev) => resolve(ev.target.result || ''); + reader.onerror = (err) => reject(err); + reader.readAsText(file); + }); + var entry = await TS_installExternalAppFromCode(code, 'file', file.name || 'archivo.telejs'); + toastr.success('App externa instalada: ' + entry.appKey); + SetPages(); + render(); + } catch (e) { + toastr.error('Error instalando archivo: ' + (e && e.message ? e.message : e)); + } + }; + + TS_loadInstalledAppsFromDB().then(() => { + TS_loadExternalAppsFromDB().then(() => { + render(); + }); + }); + }, + edit: function () { + this.index(); + }, +};