Upload app/webui/js/store.js
This commit is contained in:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user