update vuex store to use API v2

This commit is contained in:
j3d1 2024-01-07 21:38:25 +01:00
parent 0ebfe3adfb
commit 21ec29caa8
2 changed files with 316 additions and 67 deletions

View file

@ -10,28 +10,26 @@
:class="{ active: event.slug === getEventSlug }" @click="changeEvent(event)">{{ event.slug }}</a> :class="{ active: event.slug === getEventSlug }" @click="changeEvent(event)">{{ event.slug }}</a>
</div> </div>
</div> </div>
<ul class="nav nav-tabs flex-nowrap">
<div class="custom-control-inline mr-1"> <li class="nav-item" v-if="checkPermission(getEventSlug, 'view_item')">
<button type="button" class="btn mx-1 text-nowrap btn-success" @click="$emit('addClicked')"> <router-link :to="{name: 'items', params: {event: getEventSlug}}"
<font-awesome-icon icon="plus"/> :class="['nav-link', { active: isItemView() }]">
<span class="d-none d-md-inline">&nbsp;Add</span> Items
</button> </router-link>
<div class="btn-group btn-group-toggle"> </li>
<button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')"> <li class="nav-item" v-if="checkPermission(getEventSlug, 'view_issuethread')">
<font-awesome-icon icon="th"/> <router-link :to="{name: 'tickets', params: {event: getEventSlug}}"
</button> :class="['nav-link', { active: isTicketView() }]">
<button :class="['btn', 'btn-info', { active: layout === 'table' }]" @click="setLayout('table')"> Tickets
<font-awesome-icon icon="list"/> </router-link>
</button> </li>
</div> <li class="nav-item" v-if="checkPermission(getEventSlug, 'delete_event')">
</div> <router-link :to="{name: 'admin'}" class="nav-link" active-class="active">
Admin
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" </router-link>
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> </li>
<span class="navbar-toggler-icon"></span> </ul>
</button> <form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline mr-1" v-if="hasPermissions">
<form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline">
<input <input
class="form-control w-100" class="form-control w-100"
type="search" type="search"
@ -41,10 +39,33 @@
disabled disabled
> >
</form> </form>
<div class="custom-control-inline mr-1" v-if="hasPermissions">
<div class="btn-group btn-group-toggle mr-1" v-if="isItemView()">
<button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')">
<font-awesome-icon icon="th"/>
</button>
<button :class="['btn', 'btn-info', { active: layout === 'table' }]" @click="setLayout('table')">
<font-awesome-icon icon="list"/>
</button>
</div>
<button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addItemClicked')"
v-if="isItemView()">
<font-awesome-icon icon="plus"/>
<span class="d-none d-md-inline">&nbsp;Add Item</span>
</button>
<button type="button" class="btn text-nowrap btn-success mr-1" @click="$emit('addTicketClicked')"
v-if="isTicketView()">
<font-awesome-icon icon="plus"/>
<span class="d-none d-md-inline">&nbsp;Add Ticket</span>
</button>
</div>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <!--li class="nav-item dropdown">
<button class="btn nav-link dropdown-toggle" type="button" id="dropdownMenuButton2" <button class="btn nav-link dropdown-toggle" type="button" id="dropdownMenuButton2"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ getActiveView }} {{ getActiveView }}
@ -55,16 +76,20 @@
<a class="nav-link text-nowrap" href="#" @click="changeView(link)">{{ link.title }}</a> <a class="nav-link text-nowrap" href="#" @click="changeView(link)">{{ link.title }}</a>
</li> </li>
</ul> </ul>
</li> </li-->
<li class="nav-item" v-for="(link, index) in links" v-bind:key="index"> <li class="nav-item" v-for="(link, index) in links" v-bind:key="index">
<a class="nav-link text-nowrap" :href="link.path" @click.prevent="navigateTo(link.path)"> <a class="nav-link text-nowrap" :href="link.path" @click.prevent="navigateTo(link.path)">
{{ link.title }} {{ link.title }}
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link text-nowrap" href="/logout" @click.prevent="logout()">
Logout
</a>
</li>
</ul> </ul>
</div> </div>
</nav> </nav>
</template> </template>
@ -80,23 +105,55 @@ export default {
//{'title':'mass-edit','path':'massedit'}, //{'title':'mass-edit','path':'massedit'},
], ],
links: [ links: [
{'title': 'howto engel', 'path': '/howto/'} {'title': 'howto engel', 'path': '/howto/'},
] ]
}), }),
computed: { computed: {
...mapState(['events', 'activeEvent', 'layout']), ...mapState(['events', 'layout']),
...mapGetters(['getEventSlug', 'getActiveView']), ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions"]),
}, },
methods: { methods: {
...mapActions(['changeEvent', 'changeView', 'searchEventItems']), ...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['setLayout']), ...mapMutations(['setLayout', 'logout']),
navigateTo(link) { 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';
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
@import "../scss/navbar.scss"; @import "../scss/navbar.scss";
.nav-tabs {
margin-bottom: -0.5rem !important;
border-bottom: var(--gray) solid 1px !important;
& .nav-item {
margin-right: 0.5em;
&:first-child {
margin-left: 0.5em;
}
}
& .nav-link {
padding-bottom: 1rem !important;
border: var(--gray) solid 1px !important;
border-bottom: none !important;
&.active {
background: black !important;
border-bottom: none;
color: white !important;
}
}
}
</style> </style>

View file

@ -1,7 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import AxiosBootstrap from 'axios'; import AxiosBootstrap from 'axios';
import config from '../config';
import * as _ from 'lodash/fp'; import * as _ from 'lodash/fp';
import router from '../router'; import router from '../router';
@ -10,17 +9,37 @@ import * as utf8 from 'utf8';
Vue.use(Vuex); Vue.use(Vuex);
const axios = AxiosBootstrap.create({ const axios = AxiosBootstrap.create({
baseURL: config.service.url, baseURL: '/api',
auth: config.service.auth
}); });
axios.interceptors.response.use(response => response, error => { axios.interceptors.response.use(response => response, error => {
console.log('error interceptor fired'); if (error.response.status === 401) {
console.error(error); // todo: toast error console.log('401 interceptor fired');
console.log(Object.entries(error)); store.dispatch('reloadToken').then((ok) => {
if (ok) {
if (error.isAxiosError) { error.config.headers['Authorization'] = `Token ${store.state.token}`;
return axios.request(error.config);
}
});
} else if (error.response.status === 403) {
const message = ` const message = `
<h3>Access denied.</h3>
<p>
url: ${error.config.url}
<br>
method: ${error.config.method}
<br>
response-body: ${error.response && error.response.body}
</p>
`;
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 = `
<h3>A HTTP ${error.config.method} request failed.</h3> <h3>A HTTP ${error.config.method} request failed.</h3>
<p> <p>
url: ${error.config.url} url: ${error.config.url}
@ -30,11 +49,12 @@ axios.interceptors.response.use(response => response, error => {
response-body: ${error.response && error.response.body} response-body: ${error.response && error.response.body}
</p> </p>
`; `;
store.commit('createToast', {title: 'Error: HTTP', message, color: 'danger'}); store.commit('createToast', {title: 'Error: HTTP', message, color: 'danger'});
} else { } else {
store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'}); store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'});
}
return Promise.reject(error);
} }
return Promise.reject(error);
}); });
const store = new Vuex.Store({ const store = new Vuex.Store({
@ -43,20 +63,50 @@ const store = new Vuex.Store({
events: [], events: [],
layout: 'cards', layout: 'cards',
loadedItems: [], loadedItems: [],
itemCache: {},
loadedBoxes: [], loadedBoxes: [],
toasts: [], 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: { 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', getActiveView: state => state.route.name || 'items',
getFilters: state => state.route.query, 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: { mutations: {
updateLastUsed(state, diff) { updateLastUsed(state, diff) {
state.lastUsed = _.extend(state.lastUsed, 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) { replaceEvents(state, events) {
state.events = events; state.events = events;
@ -67,6 +117,9 @@ const store = new Vuex.Store({
replaceLoadedItems(state, newItems) { replaceLoadedItems(state, newItems) {
state.loadedItems = newItems; state.loadedItems = newItems;
}, },
setItemCache(state, {slug, items}) {
state.itemCache[slug] = items;
},
setLayout(state, layout) { setLayout(state, layout) {
state.layout = layout; state.layout = layout;
}, },
@ -83,6 +136,19 @@ const store = new Vuex.Store({
appendItem(state, item) { appendItem(state, item) {
state.loadedItems.push(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}) { createToast(state, {title, message, color}) {
var toast = {title, message, color, key: state.keyIncrement} var toast = {title, message, color, key: state.keyIncrement}
state.toasts.push(toast); state.toasts.push(toast);
@ -91,61 +157,187 @@ const store = new Vuex.Store({
}, },
removeToast(state, key) { removeToast(state, key) {
state.toasts = state.toasts.filter(toast => toast.key !== 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: { 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}) { async loadEvents({commit}) {
const {data} = await axios.get('/1/events'); const {data} = await axios.get('/2/events/');
commit('replaceEvents', data); commit('replaceEvents', data);
}, },
changeEvent({dispatch, getters}, eventName) { changeEvent({dispatch, getters, commit}, eventName) {
router.push({path: `/${eventName.slug}/${getters.getActiveView}`}); router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
dispatch('loadEventItems'); dispatch('loadEventItems');
}, },
changeView({getters}, link) { changeView({getters}, link) {
router.push({path: `/${getters.getEventSlug}/${link.path}`}); router.push({path: `/${getters.getEventSlug}/${link.path}/`});
}, },
showBoxContent({getters}, box) { showBoxContent({getters}, box) {
router.push({path: `/${getters.getEventSlug}/items`, query: {box}}); router.push({path: `/${getters.getEventSlug}/items/`, query: {box}});
}, },
async loadEventItems({commit, getters}) { async loadEventItems({commit, getters, state}) {
const {data} = await axios.get(`/1/${getters.getEventSlug}/items`); try {
commit('replaceLoadedItems', data); 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) { async searchEventItems({commit, getters}, query) {
const foo = utf8.encode(query); const foo = utf8.encode(query);
const bar = base64.encode(foo); 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); commit('replaceLoadedItems', data);
}, },
async loadBoxes({commit}) { async loadBoxes({commit}) {
const {data} = await axios.get('/1/boxes'); const {data} = await axios.get('/2/boxes/');
commit('replaceBoxes', data); commit('replaceBoxes', data);
}, },
async updateItem({commit, getters}, item) { 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); commit('updateItem', data);
}, },
async markItemReturned({commit, getters}, item) { 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); commit('removeItem', item);
}, },
async deleteItem({commit, getters}, 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); commit('removeItem', item);
}, },
async postItem({commit, getters}, item) { async postItem({commit, getters}, item) {
commit('updateLastUsed', {box: item.box, cid: item.cid}); 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); 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; export default store;
store.dispatch('loadEvents').then(() => { store.dispatch('loadEvents').then(() => {
store.dispatch('loadEventItems'); if (store.getters.isLoggedIn) {
store.dispatch('loadBoxes'); axios.defaults.headers.common['Authorization'] = `Token ${store.state.token}`;
store.dispatch('afterLogin');
}
}); });