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>
</div>
</div>
<div class="custom-control-inline mr-1">
<button type="button" class="btn mx-1 text-nowrap btn-success" @click="$emit('addClicked')">
<font-awesome-icon icon="plus"/>
<span class="d-none d-md-inline">&nbsp;Add</span>
</button>
<div class="btn-group btn-group-toggle">
<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>
</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>
<form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline">
<ul class="nav nav-tabs flex-nowrap">
<li class="nav-item" v-if="checkPermission(getEventSlug, 'view_item')">
<router-link :to="{name: 'items', params: {event: getEventSlug}}"
:class="['nav-link', { active: isItemView() }]">
Items
</router-link>
</li>
<li class="nav-item" v-if="checkPermission(getEventSlug, 'view_issuethread')">
<router-link :to="{name: 'tickets', params: {event: getEventSlug}}"
:class="['nav-link', { active: isTicketView() }]">
Tickets
</router-link>
</li>
<li class="nav-item" v-if="checkPermission(getEventSlug, 'delete_event')">
<router-link :to="{name: 'admin'}" class="nav-link" active-class="active">
Admin
</router-link>
</li>
</ul>
<form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline mr-1" v-if="hasPermissions">
<input
class="form-control w-100"
type="search"
@ -41,10 +39,33 @@
disabled
>
</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">
<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"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ getActiveView }}
@ -55,16 +76,20 @@
<a class="nav-link text-nowrap" href="#" @click="changeView(link)">{{ link.title }}</a>
</li>
</ul>
</li>
</li-->
<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)">
{{ link.title }}
</a>
</li>
<li class="nav-item">
<a class="nav-link text-nowrap" href="/logout" @click.prevent="logout()">
Logout
</a>
</li>
</ul>
</div>
</nav>
</template>
@ -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) {
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>
<style lang="scss">
<style lang="scss" scoped>
@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>

View file

@ -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,11 +9,31 @@ 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 => {
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 = `
<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));
@ -35,6 +54,7 @@ axios.interceptors.response.use(response => response, error => {
store.commit('createToast', {title: 'Error: Unknown', message: error.toString(), color: 'danger'});
}
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`);
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');
}
});