diff --git a/web/src/components/Navbar.vue b/web/src/components/Navbar.vue index 3aa52d1..714dee0 100644 --- a/web/src/components/Navbar.vue +++ b/web/src/components/Navbar.vue @@ -10,28 +10,26 @@ :class="{ active: event.slug === getEventSlug }" @click="changeEvent(event)">{{ event.slug }} - -
- -
- - -
-
- - - -
+ +
- +
+
+ + +
+ + +
+ - @@ -80,23 +105,55 @@ export default { //{'title':'mass-edit','path':'massedit'}, ], links: [ - {'title': 'howto engel', 'path': '/howto/'} + {'title': 'howto engel', 'path': '/howto/'}, ] }), computed: { - ...mapState(['events', 'activeEvent', 'layout']), - ...mapGetters(['getEventSlug', 'getActiveView']), + ...mapState(['events', 'layout']), + ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions"]), }, methods: { ...mapActions(['changeEvent', 'changeView', 'searchEventItems']), - ...mapMutations(['setLayout']), + ...mapMutations(['setLayout', 'logout']), navigateTo(link) { - this.$router.push(link); + if (this.$router.currentRoute.path !== link) + this.$router.push(link); + }, + isItemView() { + return this.getActiveView === 'items' || this.getActiveView === 'item'; + }, + isTicketView() { + return this.getActiveView === 'tickets' || this.getActiveView === 'ticket'; } } }; - \ No newline at end of file diff --git a/web/src/store/index.js b/web/src/store/index.js index 1fb6474..4a2cc35 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; import AxiosBootstrap from 'axios'; -import config from '../config'; import * as _ from 'lodash/fp'; import router from '../router'; @@ -10,17 +9,37 @@ import * as utf8 from 'utf8'; Vue.use(Vuex); const axios = AxiosBootstrap.create({ - baseURL: config.service.url, - auth: config.service.auth + baseURL: '/api', }); - axios.interceptors.response.use(response => response, error => { - console.log('error interceptor fired'); - console.error(error); // todo: toast error - console.log(Object.entries(error)); - - if (error.isAxiosError) { + if (error.response.status === 401) { + console.log('401 interceptor fired'); + store.dispatch('reloadToken').then((ok) => { + if (ok) { + error.config.headers['Authorization'] = `Token ${store.state.token}`; + return axios.request(error.config); + } + }); + } else if (error.response.status === 403) { const message = ` +

Access denied.

+

+ url: ${error.config.url} +
+ method: ${error.config.method} +
+ response-body: ${error.response && error.response.body} +

+ `; + store.commit('createToast', {title: 'Error: Access denied', message, color: 'danger'}); + return Promise.reject(error) + } else { + console.log('error interceptor fired'); + console.error(error); // todo: toast error + console.log(Object.entries(error)); + + if (error.isAxiosError) { + const message = `

A HTTP ${error.config.method} request failed.

url: ${error.config.url} @@ -30,11 +49,12 @@ axios.interceptors.response.use(response => response, error => { response-body: ${error.response && error.response.body}

`; - store.commit('createToast', {title: 'Error: HTTP', message, color: 'danger'}); - } else { - store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'}); + store.commit('createToast', {title: 'Error: HTTP', message, color: 'danger'}); + } else { + store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'}); + } + return Promise.reject(error); } - return Promise.reject(error); }); const store = new Vuex.Store({ @@ -43,20 +63,50 @@ const store = new Vuex.Store({ events: [], layout: 'cards', loadedItems: [], + itemCache: {}, loadedBoxes: [], toasts: [], - lastUsed: localStorage.getItem('lf_lastUsed') || {}, + tickets: [], + users: [], + groups: [], + lastEvent: localStorage.getItem('lf_lastEvent') || '36C3', + lastUsed: JSON.parse(localStorage.getItem('lf_lastUsed') || '{}'), + remember: false, + user: null, + password: null, + userPermissions: [], + token: null, + token_expiry: null, + local_loaded: false, }, getters: { - getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.events.length ? state.events[0].slug : '36C3', + getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.lastEvent, getActiveView: state => state.route.name || 'items', getFilters: state => state.route.query, - getBoxes: state => state.loadedBoxes + getBoxes: state => state.loadedBoxes, + checkPermission: state => (event, perm) => state.userPermissions.includes(`${event}:${perm}`) || state.userPermissions.includes(`*:${perm}`), + hasPermissions: state => state.userPermissions.length > 0, + isLoggedIn(state) { + if (!state.local_loaded) { + state.remember = localStorage.getItem('remember') === 'true' + state.user = localStorage.getItem('user') + state.userPermissions = JSON.parse(localStorage.getItem('permissions') || '[]') + state.token = localStorage.getItem('token') + state.token_expiry = localStorage.getItem('token_expiry') + state.local_loaded = true + } + + return state.user !== null && state.token !== null; + }, }, mutations: { updateLastUsed(state, diff) { state.lastUsed = _.extend(state.lastUsed, diff); - localStorage.setItem('lf_lastUsed', state.lastUsed); + localStorage.setItem('lf_lastUsed', JSON.stringify(state.lastUsed)); + }, + updateLastEvent(state, slug) { + state.lastEvent = slug; + localStorage.setItem('lf_lastEvent', slug); }, replaceEvents(state, events) { state.events = events; @@ -67,6 +117,9 @@ const store = new Vuex.Store({ replaceLoadedItems(state, newItems) { state.loadedItems = newItems; }, + setItemCache(state, {slug, items}) { + state.itemCache[slug] = items; + }, setLayout(state, layout) { state.layout = layout; }, @@ -83,6 +136,19 @@ const store = new Vuex.Store({ appendItem(state, item) { state.loadedItems.push(item); }, + replaceTickets(state, tickets) { + state.tickets = tickets; + }, + replaceUsers(state, users) { + state.users = users; + }, + replaceGroups(state, groups) { + state.groups = groups; + }, + updateTicket(state, updatedTicket) { + const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; + Object.assign(ticket, updatedTicket); + }, createToast(state, {title, message, color}) { var toast = {title, message, color, key: state.keyIncrement} state.toasts.push(toast); @@ -91,61 +157,187 @@ const store = new Vuex.Store({ }, removeToast(state, key) { state.toasts = state.toasts.filter(toast => toast.key !== key); - } + }, + setRemember(state, remember) { + state.remember = remember; + localStorage.setItem('remember', remember); + }, + setUser(state, user) { + state.user = user; + if (user) + localStorage.setItem('user', user); + }, + setPassword(state, password) { + state.password = password; + }, + setPermissions(state, permissions) { + state.userPermissions = permissions; + if (permissions) + localStorage.setItem('permissions', JSON.stringify(permissions)); + }, + setToken(state, {token, expiry}) { + state.token = token; + state.token_expiry = expiry; + if (token) + localStorage.setItem('token', token); + localStorage.setItem('token_expiry', expiry); + }, + logout(state) { + state.user = null; + state.token = null; + localStorage.removeItem('user'); + localStorage.removeItem('permissions'); + localStorage.removeItem('token'); + localStorage.removeItem('token_expiry'); + if (router.currentRoute.name !== 'login') + router.push('/login'); + }, }, actions: { + async login({commit, dispatch, state}, {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.token) { + commit('setToken', data); + commit('setUser', username); + commit('setPassword', password); + axios.defaults.headers.common['Authorization'] = `Token ${data.token}`; + dispatch('afterLogin'); + return true; + } else { + return false; + } + } catch (e) { + console.error(e); + return false; + } + }, + async reloadToken({commit, state}) { + try { + if (data.password) { + const data = await fetch('/api/2/login/', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({username: state.user, password: state.password}), + credentials: 'omit' + }).then(r => r.json()) + if (data.token) { + commit('setToken', data); + axios.defaults.headers.common['Authorization'] = `Token ${data.token}`; + return true; + } + } + } catch (e) { + console.error(e); + } + //credentials failed, logout + store.commit('logout'); + }, + async afterLogin({dispatch}) { + const boxes = dispatch('loadBoxes'); + const items = dispatch('loadEventItems'); + const tickets = dispatch('loadTickets'); + const user = dispatch('loadUserInfo'); + await Promise.all([boxes, items, tickets, user]); + }, + async fetchImage({state}, url) { + return await fetch(url, {headers: {'Authorization': `Token ${state.token}`}}); + }, + async loadUserInfo({commit}) { + const {data} = await axios.get('/2/self/'); + commit('setUser', data.username); + commit('setPermissions', data.permissions); + }, async loadEvents({commit}) { - const {data} = await axios.get('/1/events'); + const {data} = await axios.get('/2/events/'); commit('replaceEvents', data); }, - changeEvent({dispatch, getters}, eventName) { - router.push({path: `/${eventName.slug}/${getters.getActiveView}`}); + changeEvent({dispatch, getters, commit}, eventName) { + router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); dispatch('loadEventItems'); }, changeView({getters}, link) { - router.push({path: `/${getters.getEventSlug}/${link.path}`}); + router.push({path: `/${getters.getEventSlug}/${link.path}/`}); }, showBoxContent({getters}, box) { - router.push({path: `/${getters.getEventSlug}/items`, query: {box}}); + router.push({path: `/${getters.getEventSlug}/items/`, query: {box}}); }, - async loadEventItems({commit, getters}) { - const {data} = await axios.get(`/1/${getters.getEventSlug}/items`); - commit('replaceLoadedItems', data); + async loadEventItems({commit, getters, state}) { + try { + commit('replaceLoadedItems', []); + const slug = getters.getEventSlug; + if (slug in state.itemCache) { + commit('replaceLoadedItems', state.itemCache[slug]); + } + const {data} = await axios.get(`/2/${slug}/items/`); + commit('replaceLoadedItems', data); + commit('setItemCache', {slug, items: data}); + } catch (e) { + console.error("Error loading items"); + } }, async searchEventItems({commit, getters}, query) { const foo = utf8.encode(query); const bar = base64.encode(foo); - const {data} = await axios.get(`/1/${getters.getEventSlug}/items/${bar}`); + const {data} = await axios.get(`/2/${getters.getEventSlug}/items/${bar}/`); commit('replaceLoadedItems', data); }, async loadBoxes({commit}) { - const {data} = await axios.get('/1/boxes'); + const {data} = await axios.get('/2/boxes/'); commit('replaceBoxes', data); }, async updateItem({commit, getters}, item) { - const {data} = await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, item); + const {data} = await axios.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item); commit('updateItem', data); }, async markItemReturned({commit, getters}, item) { - await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, {returned: true}); + await axios.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true}); commit('removeItem', item); }, async deleteItem({commit, getters}, item) { - await axios.delete(`/1/${getters.getEventSlug}/item/${item.uid}`, item); + await axios.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item); commit('removeItem', item); }, async postItem({commit, getters}, item) { commit('updateLastUsed', {box: item.box, cid: item.cid}); - const {data} = await axios.post(`/1/${getters.getEventSlug}/item`, item); + const {data} = await axios.post(`/2/${getters.getEventSlug}/item/`, item); commit('appendItem', data); - } + }, + async loadTickets({commit}) { + const {data} = await axios.get('/2/tickets/'); + commit('replaceTickets', data); + }, + async sendMail({commit, dispatch}, {id, message}) { + const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message}); + await dispatch('loadTickets'); + }, + async postManualTicket({commit, dispatch}, {sender, message, title,}) { + const {data} = await axios.post(`/2/tickets/manual/`, {name: title, sender, body: message, recipient: 'mail@c3lf.de'}); + await dispatch('loadTickets'); + }, + async loadUsers({commit}) { + const {data} = await axios.get('/2/users/'); + commit('replaceUsers', data); + }, + async loadGroups({commit}) { + const {data} = await axios.get('/2/groups/'); + commit('replaceGroups', data); + }, } }); export default store; store.dispatch('loadEvents').then(() => { - store.dispatch('loadEventItems'); - store.dispatch('loadBoxes'); + if (store.getters.isLoggedIn) { + axios.defaults.headers.common['Authorization'] = `Token ${store.state.token}`; + store.dispatch('afterLogin'); + } });