diff --git a/api/index.php b/api/index.php index 7c110b1..aef542c 100644 --- a/api/index.php +++ b/api/index.php @@ -191,6 +191,20 @@ $action = $_GET['action'] ?? ''; if (!defined('CRON_MODE')): try { + // DEMO_MODE guard + if (env('DEMO_MODE') === 'true') { + $demoBlocked = [ + 'save_settings', 'product_save', 'product_delete', 'product_merge', + 'inventory_add', 'inventory_use', 'inventory_update', 'inventory_remove', + 'dismiss_anomaly', 'bring_add', 'bring_remove', 'bring_sync', + ]; + if (in_array($action, $demoBlocked, true)) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'demo_mode']); + exit; + } + } + switch ($action) { // ===== PRODUCTS ===== case 'search_barcode': @@ -2037,9 +2051,10 @@ function getServerSettings(): void { $bringEmail = env('BRING_EMAIL'); echo json_encode([ - 'gemini_key' => $geminiKey, 'gemini_key_set' => !empty($geminiKey), 'bring_email' => $bringEmail, + 'settings_token_set' => !empty(env('SETTINGS_TOKEN')), + 'demo_mode' => env('DEMO_MODE') === 'true', 'bring_password_set' => !empty(env('BRING_PASSWORD')), 'tts_url' => env('TTS_URL'), 'tts_token' => env('TTS_TOKEN'), @@ -2066,6 +2081,17 @@ function getServerSettings(): void { } function saveSettings(): void { + // Require SETTINGS_TOKEN if configured + $requiredToken = env('SETTINGS_TOKEN'); + if (!empty($requiredToken)) { + $provided = $_SERVER['HTTP_X_SETTINGS_TOKEN'] ?? ''; + if (!hash_equals($requiredToken, $provided)) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'unauthorized']); + return; + } + } + $input = json_decode(file_get_contents('php://input'), true); $envFile = __DIR__ . '/../.env'; $envVars = loadEnv(); diff --git a/assets/js/app.js b/assets/js/app.js index ab14443..5e21cc2 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -84,6 +84,7 @@ const _loadedVersion = (document.querySelector('.header-version')?.textContent?. // Set to true in _initApp / syncSettingsFromDB once server confirms key is set. // All AI entry points call _requireGemini() before opening camera / API calls. let _geminiAvailable = false; +let _demoMode = false; function _requireGemini() { if (_geminiAvailable) return true; @@ -1808,7 +1809,8 @@ async function syncSettingsFromDB() { try { // Primary: load from server .env const serverSettings = await api('get_settings'); - _geminiAvailable = !!(serverSettings.gemini_key_set || serverSettings.gemini_key); + _geminiAvailable = !!(serverSettings.gemini_key_set); + _demoMode = !!serverSettings.demo_mode; _updateGeminiButtonState(); const s = getSettings(); const serverKeys = ['default_persons','pref_veloce','pref_pocafame','pref_scadenze', @@ -1915,13 +1917,17 @@ async function loadSettingsUI() { try { const serverSettings = await api('get_settings'); // Merge all server settings into local cache (server wins) - const serverKeys = ['gemini_key','bring_email','bring_password', + const serverKeys = ['bring_email', 'default_persons','pref_veloce','pref_pocafame','pref_scadenze', 'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances', 'camera_facing','scale_enabled','scale_gateway_url', 'meal_plan_enabled', 'tts_enabled','tts_url','tts_token','tts_method','tts_auth_type', 'tts_content_type','tts_payload_key']; + // Note: gemini_key is never sent from server; settings_token_set is metadata only + const settingsTokenRequired = !!serverSettings.settings_token_set; + const tokenHintEl = document.getElementById('settings-token-status-hint'); + if (tokenHintEl) tokenHintEl.style.display = settingsTokenRequired ? 'block' : 'none'; let changed = false; for (const key of serverKeys) { if (serverSettings[key] !== undefined && serverSettings[key] !== null && serverSettings[key] !== '') { @@ -2141,6 +2147,8 @@ async function saveSettings() { // Save ALL settings to server .env try { + const settingsToken = document.getElementById('setting-settings-token')?.value.trim() || ''; + const tokenHeader = settingsToken ? { 'X-Settings-Token': settingsToken } : {}; const result = await api('save_settings', {}, 'POST', { gemini_key: s.gemini_key, bring_email: s.bring_email, @@ -2165,14 +2173,17 @@ async function saveSettings() { tts_auth_type: s.tts_auth_type, tts_content_type: s.tts_content_type, tts_payload_key: s.tts_payload_key, - }); + }, tokenHeader); const statusEl = document.getElementById('settings-status'); if (result.success) { statusEl.className = 'settings-status success'; statusEl.textContent = `โœ… ${t('settings.saved')}`; } else { statusEl.className = 'settings-status error'; - statusEl.textContent = `โš ๏ธ ${t('settings.saved_local_error').replace('{error}', result.error || '')}`; + const errMsg = result.error === 'unauthorized' + ? '๐Ÿ”’ Token non valido o mancante' + : `โš ๏ธ ${t('settings.saved_local_error').replace('{error}', result.error || '')}`; + statusEl.textContent = errMsg; } statusEl.style.display = 'block'; setTimeout(() => statusEl.style.display = 'none', 4000); @@ -2198,7 +2209,7 @@ function togglePasswordVisibility(inputId) { } // ===== API HELPER ===== -async function api(action, params = {}, method = 'GET', body = null) { +async function api(action, params = {}, method = 'GET', body = null, extraHeaders = {}) { let url = `${API_BASE}?action=${action}`; if (method === 'GET') { Object.entries(params).forEach(([k, v]) => { @@ -2207,8 +2218,10 @@ async function api(action, params = {}, method = 'GET', body = null) { } const opts = { method }; if (body) { - opts.headers = { 'Content-Type': 'application/json' }; + opts.headers = { 'Content-Type': 'application/json', ...extraHeaders }; opts.body = JSON.stringify(body); + } else if (Object.keys(extraHeaders).length > 0) { + opts.headers = { ...extraHeaders }; } const res = await fetch(url, opts); if (!res.ok) { @@ -11706,7 +11719,7 @@ function _getMissingSetupSteps(serverSettings) { // Steps 1 & 2 only show on first run (before setup is completed/skipped) if (!setupDone) { // Step 1 โ€” Gemini API key (check both localStorage and server .env) - if (!s.gemini_key && !srv.gemini_key && !srv.gemini_key_set) missing.push(1); + if (!s.gemini_key && !srv.gemini_key_set) missing.push(1); // Step 2 โ€” Bring! credentials (check both localStorage and server .env) if ((!s.bring_email && !srv.bring_email) || (!s.bring_password && !srv.bring_password_set)) missing.push(2); } @@ -11910,7 +11923,8 @@ async function _initApp() { // are taken into account before deciding which wizard steps to show. let serverSettings = {}; try { serverSettings = await api('get_settings'); } catch(e) {} - _geminiAvailable = !!(serverSettings.gemini_key_set || serverSettings.gemini_key); + _geminiAvailable = !!(serverSettings.gemini_key_set); + _demoMode = !!serverSettings.demo_mode; _updateGeminiButtonState(); const missing = _getMissingSetupSteps(serverSettings); if (missing.length > 0) { diff --git a/index.html b/index.html index d96d053..fb8bec4 100644 --- a/index.html +++ b/index.html @@ -870,6 +870,16 @@
+
+

๐Ÿ”‘ Token Impostazioni

+

Se SETTINGS_TOKEN รจ configurato nel .env server, inserisci qui il token prima di salvare le impostazioni. Lascia vuoto se non configurato.

+
+ + + +
+ +

๐Ÿ”’ Certificato HTTPS

Se il browser mostra l'errore "La connessione non รจ privata" (ERR_CERT_AUTHORITY_INVALID), devi installare il certificato CA nel dispositivo.