import {createStore} from 'vuex'; import router from './router'; import * as base64 from 'base-64'; import * as utf8 from 'utf8'; import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils"; import sharedStatePlugin from "@/shared-state-plugin"; import persistentStatePlugin from "@/persistent-state-plugin"; import {triggerRef} from "vue"; const store = createStore({ state: { keyIncrement: 0, events: [], loadedItems: [], itemCache: {}, loadedBoxes: [], toasts: [], tickets: [], users: [], groups: [], state_options: [], lastEvent: '37C3', lastUsed: {}, remember: false, user: { username: null, password: null, permissions: [], token: null, expiry: null, }, thumbnailCache: {}, fetchedData: { events: 0, items: 0, boxes: 0, tickets: 0, users: 0, groups: 0, states: 0, }, persistent_loaded: false, shared_loaded: false, afterInitHandlers: [], showAddBoxModal: false, }, getters: { route: state => router.currentRoute.value, getEventSlug: state => router.currentRoute.value.params.event ? router.currentRoute.value.params.event : state.lastEvent, getActiveView: state => router.currentRoute.value.name || 'items', getFilters: state => router.currentRoute.value.query, getBoxes: state => state.loadedBoxes, checkPermission: state => (event, perm) => state.user.permissions && (state.user.permissions.includes(`${event}:${perm}`) || state.user.permissions.includes(`*:${perm}`)), hasPermissions: state => state.user.permissions && state.user.permissions.length > 0, activeUser: state => state.user.username || 'anonymous', stateInfo: state => (slug) => { const obj = state.state_options.filter((s) => s.value === slug)[0]; if (obj) { return { color: ticketStateColorLookup(obj.value), icon: ticketStateIconLookup(obj.value), slug: obj.value, text: obj.text, } } else { return { color: 'danger', icon: 'exclamation', slug: slug, text: 'Unknown' } } }, layout: (state, getters) => { if (router.currentRoute.value.query.layout) return router.currentRoute.value.query.layout; if (getters.getActiveView === 'items') return 'cards'; if (getters.getActiveView === 'tickets') return 'tasks'; }, isLoggedIn(state) { return state.user && state.user.username !== null && state.user.token !== null; }, getThumbnail: (state) => (url) => { if (!url) return null; if (!(url in state.thumbnailCache)) return null; return state.thumbnailCache[url]; }, }, mutations: { updateLastUsed(state, diff) { state.lastUsed = {...state.lastUsed, ...diff}; }, updateLastEvent(state, slug) { state.lastEvent = slug; }, replaceEvents(state, events) { state.events = events; state.fetchedData = {...state.fetchedData, events: Date.now()}; }, replaceTicketStates(state, states) { state.state_options = states; state.fetchedData = {...state.fetchedData, states: Date.now()}; }, changeView(state, {view, slug}) { router.push({path: `/${slug}/${view}`}); }, replaceLoadedItems(state, newItems) { state.loadedItems = newItems; state.fetchedData = {...state.fetchedData, items: Date.now()}; // TODO: manage caching items for different events and search results correctly }, setItemCache(state, {slug, items}) { state.itemCache[slug] = items; }, replaceBoxes(state, loadedBoxes) { state.loadedBoxes = loadedBoxes; state.fetchedData = {...state.fetchedData, boxes: Date.now()}; }, updateItem(state, updatedItem) { const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0]; Object.assign(item, updatedItem); }, removeItem(state, item) { state.loadedItems = state.loadedItems.filter(it => it !== item); }, appendItem(state, item) { state.loadedItems.push(item); }, replaceTickets(state, tickets) { state.tickets = tickets; state.fetchedData = {...state.fetchedData, tickets: Date.now()}; }, updateTicket(state, updatedTicket) { const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; Object.assign(ticket, updatedTicket); //triggerRef(state.tickets); state.tickets = [...state.tickets]; }, replaceUsers(state, users) { state.users = users; state.fetchedData = {...state.fetchedData, users: Date.now()}; }, replaceGroups(state, groups) { state.groups = groups; state.fetchedData = {...state.fetchedData, groups: Date.now()}; }, openAddBoxModal(state) { state.showAddBoxModal = true; }, closeAddBoxModal(state) { state.showAddBoxModal = false; }, createToast(state, {title, message, color}) { var toast = {title, message, color, key: state.keyIncrement} state.toasts.push(toast); state.keyIncrement += 1; return toast; }, removeToast(state, key) { state.toasts = state.toasts.filter(toast => toast.key !== key); }, setRemember(state, remember) { state.remember = remember; }, setUser(state, user) { state.user.username = user; }, setPassword(state, password) { state.user.password = password; }, setPermissions(state, permissions) { state.user.permissions = permissions; }, setToken(state, {token, expiry}) { const user = {...state.user}; user.token = token; user.expiry = expiry; state.user = user; }, setUserInfo(state, user) { state.user = user; }, logout(state) { const user = {...state.user}; user.user = null; user.password = null; user.token = null; user.expiry = null; user.permissions = null; state.user = user; }, setThumbnail(state, {url, data}) { state.thumbnailCache[url] = data; }, }, actions: { async login({commit}, {username, password, remember}) { commit('setRemember', remember); try { const data = await fetch('/api/2/login/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: username, password: password}), credentials: 'omit' }).then(r => r.json()) if (data && data.token) { const {data: {permissions}} = await http.get('/2/self/', data.token); commit('setUserInfo', {...data, permissions, username, password}); return true; } else { return false; } } catch (e) { console.error(e); return false; } }, async reloadToken({commit, state, getters}) { try { if (state.user.username && state.user.password) { const data = await fetch('/api/2/login/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: state.user.username, password: state.user.password}), credentials: 'omit' }).then(r => r.json()).catch(e => console.error(e)) if (data && data.token) { commit('setToken', data); return true; } } } catch (e) { console.error(e); } //credentials failed, logout store.commit('logout'); }, //async verifyToken({commit, state}) { async afterLogin({dispatch, state}) { let promises = []; promises.push(dispatch('loadBoxes')); promises.push(dispatch('fetchTicketStates')); promises.push(dispatch('loadEventItems')); promises.push(dispatch('loadTickets')); if (!state.user.permissions) { promises.push(dispatch('loadUserInfo')); } await Promise.all(promises); }, async afterSharedInit({dispatch, state}) { const handlers = state.afterInitHandlers; state.afterInitHandlers = []; await Promise.all(handlers.map(h => h()).flat()); }, scheduleAfterInit({dispatch, state}, handler) { if (state.shared_loaded) { Promise.all(handler()).then(() => { }); } else { state.afterInitHandlers.push(handler); } }, async fetchImage({state}, url) { return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}}); }, async loadUserInfo({commit, state}) { const {data, success} = await http.get('/2/self/', state.user.token); commit('setPermissions', data.permissions); }, async loadEvents({commit, state}) { if (!state.user.token) return; if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/events/', state.user.token); if (data && success) commit('replaceEvents', data); }, async fetchTicketStates({commit, state}) { if (!state.user.token) return; if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/tickets/states/', state.user.token); if (data && success) commit('replaceTicketStates', data); }, changeEvent({dispatch, getters, commit}, eventName) { router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); dispatch('loadEventItems'); }, changeView({getters}, link) { router.push({path: `/${getters.getEventSlug}/${link.path}/`}); }, showBoxContent({getters}, box) { router.push({path: `/${getters.getEventSlug}/items/`, query: {box}}); }, async loadEventItems({commit, getters, state}) { if (!state.user.token) return; if (state.fetchedData.items > Date.now() - 1000 * 60 * 60 * 24) return; try { commit('replaceLoadedItems', []); const slug = getters.getEventSlug; if (slug in state.itemCache) { commit('replaceLoadedItems', state.itemCache[slug]); } const {data, success} = await http.get(`/2/${slug}/items/`, state.user.token); if (data && success) { commit('replaceLoadedItems', data); commit('setItemCache', {slug, items: data}); } } catch (e) { console.error("Error loading items"); } }, async searchEventItems({commit, getters, state}, query) { const foo = utf8.encode(query); const bar = base64.encode(foo); const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${bar}/`, state.user.token); if (data && success) commit('replaceLoadedItems', data); }, async loadBoxes({commit, state}) { if (!state.user.token) return; if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/boxes/', state.user.token); if (data && success) commit('replaceBoxes', data); }, async createBox({commit, dispatch, state}, box) { const {data, success} = await http.post('/2/boxes/', box, state.user.token); commit('replaceBoxes', data); dispatch('loadBoxes').then(() => { commit('closeAddBoxModal'); }); }, async deleteBox({commit, dispatch, state}, box_id) { await http.delete(`/2/boxes/${box_id}/`, state.user.token); dispatch('loadBoxes'); }, async updateItem({commit, getters, state}, item) { const { data, success } = await http.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token); commit('updateItem', data); }, async markItemReturned({commit, getters, state}, item) { await http.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true}, state.user.token); commit('removeItem', item); }, async deleteItem({commit, getters, state}, item) { await http.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token); commit('removeItem', item); }, async postItem({commit, getters, state}, item) { commit('updateLastUsed', {box: item.box, cid: item.cid}); const {data, success} = await http.post(`/2/${getters.getEventSlug}/item/`, item, state.user.token); commit('appendItem', data); }, async loadTickets({commit, state}) { if (!state.user.token) return; if (state.fetchedData.tickets > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/tickets/', state.user.token); if (data && success) commit('replaceTickets', data); }, async sendMail({commit, dispatch, state}, {id, message}) { const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token); if (data && success) { state.fetchedData.tickets = 0; await dispatch('loadTickets'); } }, async postManualTicket({commit, dispatch, state}, {sender, message, title,}) { const {data, success} = await http.post(`/2/tickets/manual/`, { name: title, sender, body: message, recipient: 'mail@c3lf.de' }, state.user.token); await dispatch('loadTickets'); }, async postComment({commit, dispatch, state}, {id, message}) { const {data, success} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token); if (data && success) { state.fetchedData.tickets = 0; await dispatch('loadTickets'); } }, async loadUsers({commit, state}) { if (!state.user.token) return; if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/users/', state.user.token); if (data && success) commit('replaceUsers', data); }, async loadGroups({commit, state}) { if (!state.user.token) return; if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return; const {data, success} = await http.get('/2/groups/', state.user.token); if (data && success) commit('replaceGroups', data); }, async updateTicket({commit, state}, ticket) { const {data, success} = await http.put(`/2/tickets/${ticket.id}/`, ticket, state.user.token); commit('updateTicket', data); }, async updateTicketPartial({commit, state}, {id, ...ticket}) { const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token); commit('updateTicket', data); } }, plugins: [ persistentStatePlugin({ // TODO change remember to some kind of enable field prefix: "lf_", debug: false, isLoadedKey: "persistent_loaded", state: [ "remember", "user", "events", "lastUsed", ] }), sharedStatePlugin({ debug: true, isLoadedKey: "shared_loaded", clearingMutation: "logout", afterInit: "afterSharedInit", state: [ "test", "state_options", "fetchedData", "tickets", "users", "groups", "loadedBoxes", "loadedItems", ], watch: [ "test", "state_options", "fetchedData", "tickets", "users", "groups", "loadedBoxes", "loadedItems", ], mutations: [ //"replaceTickets", ], }), ], }); store.watch((state) => state.user, (user) => { if (store.getters.isLoggedIn) { if (router.currentRoute.value.name === 'login' && router.currentRoute.value.query.redirect) router.push(router.currentRoute.value.query.redirect); else if (router.currentRoute.value.name === 'login') router.push('/'); } else { if (router.currentRoute.value.name !== 'login') { router.push({ name: 'login', query: {redirect: router.currentRoute.value.fullPath}, }); } } }); export default store;