feat: optional auth + backup/restore reliability fixes
Some checks failed
Lint & Test / test (push) Failing after 29s
Some checks failed
Lint & Test / test (push) Failing after 29s
Auth is now optional: when `auth.api_keys` is empty, all endpoints are open (no login screen, no Bearer tokens). Health endpoint reports `auth_required` so the frontend knows which mode to use. Backup/restore fixes: - Auto-backup uses atomic writes (was `write_text`, risked corruption) - Startup backup skipped if recent backup exists (<5 min cooldown), preventing rapid restarts from rotating out good backups - Restore rejects all-empty backups to prevent accidental data wipes - Store saves frozen after restore to prevent stale in-memory data from overwriting freshly-restored files before restart completes - Missing stores during restore logged as warnings - STORE_MAP completeness verified at startup against StorageConfig
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
// Layer 0: state
|
||||
import { apiKey, setApiKey, refreshInterval } from './core/state.ts';
|
||||
import { apiKey, setApiKey, authRequired, refreshInterval } from './core/state.ts';
|
||||
import { Modal } from './core/modal.ts';
|
||||
import { queryEl } from './core/dom-utils.ts';
|
||||
|
||||
@@ -180,6 +180,9 @@ import {
|
||||
import { switchTab, initTabs, startAutoRefresh, handlePopState } from './features/tabs.ts';
|
||||
import { navigateToCard } from './core/navigation.ts';
|
||||
import { openCommandPalette, closeCommandPalette, initCommandPalette } from './core/command-palette.ts';
|
||||
import {
|
||||
applyStylePreset, applyBgEffect, renderAppearanceTab, initAppearance,
|
||||
} from './features/appearance.ts';
|
||||
import {
|
||||
openSettingsModal, closeSettingsModal, switchSettingsTab,
|
||||
downloadBackup, handleRestoreFileSelected,
|
||||
@@ -548,6 +551,11 @@ Object.assign(window, {
|
||||
setLogLevel,
|
||||
saveExternalUrl,
|
||||
getBaseOrigin,
|
||||
|
||||
// appearance
|
||||
applyStylePreset,
|
||||
applyBgEffect,
|
||||
renderAppearanceTab,
|
||||
});
|
||||
|
||||
// ─── Global keyboard shortcuts ───
|
||||
@@ -626,6 +634,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize visual effects
|
||||
initCardGlare();
|
||||
initBgAnim();
|
||||
initAppearance();
|
||||
initTabIndicator();
|
||||
updateBgAnimTheme(document.documentElement.getAttribute('data-theme') !== 'light');
|
||||
const accent = localStorage.getItem('accentColor') || '#4CAF50';
|
||||
@@ -659,11 +668,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (addDeviceForm) addDeviceForm.addEventListener('submit', handleAddDevice);
|
||||
|
||||
// Always monitor server connection (even before login)
|
||||
loadServerInfo();
|
||||
await loadServerInfo();
|
||||
startConnectionMonitor();
|
||||
|
||||
// Show modal if no API key is stored
|
||||
if (!apiKey) {
|
||||
// Expose auth state for inline scripts (after loadServerInfo sets it)
|
||||
(window as any)._authRequired = authRequired;
|
||||
if (typeof window.updateAuthUI === 'function') window.updateAuthUI();
|
||||
|
||||
// Show login modal only when auth is enabled and no API key is stored
|
||||
if (authRequired && !apiKey) {
|
||||
setTimeout(() => {
|
||||
if (typeof window.showApiKeyModal === 'function') {
|
||||
window.showApiKeyModal(null, true);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* API utilities — base URL, auth headers, fetch wrapper, helpers.
|
||||
*/
|
||||
|
||||
import { apiKey, setApiKey, refreshInterval, setRefreshInterval, displaysCache } from './state.ts';
|
||||
import { apiKey, setApiKey, authRequired, setAuthRequired, refreshInterval, setRefreshInterval, displaysCache } from './state.ts';
|
||||
import { t } from './i18n.ts';
|
||||
import { showToast } from './ui.ts';
|
||||
import { getEl, queryEl } from './dom-utils.ts';
|
||||
@@ -137,6 +137,7 @@ export function isGameSenseDevice(type: string) {
|
||||
}
|
||||
|
||||
export function handle401Error() {
|
||||
if (!authRequired) return; // Auth disabled — ignore 401s
|
||||
if (!apiKey) return; // Already handled or no session
|
||||
localStorage.removeItem('wled_api_key');
|
||||
setApiKey(null);
|
||||
@@ -200,6 +201,11 @@ export async function loadServerInfo() {
|
||||
window.dispatchEvent(new CustomEvent('server:reconnected'));
|
||||
}
|
||||
|
||||
// Auth mode detection
|
||||
const authNeeded = data.auth_required !== false;
|
||||
setAuthRequired(authNeeded);
|
||||
(window as any)._authRequired = authNeeded;
|
||||
|
||||
// Demo mode detection
|
||||
if (data.demo_mode && !demoMode) {
|
||||
demoMode = true;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* server:device_health_changed — device online/offline status change
|
||||
*/
|
||||
|
||||
import { apiKey } from './state.ts';
|
||||
import { apiKey, authRequired } from './state.ts';
|
||||
|
||||
let _ws: WebSocket | null = null;
|
||||
let _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -19,10 +19,10 @@ const _RECONNECT_MAX = 30000;
|
||||
|
||||
export function startEventsWS() {
|
||||
stopEventsWS();
|
||||
if (!apiKey) return;
|
||||
if (authRequired && !apiKey) return;
|
||||
|
||||
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const url = `${wsProto}//${location.host}/api/v1/events/ws?token=${encodeURIComponent(apiKey)}`;
|
||||
const url = `${wsProto}//${location.host}/api/v1/events/ws?token=${encodeURIComponent(apiKey || '')}`;
|
||||
|
||||
try {
|
||||
_ws = new WebSocket(url);
|
||||
|
||||
@@ -18,6 +18,9 @@ import type {
|
||||
export let apiKey: string | null = null;
|
||||
export function setApiKey(v: string | null) { apiKey = v; }
|
||||
|
||||
export let authRequired = true;
|
||||
export function setAuthRequired(v: boolean) { authRequired = v; }
|
||||
|
||||
export let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
||||
export function setRefreshInterval(v: ReturnType<typeof setInterval> | null) { refreshInterval = v; }
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Window {
|
||||
|
||||
// ─── Core / state ───
|
||||
setApiKey: (key: string | null) => void;
|
||||
_authRequired: boolean | undefined;
|
||||
|
||||
// ─── Visual effects (called from inline <script>) ───
|
||||
_updateBgAnimAccent: (accent: string) => void;
|
||||
@@ -372,6 +373,11 @@ interface Window {
|
||||
saveExternalUrl: (...args: any[]) => any;
|
||||
getBaseOrigin: (...args: any[]) => any;
|
||||
|
||||
// ─── Appearance ───
|
||||
applyStylePreset: (id: string) => void;
|
||||
applyBgEffect: (id: string) => void;
|
||||
renderAppearanceTab: () => void;
|
||||
|
||||
// ─── Overlay spinner internals ───
|
||||
overlaySpinnerTimer: ReturnType<typeof setInterval> | null;
|
||||
_overlayEscHandler: ((e: KeyboardEvent) => void) | null;
|
||||
|
||||
Reference in New Issue
Block a user