From 710433533478ccce64690cab56f314e60c5fe34a Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Thu, 4 Jun 2026 00:06:30 +0000 Subject: [PATCH] Upload app/webui/js/store.js --- app/webui/js/store.js | 155 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 app/webui/js/store.js diff --git a/app/webui/js/store.js b/app/webui/js/store.js new file mode 100644 index 0000000..d8785dd --- /dev/null +++ b/app/webui/js/store.js @@ -0,0 +1,155 @@ +/** + * Alpine.js global stores for the CRM frontend. + * + * Stores: + * - $store.auth: current user, JWT, isAuthenticated, login/logout helpers + * - $store.notifications: toast queue (success/error/info/warning) + * + * Pages access these as $store.auth.user, $store.notifications.success(msg), etc. + * + * IMPORTANT: This file is loaded as a module AFTER Alpine's CDN script. The + * `alpine:init` event fires once Alpine is ready, at which point we register + * the stores. Until then, $store.auth etc. are undefined. + * + * R-5: never use x-html. All user-derived strings go through x-text or + * interpolation in templates. + */ + +import { api, setToken, clearToken, getToken } from './api.js'; +import { login as authLogin, register as authRegister, + logout as authLogout, refreshToken as authRefresh, + fetchCurrentUser as authFetchMe } from './auth.js'; + +document.addEventListener('alpine:init', () => { + // === AUTH STORE ========================================================= + Alpine.store('auth', { + token: getToken(), + user: null, + isAuthenticated: !!getToken(), + loading: false, + error: null, + + /** Called from x-init on protected pages */ + async init() { + if (!this.isAuthenticated) { + // Auth-gate: redirect to login + window.location.href = '/index.html'; + return; + } + await this.fetchUser(); + }, + + async fetchUser() { + this.loading = true; + this.error = null; + try { + this.user = await authFetchMe(); + } catch (err) { + // 401 is already handled by api.js (token cleared, redirect). + // For other errors, store them and log out. + this.error = err.message || 'Failed to load user'; + await this.logout(); + } finally { + this.loading = false; + } + }, + + async login(email, password) { + this.loading = true; + this.error = null; + try { + await authLogin(email, password); + this.token = getToken(); + this.isAuthenticated = true; + await this.fetchUser(); + } catch (err) { + this.error = err.message || 'Login failed'; + throw err; + } finally { + this.loading = false; + } + }, + + async register(payload) { + this.loading = true; + this.error = null; + try { + await authRegister(payload); + this.token = getToken(); + this.isAuthenticated = true; + await this.fetchUser(); + } catch (err) { + this.error = err.message || 'Registration failed'; + throw err; + } finally { + this.loading = false; + } + }, + + async logout() { + try { + await authLogout(); + } catch { + // best-effort + } finally { + this.token = null; + this.user = null; + this.isAuthenticated = false; + clearToken(); + } + }, + + async refresh() { + try { + await authRefresh(); + this.token = getToken(); + } catch (err) { + this.error = err.message || 'Token refresh failed'; + await this.logout(); + } + }, + + /** Convenience: is the current user an admin? */ + get isAdmin() { + return this.user && this.user.role === 'admin'; + }, + }); + + // === NOTIFICATIONS STORE ================================================ + Alpine.store('notifications', { + items: [], + + /** + * Push a new toast. + * @param {string} message + * @param {'info'|'success'|'error'|'warning'} [type='info'] + * @param {number} [timeout=5000] ms before auto-dismiss (0 = no auto-dismiss) + */ + push(message, type = 'info', timeout = 5000) { + const id = Date.now() + '-' + Math.random().toString(36).slice(2, 8); + this.items.push({ id, message: String(message), type }); + if (timeout > 0) { + setTimeout(() => this.dismiss(id), timeout); + } + return id; + }, + + dismiss(id) { + this.items = this.items.filter(n => n.id !== id); + }, + + success(msg, timeout) { return this.push(msg, 'success', timeout); }, + error(msg, timeout) { return this.push(msg, 'error', timeout); }, + info(msg, timeout) { return this.push(msg, 'info', timeout); }, + warning(msg, timeout) { return this.push(msg, 'warning', timeout); }, + }); +}); + +// Expose a tiny global helper so non-module code (e.g. inline page handlers) +// can call notifications without an explicit import. +window.crmNotify = (msg, type) => { + if (window.Alpine && window.Alpine.store('notifications')) { + return window.Alpine.store('notifications').push(msg, type || 'info'); + } + console.warn('crmNotify called before Alpine was ready:', msg); +};