migrate to vue 3

This commit is contained in:
j3d1 2024-06-18 20:10:10 +02:00
parent bb07a6b641
commit bb71c44aa7
16 changed files with 318 additions and 432 deletions

View file

@ -1,43 +1,39 @@
{ {
"name": "c3cloc", "name": "c3lf",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve --modern",
"build": "vue-cli-service build", "build": "vue-cli-service build --modern",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^0.1.8", "@fortawesome/vue-fontawesome": "^3.0.6",
"axios": "^1.6.2", "base-64": "^1.0.0",
"base-64": "^0.1.0",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"core-js": "^3.3.2", "core-js": "^3.35.1",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"lodash": "^4.17.15",
"luxon": "^1.21.3", "luxon": "^1.21.3",
"popper.js": "^1.16.0", "popper.js": "^1.16.1",
"ramda": "^0.26.1", "ramda": "^0.26.1",
"sass": "^1.19.0", "sass": "^1.19.0",
"sass-loader": "^10.4.1", "sass-loader": "^10.4.1",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"vue": "^2.6.10", "vue": "^3.2.47",
"vue-debounce": "^2.2.0", "vue-router": "^4.1.6",
"vue-qrcode-component": "^2.1.1", "vuex": "^4.1.0",
"vue-router": "^3.1.3",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0",
"vuex-shared-mutations": "^1.0.2",
"yarn": "^1.22.21" "yarn": "^1.22.21"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-service": "^5.0.8", "@vue/cli-service": "^5.0.8",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"http-proxy-middleware": "^2.0.6",
"vue-template-compiler": "^2.6.10", "vue-template-compiler": "^2.6.10",
"webpack": "^5" "webpack": "^5",
"webpack-dev-server": "^4.15.1"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View file

@ -5,40 +5,30 @@
<AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/> <AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/>
<Navbar v-if="isLoggedIn" @addItemClicked="openAddItemModal()" @addTicketClicked="openAddTicketModal()"/> <Navbar v-if="isLoggedIn" @addItemClicked="openAddItemModal()" @addTicketClicked="openAddTicketModal()"/>
<router-view/> <router-view/>
<div aria-live="polite" aria-atomic="true" v-if="isLoggedIn"
class="d-flex justify-content-end align-items-start fixed-top mx-1 my-5 py-3"
style="min-height: 200px; z-index: 100000; pointer-events: none">
<Toast v-for="(toast , index) in toasts" :key="index" :title="toast.title" :message="toast.message"
:color="toast.color"
@close="removeToast(toast.key)" style="pointer-events: auto"/>
</div>
</div> </div>
</template> </template>
<script> <script>
import Navbar from '@/components/Navbar'; import Navbar from '@/components/Navbar';
import AddItemModal from '@/components/AddItemModal'; import AddItemModal from '@/components/AddItemModal';
import Toast from './components/Toast';
import {mapState, mapMutations, mapActions, mapGetters} from 'vuex'; import {mapState, mapMutations, mapActions, mapGetters} from 'vuex';
import AddTicketModal from "@/components/AddTicketModal.vue"; import AddTicketModal from "@/components/AddTicketModal.vue";
import AddBoxModal from "@/components/AddBoxModal.vue"; import AddBoxModal from "@/components/AddBoxModal.vue";
export default { export default {
name: 'app', name: 'app',
components: {AddBoxModal, Toast, Navbar, AddItemModal, AddTicketModal}, components: {AddBoxModal, Navbar, AddItemModal, AddTicketModal},
computed: { computed: {
...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal']), ...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal']),
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
}, },
data: () => ({ data: () => ({
addItemModalOpen: false, addItemModalOpen: false,
addTicketModalOpen: false, addTicketModalOpen: false
notify_socket: null,
socket_toast: null,
}), }),
methods: { methods: {
...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal']), ...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal']),
...mapActions(['loadEventItems', 'loadTickets']), ...mapActions(['loadEvents']),
openAddItemModal() { openAddItemModal() {
this.addItemModalOpen = true; this.addItemModalOpen = true;
}, },
@ -50,72 +40,10 @@ export default {
}, },
closeAddTicketModal() { closeAddTicketModal() {
this.addTicketModalOpen = false; this.addTicketModalOpen = false;
}, }
tryConnect() {
if (!this.notify_socket || this.notify_socket.readyState !== WebSocket.OPEN) {
//if (this.socket_toast) {
// this.removeToast(this.socket_toast.key);
// this.socket_toast = null;
//}
//this.socket_toast = this.createToast({
// title: "Connecting...",
// message: "Connecting to websocket...",
// color: "warning"
//});
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
this.notify_socket = new WebSocket(scheme + '://' + window.location.host + '/ws/2/notify/');
this.notify_socket.onopen = (e) => {
//if (this.socket_toast) {
// this.removeToast(this.socket_toast.key);
// this.socket_toast = null;
//}
//this.socket_toast = this.createToast({
// title: "Connection established",
// message: JSON.stringify(e),
// color: "success"
//});
//console.log(e);
};
this.notify_socket.onclose = (e) => {
//if (this.socket_toast) {
// this.removeToast(this.socket_toast.key);
// this.socket_toast = null;
//}
//this.socket_toast = this.createToast({
// title: "Connection closed",
// message: JSON.stringify(e),
// color: "danger"
//});
//console.log(e);
setTimeout(() => {
this.tryConnect();
}, 1000);
};
this.notify_socket.onerror = (e) => {
//if (this.socket_toast) {
// this.removeToast(this.socket_toast.key);
// this.socket_toast = null;
//}
//this.socket_toast = this.createToast({
// title: "Connection error",
// message: JSON.stringify(e),
// color: "danger"
//});
//console.log(e);
setTimeout(() => {
this.tryConnect();
}, 1000);
};
this.notify_socket.onmessage = (e) => {
let data = JSON.parse(e.data);
this.loadEventItems()
this.loadTickets()
}
}
},
}, },
created: function () { created: function () {
this.tryConnect(); document.title = document.location.hostname;
} }
}; };
</script> </script>

View file

@ -15,6 +15,7 @@
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem'; import EditItem from '@/components/EditItem';
import {mapActions, mapState} from "vuex";
export default { export default {
name: 'AddItemModal', name: 'AddItemModal',
@ -23,12 +24,16 @@ export default {
data: () => ({ data: () => ({
item: {} item: {}
}), }),
computed: {
...mapState(['lastUsed'])
},
created() { created() {
this.item = {box: this.$store.state.lastUsed.box || '', cid: this.$store.state.lastUsed.cid || ''}; this.item = {box: this.lastUsed.box || '', cid: this.lastUsed.cid || ''};
}, },
methods: { methods: {
...mapActions(['postItem']),
saveNewItem() { saveNewItem() {
this.$store.dispatch('postItem', this.item).then(() => { this.postItem(this.item).then(() => {
this.$emit('close'); this.$emit('close');
}); });
} }

View file

@ -17,6 +17,7 @@
</template> </template>
<script> <script>
import {mapActions} from 'vuex';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import EditItem from '@/components/EditItem'; import EditItem from '@/components/EditItem';
@ -32,11 +33,12 @@ export default {
} }
}), }),
created() { created() {
this.ticket = {box: this.$store.state.lastUsed.box || '', cid: this.$store.state.lastUsed.cid || ''}; this.ticket = {};
}, },
methods: { methods: {
...mapActions(['postManualTicket']),
saveNewTicket() { saveNewTicket() {
this.$store.dispatch('postManualTicket', this.ticket).then(() => { this.postManualTicket(this.ticket).then(() => {
this.$emit('close'); this.$emit('close');
}); });
} }

View file

@ -1,31 +1,6 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-lg-3 col-xl-2"> <div class="col-lg-3 col-xl-2">
<!--div class="card bg-dark text-light mb-2" id="filters">
<div class="card-body">
<h5 class="card-title text-info">Sort & Filter</h5>
<div class="form-group" v-for="(column, index) in columns" :key="index">
<label>{{ column }}</label>
<div class="input-group">
<div class="input-group-prepend">
<button
:class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
type="button"
@click="toggleSort(column)">
<font-awesome-icon :icon="getSortIcon(column)"/>
</button>
</div>
<input
type="text"
class="form-control"
placeholder="filter"
:value="filters[column]"
@input="changeFilter(column, $event.target.value)"
>
</div>
</div>
</div>
</div-->
</div> </div>
<div class="col-lg-9 col-xl-8"> <div class="col-lg-9 col-xl-8">
<div class="w-100" <div class="w-100"
@ -48,6 +23,8 @@
<script> <script>
import {mapGetters} from "vuex";
export default { export default {
name: 'CollapsableCards', name: 'CollapsableCards',
props: { props: {
@ -75,25 +52,18 @@ export default {
}; };
}, },
created() { created() {
const query = this.$router.currentRoute.query.collapsed; const query = this.$router.currentRoute ? (this.$router.currentRoute.query ? this.$router.currentRoute.query.collapsed : null) : null;
if (query !== null && query !== undefined) { if (query !== null && query !== undefined) {
this.collapsed = this.unpackInt(parseInt(query), this.sections.length); this.collapsed = this.unpackInt(parseInt(query), this.sections.length);
} else { } else {
this.collapsed = this.sections.map(() => true); this.collapsed = this.sections.map(() => true);
} }
//this.$router.push({...this.$router.currentRoute, query: {...this.$router.currentRoute.query, layout}});
//this.collapsed = this.sections.map(() => true);
/*this.columns.map(e => ({
k: e,
v: this.$store.getters.getFilters[e]
})).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));*/
}, },
computed: { computed: {
grouped_items() { grouped_items() {
return this.sections.map(section => this.items.filter(item => item[this.keyName] === section.slug)); return this.sections.map(section => this.items.filter(item => item[this.keyName] === section.slug));
}, },
...mapGetters(['route']),
}, },
methods: { methods: {
packInt(arr) { packInt(arr) {
@ -112,8 +82,11 @@ export default {
collapsed: { collapsed: {
handler() { handler() {
const encoded = this.packInt(this.collapsed).toString() const encoded = this.packInt(this.collapsed).toString()
if (this.$router.currentRoute.query.collapsed !== encoded) if (this.route.query.collapsed !== encoded)
this.$router.push({...this.$router.currentRoute, query: {...this.$router.currentRoute.query, collapsed: encoded}}); this.$router.push({
...this.$router.currentRoute,
query: {...this.$router.currentRoute.query, collapsed: encoded}
});
}, },
deep: true, deep: true,
}, },

View file

@ -119,13 +119,13 @@ export default {
emits: ['addItemClicked', 'addTicketClicked'], emits: ['addItemClicked', 'addTicketClicked'],
computed: { computed: {
...mapState(['events']), ...mapState(['events']),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout"]), ...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]),
}, },
methods: { methods: {
...mapActions(['changeEvent', 'changeView', 'searchEventItems']), ...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['logout']), ...mapMutations(['logout']),
navigateTo(link) { navigateTo(link) {
if (this.$router.currentRoute.path !== link) if (this.route.path !== link)
this.$router.push(link); this.$router.push(link);
}, },
isItemView() { isItemView() {
@ -135,9 +135,9 @@ export default {
return this.getActiveView === 'tickets' || this.getActiveView === 'ticket'; return this.getActiveView === 'tickets' || this.getActiveView === 'ticket';
}, },
setLayout(layout) { setLayout(layout) {
if (this.$router.currentRoute.query.layout === layout) if (this.route.query.layout === layout)
return; return;
this.$router.push({...this.$router.currentRoute, query: {...this.$router.currentRoute.query, layout}}); this.$router.push({...this.route, query: {...this.route.query, layout}});
}, },
} }
}; };

View file

@ -1,48 +0,0 @@
<template>
<div class="toast" :class="color && ('border-' + color)" role="alert" ref="toast" data-autohide="false">
<div class="toast-header" :class="[color && ('bg-' + color), color && 'text-light']">
<strong class="mr-auto pr-3">{{ title }}</strong>
<small>{{ displayTime }}</small>
<button type="button" class="ml-2 mb-1 close" @click="close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body" v-html="message">{{ message }}</div>
</div>
</template>
<script>
import $ from 'jquery';
import 'bootstrap/js/dist/toast';
import {DateTime} from 'luxon';
export default {
name: 'Toast',
props: ['title', 'message', 'color'],
data: () => ({
creationTime: DateTime.local(),
displayTime: 'just now',
timer: undefined
}),
mounted() {
const {toast} = this.$refs;
$(toast).toast('show');
this.timer = setInterval(this.updateDisplayTime, 1000);
},
methods: {
close() {
const {toast} = this.$refs;
$(toast).toast('hide');
window.setTimeout(() => {
this.$emit('close');
}, 500);
},
updateDisplayTime() {
this.displayTime = this.creationTime.toRelative();
}
},
beforeDestroy() {
clearInterval(this.timer);
}
};
</script>

View file

@ -1,11 +1,8 @@
import Vue from 'vue'; import {createApp} from 'vue'
import App from './App.vue'; import App from './App.vue';
import {sync} from 'vuex-router-sync';
import store from './store'; import store from './store';
import router from './router'; import router from './router';
// bootstrap
import 'jquery/dist/jquery.min.js';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.min.js'; import 'bootstrap/dist/js/bootstrap.min.js';
@ -46,20 +43,12 @@ import {
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
import vueDebounce from 'vue-debounce';
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList,
faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave, faEye, faComment, faUser, faComments, faEnvelope, faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave, faEye, faComment, faUser, faComments, faEnvelope,
faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight); faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight);
Vue.component('font-awesome-icon', FontAwesomeIcon);
sync(store, router);
new Vue({ const app = createApp(App).use(store).use(router);
el: '#app',
store,
router,
render: h => h(App),
});
Vue.use(vueDebounce); app.component('font-awesome-icon', FontAwesomeIcon);
app.mount('#app')

View file

@ -1,24 +1,21 @@
import {createRouter, createWebHistory} from 'vue-router'
import store from '@/store';
import Items from './views/Items'; import Items from './views/Items';
import Boxes from './views/Boxes'; import Boxes from './views/Boxes';
import Files from './views/Files'; import Files from './views/Files';
import Error from './views/Error';
import HowTo from './views/HowTo'; import HowTo from './views/HowTo';
import VueRouter from 'vue-router';
import Vue from 'vue';
import Login from '@/views/Login.vue'; import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue'; import Register from '@/views/Register.vue';
import Debug from "@/views/admin/Debug.vue"; import Debug from "@/views/admin/Debug.vue";
import Tickets from "@/views/Tickets.vue"; import Tickets from "@/views/Tickets.vue";
import Ticket from "@/views/Ticket.vue"; import Ticket from "@/views/Ticket.vue";
import Admin from "@/views/admin/Admin.vue"; import Admin from "@/views/admin/Admin.vue";
import store from "@/store";
import Empty from "@/views/Empty.vue"; import Empty from "@/views/Empty.vue";
import Events from "@/views/admin/Events.vue"; import Events from "@/views/admin/Events.vue";
import AccessControl from "@/views/admin/AccessControl.vue"; import AccessControl from "@/views/admin/AccessControl.vue";
import {default as BoxesAdmin} from "@/views/admin/Boxes.vue" import {default as BoxesAdmin} from "@/views/admin/Boxes.vue"
Vue.use(VueRouter);
const routes = [ const routes = [
{path: '/', redirect: '/37C3/items', meta: {requiresAuth: false}}, {path: '/', redirect: '/37C3/items', meta: {requiresAuth: false}},
{path: '/login/', name: 'login', component: Login, meta: {requiresAuth: false}}, {path: '/login/', name: 'login', component: Login, meta: {requiresAuth: false}},
@ -75,11 +72,11 @@ const routes = [
] ]
}, },
{path: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}}, {path: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}},
{path: '*', component: Error},
]; ];
const router = new VueRouter({ const router = createRouter({
mode: 'history', history: createWebHistory(),
linkActiveClass: "active",
routes, routes,
}); });
@ -101,13 +98,10 @@ router.beforeEach((to, from, next) => {
}); });
router.afterEach((to, from) => { router.afterEach((to, from) => {
if (to.params.event) { if (to.params.event && to.params.event !== store.state.lastEvent) {
//console.log('update last event', to.params.event); //console.log('update last event', to.params.event);
store.commit('updateLastEvent', to.params.event); store.commit('updateLastEvent', to.params.event);
} }
if (to.query.layout !== from.query.layout) {
store.commit('triggerLayoutChange', to.query.layout);
}
}); });
export default router; export default router;

View file

@ -1,63 +1,11 @@
import Vue from 'vue'; import {createStore} from 'vuex';
import Vuex from 'vuex'; import router from './router';
import AxiosBootstrap from 'axios';
import * as _ from 'lodash/fp';
import router from '../router';
import * as base64 from 'base-64'; import * as base64 from 'base-64';
import * as utf8 from 'utf8'; import * as utf8 from 'utf8';
import {ticketStateColorLookup, ticketStateIconLookup} from "@/utils"; import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils";
import createMutationsSharer from "vuex-shared-mutations";
Vue.use(Vuex); const store = createStore({
const axios = AxiosBootstrap.create({
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.error('error interceptor fired', error.message);
if (error.isAxiosError) {
const message = `
<h3>A HTTP ${error.config.method} request failed.</h3>
<p>
url: ${error.config.url}
<br>
timeout: ${!!error.request.timeout}
<br>
response-body: ${error.response && error.response.body}
</p>
`;
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);
}
});
const store = new Vuex.Store({
state: { state: {
keyIncrement: 0, keyIncrement: 0,
events: [], events: [],
@ -68,26 +16,30 @@ const store = new Vuex.Store({
tickets: [], tickets: [],
users: [], users: [],
groups: [], groups: [],
state_options: [],
lastEvent: localStorage.getItem('lf_lastEvent') || '37C3', lastEvent: localStorage.getItem('lf_lastEvent') || '37C3',
lastUsed: JSON.parse(localStorage.getItem('lf_lastUsed') || '{}'), lastUsed: JSON.parse(localStorage.getItem('lf_lastUsed') || '{}'),
remember: false, remember: false,
user: null,
password: null, user: {
userPermissions: [], username: null,
token: null, password: null,
state_options: [], permissions: [],
token_expiry: null, token: null,
expiry: null,
},
local_loaded: false, local_loaded: false,
showAddBoxModal: false, showAddBoxModal: false,
toggle: false,
}, },
getters: { getters: {
getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.lastEvent, route: state => router.currentRoute.value,
getActiveView: state => state.route.name || 'items', getEventSlug: state => router.currentRoute.value.params.event ? router.currentRoute.value.params.event : state.lastEvent,
getFilters: state => state.route.query, getActiveView: state => router.currentRoute.value.name || 'items',
getFilters: state => router.currentRoute.value.query,
getBoxes: state => state.loadedBoxes, getBoxes: state => state.loadedBoxes,
checkPermission: state => (event, perm) => state.userPermissions.includes(`${event}:${perm}`) || state.userPermissions.includes(`*:${perm}`), checkPermission: state => (event, perm) => state.user.permissions.includes(`${event}:${perm}`) || state.user.permissions.includes(`*:${perm}`),
hasPermissions: state => state.userPermissions.length > 0, hasPermissions: state => state.user.permissions.length > 0,
stateInfo: state => (slug) => { stateInfo: state => (slug) => {
const obj = state.state_options.filter((s) => s.value === slug)[0]; const obj = state.state_options.filter((s) => s.value === slug)[0];
if (obj) { if (obj) {
@ -107,9 +59,8 @@ const store = new Vuex.Store({
} }
}, },
layout: (state, getters) => { layout: (state, getters) => {
state.toggle = !state.toggle; if (router.currentRoute.value.query.layout)
if (router.currentRoute.query.layout) return router.currentRoute.value.query.layout;
return router.currentRoute.query.layout;
if (getters.getActiveView === 'items') if (getters.getActiveView === 'items')
return 'cards'; return 'cards';
if (getters.getActiveView === 'tickets') if (getters.getActiveView === 'tickets')
@ -118,21 +69,20 @@ const store = new Vuex.Store({
isLoggedIn(state) { isLoggedIn(state) {
if (!state.local_loaded) { if (!state.local_loaded) {
state.remember = localStorage.getItem('remember') === 'true'; state.remember = localStorage.getItem('remember') === 'true';
state.user = localStorage.getItem('user'); state.user.username = localStorage.getItem('user');
//state.password = localStorage.getItem('password'); //state.password = localStorage.getItem('password');
state.userPermissions = JSON.parse(localStorage.getItem('permissions') || '[]'); state.user.permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
state.token = localStorage.getItem('token'); state.user.token = localStorage.getItem('token');
state.token_expiry = localStorage.getItem('token_expiry'); state.user.expiry = localStorage.getItem('token_expiry');
state.local_loaded = true; state.local_loaded = true;
axios.defaults.headers.common['Authorization'] = `Token ${state.token}`;
} }
return state.user !== null && state.token !== null; return state.user && state.user.username !== null && state.user.token !== null;
}, },
}, },
mutations: { mutations: {
updateLastUsed(state, diff) { updateLastUsed(state, diff) {
state.lastUsed = _.extend(state.lastUsed, diff); state.lastUsed = {...state.lastUsed, ...diff};
localStorage.setItem('lf_lastUsed', JSON.stringify(state.lastUsed)); localStorage.setItem('lf_lastUsed', JSON.stringify(state.lastUsed));
}, },
updateLastEvent(state, slug) { updateLastEvent(state, slug) {
@ -200,38 +150,39 @@ const store = new Vuex.Store({
localStorage.setItem('remember', remember); localStorage.setItem('remember', remember);
}, },
setUser(state, user) { setUser(state, user) {
state.user = user; state.user.username = user;
if (user) if (user)
localStorage.setItem('user', user); localStorage.setItem('user', user);
}, },
setPassword(state, password) { setPassword(state, password) {
state.password = password; state.user.password = password;
}, },
setPermissions(state, permissions) { setPermissions(state, permissions) {
state.userPermissions = permissions; state.user.permissions = permissions;
if (permissions) if (permissions)
localStorage.setItem('permissions', JSON.stringify(permissions)); localStorage.setItem('permissions', JSON.stringify(permissions));
}, },
setToken(state, {token, expiry}) { setToken(state, {token, expiry}) {
state.token = token; const user = {...state.user};
state.token_expiry = expiry; user.token = token;
user.expiry = expiry;
state.user = user;
if (token) if (token)
localStorage.setItem('token', token); localStorage.setItem('token', token);
localStorage.setItem('token_expiry', expiry); localStorage.setItem('token_expiry', expiry);
}, },
logout(state) { setUserInfo(state, user) {
state.user = null; state.user = user;
state.token = null; },
localStorage.removeItem('user'); logout(state) {
localStorage.removeItem('permissions'); const user = {...state.user};
localStorage.removeItem('token'); user.user = null;
localStorage.removeItem('token_expiry'); user.password = null;
if (router.currentRoute.name !== 'login') user.token = null;
router.push('/login'); user.expiry = null;
user.permissions = null;
state.user = user;
}, },
triggerLayoutChange(state) {
state.toggle = !state.toggle;
}
}, },
actions: { actions: {
async login({commit, dispatch, state}, {username, password, remember}) { async login({commit, dispatch, state}, {username, password, remember}) {
@ -247,7 +198,6 @@ const store = new Vuex.Store({
commit('setToken', data); commit('setToken', data);
commit('setUser', username); commit('setUser', username);
commit('setPassword', password); commit('setPassword', password);
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
dispatch('afterLogin'); dispatch('afterLogin');
return true; return true;
} else { } else {
@ -258,18 +208,17 @@ const store = new Vuex.Store({
return false; return false;
} }
}, },
async reloadToken({commit, state}) { async reloadToken({commit, state, getters}) {
try { try {
if (state.password) { if (state.user.username && state.user.password) {
const data = await fetch('/api/2/login/', { const data = await fetch('/api/2/login/', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: state.user, password: state.password}), body: JSON.stringify({username: state.user.username, password: state.user.password}),
credentials: 'omit' credentials: 'omit'
}).then(r => r.json()).catch(e => console.error(e)) }).then(r => r.json()).catch(e => console.error(e))
if (data && data.token) { if (data && data.token) {
commit('setToken', data); commit('setToken', data);
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
return true; return true;
} }
} }
@ -280,29 +229,33 @@ const store = new Vuex.Store({
store.commit('logout'); store.commit('logout');
}, },
//async verifyToken({commit, state}) { //async verifyToken({commit, state}) {
async afterLogin({dispatch}) { async afterLogin({dispatch, state}) {
const boxes = dispatch('loadBoxes'); let promises = [];
const states = dispatch('fetchTicketStates'); promises.push(dispatch('loadBoxes'));
const items = dispatch('loadEventItems'); promises.push(dispatch('fetchTicketStates'));
const tickets = dispatch('loadTickets'); promises.push(dispatch('loadEventItems'));
const user = dispatch('loadUserInfo'); promises.push(dispatch('loadTickets'));
await Promise.all([boxes, items, tickets, user, states]); if (!state.user.permissions) {
promises.push(dispatch('loadUserInfo'));
}
await Promise.all(promises);
}, },
async fetchImage({state}, url) { async fetchImage({state}, url) {
return await fetch(url, {headers: {'Authorization': `Token ${state.token}`}}); return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}});
}, },
async loadUserInfo({commit}) { async loadUserInfo({commit, state}) {
const {data} = await axios.get('/2/self/'); const {data, success} = await http.get('/2/self/', state.user.token);
commit('setUser', data.username);
commit('setPermissions', data.permissions); commit('setPermissions', data.permissions);
}, },
async loadEvents({commit}) { async loadEvents({commit, state}) {
const {data} = await axios.get('/2/events/'); const {data, success} = await http.get('/2/events/', state.user.token);
commit('replaceEvents', data); if (data && success)
commit('replaceEvents', data);
}, },
async fetchTicketStates({commit}) { async fetchTicketStates({commit, state}) {
const {data} = await axios.get('/2/tickets/states/'); const {data, success} = await http.get('/2/tickets/states/', state.user.token);
commit('replaceTicketStates', data); if (data && success)
commit('replaceTicketStates', data);
}, },
changeEvent({dispatch, getters, commit}, eventName) { changeEvent({dispatch, getters, commit}, eventName) {
router.push({path: `/${eventName.slug}/${getters.getActiveView}/`}); router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
@ -321,121 +274,121 @@ const store = new Vuex.Store({
if (slug in state.itemCache) { if (slug in state.itemCache) {
commit('replaceLoadedItems', state.itemCache[slug]); commit('replaceLoadedItems', state.itemCache[slug]);
} }
const {data} = await axios.get(`/2/${slug}/items/`); const {data, success} = await http.get(`/2/${slug}/items/`, state.user.token);
commit('replaceLoadedItems', data); if (data && success) {
commit('setItemCache', {slug, items: data}); commit('replaceLoadedItems', data);
commit('setItemCache', {slug, items: data});
}
} catch (e) { } catch (e) {
console.error("Error loading items"); console.error("Error loading items");
} }
}, },
async searchEventItems({commit, getters}, query) { async searchEventItems({commit, getters, state}, 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(`/2/${getters.getEventSlug}/items/${bar}/`); const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${bar}/`, state.user.token);
commit('replaceLoadedItems', data); if (data && success)
commit('replaceLoadedItems', data);
}, },
async loadBoxes({commit}) { async loadBoxes({commit, state}) {
const {data} = await axios.get('/2/boxes/'); const {data, success} = await http.get('/2/boxes/', state.user.token);
commit('replaceBoxes', data); if (data && success)
commit('replaceBoxes', data);
}, },
async createBox({commit, dispatch}, box) { async createBox({commit, dispatch, state}, box) {
const {data} = await axios.post('/2/boxes/', box); const {data, success} = await http.post('/2/boxes/', box, state.user.token);
commit('replaceBoxes', data); commit('replaceBoxes', data);
dispatch('loadBoxes').then(() => { dispatch('loadBoxes').then(() => {
commit('closeAddBoxModal'); commit('closeAddBoxModal');
}); });
}, },
async deleteBox({commit, dispatch}, box_id) { async deleteBox({commit, dispatch, state}, box_id) {
await axios.delete(`/2/boxes/${box_id}/`); await http.delete(`/2/boxes/${box_id}/`, state.user.token);
dispatch('loadBoxes'); dispatch('loadBoxes');
}, },
async updateItem({commit, getters}, item) { async updateItem({commit, getters, state}, item) {
const {data} = await axios.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item); const {
data,
success
} = await http.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token);
commit('updateItem', data); commit('updateItem', data);
}, },
async markItemReturned({commit, getters}, item) { async markItemReturned({commit, getters, state}, item) {
await axios.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true}); await http.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true}, state.user.token);
commit('removeItem', item); commit('removeItem', item);
}, },
async deleteItem({commit, getters}, item) { async deleteItem({commit, getters, state}, item) {
await axios.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item); await http.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token);
commit('removeItem', item); commit('removeItem', item);
}, },
async postItem({commit, getters}, item) { async postItem({commit, getters, state}, item) {
commit('updateLastUsed', {box: item.box, cid: item.cid}); commit('updateLastUsed', {box: item.box, cid: item.cid});
const {data} = await axios.post(`/2/${getters.getEventSlug}/item/`, item); const {data, success} = await http.post(`/2/${getters.getEventSlug}/item/`, item, state.user.token);
commit('appendItem', data); commit('appendItem', data);
}, },
async loadTickets({commit}) { async loadTickets({commit, state}) {
const {data} = await axios.get('/2/tickets/'); const {data, success} = await http.get('/2/tickets/', state.user.token);
commit('replaceTickets', data); if (data && success)
commit('replaceTickets', data);
}, },
async sendMail({commit, dispatch}, {id, message}) { async sendMail({commit, dispatch, state}, {id, message}) {
const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message}); const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token);
await dispatch('loadTickets'); if (data && success) {
await dispatch('loadTickets');
}
}, },
async postManualTicket({commit, dispatch}, {sender, message, title,}) { async postManualTicket({commit, dispatch, state}, {sender, message, title,}) {
const {data} = await axios.post(`/2/tickets/manual/`, { const {data, success} = await http.post(`/2/tickets/manual/`, {
name: title, name: title,
sender, sender,
body: message, body: message,
recipient: 'mail@c3lf.de' recipient: 'mail@c3lf.de'
}); }, state.user.token);
await dispatch('loadTickets'); await dispatch('loadTickets');
}, },
async postComment({commit, dispatch}, {id, message}) { async postComment({commit, dispatch, state}, {id, message}) {
const {data} = await axios.post(`/2/tickets/${id}/comment/`, {comment: message}); const {data, success} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token);
await dispatch('loadTickets'); if (data && success) {
await dispatch('loadTickets');
}
}, },
async loadUsers({commit}) { async loadUsers({commit, state}) {
const {data} = await axios.get('/2/users/'); const {data, success} = await http.get('/2/users/', state.user.token);
commit('replaceUsers', data); if (data && success)
commit('replaceUsers', data);
}, },
async loadGroups({commit}) { async loadGroups({commit, state}) {
const {data} = await axios.get('/2/groups/'); const {data, success} = await http.get('/2/groups/', state.user.token);
commit('replaceGroups', data); if (data && success)
commit('replaceGroups', data);
}, },
async updateTicket({commit}, ticket) { async updateTicket({commit, state}, ticket) {
const {data} = await axios.put(`/2/tickets/${ticket.id}/`, ticket); const {data, success} = await http.put(`/2/tickets/${ticket.id}/`, ticket, state.user.token);
commit('updateTicket', data); commit('updateTicket', data);
}, },
async updateTicketPartial({commit}, {id, ...ticket}) { async updateTicketPartial({commit, state}, {id, ...ticket}) {
const {data} = await axios.patch(`/2/tickets/${id}/`, ticket); const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token);
commit('updateTicket', data); commit('updateTicket', data);
} }
}, },
plugins: [createMutationsSharer({ });
predicate: [
'replaceLoadedItems', store.watch((state) => state.user, (user) => {
'setItemCache', console.log('user changed', user);
'setLayout', if (store.getters.isLoggedIn) {
'replaceBoxes', if (router.currentRoute.value.name === 'login' && router.currentRoute.value.query.redirect)
'updateItem', router.push(router.currentRoute.value.query.redirect);
'removeItem', else if (router.currentRoute.value.name === 'login')
'appendItem', router.push('/');
'replaceTickets', } else {
'replaceUsers', if (router.currentRoute.value.name !== 'login') {
'replaceGroups', router.push({
'updateTicket', name: 'login',
'openAddBoxModal', query: {redirect: router.currentRoute.value.fullPath},
'closeAddBoxModal', });
'createToast', }
'removeToast', }
'setRemember',
'setUser',
'setPermissions',
'setToken',
'logout',
]
})],
}); });
export default store; export default store;
store.dispatch('loadEvents').then(() => {
if (store.getters.isLoggedIn) {
axios.defaults.headers.common['Authorization'] = `Token ${store.state.token}`;
store.dispatch('afterLogin');
}
});

View file

@ -24,4 +24,80 @@ function ticketStateIconLookup(ticket) {
return 'exclamation'; return 'exclamation';
} }
export {ticketStateColorLookup, ticketStateIconLookup}; const http = {
get: async (url, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
});
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
post: async (url, data, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
body: JSON.stringify(data),
});
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
put: async (url, data, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
body: JSON.stringify(data),
});
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
patch: async (url, data, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'PATCH',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
body: JSON.stringify(data),
});
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
delete: async (url, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'DELETE',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
});
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
}
}
export {ticketStateColorLookup, ticketStateIconLookup, http};

View file

@ -93,7 +93,7 @@ export default {
...mapGetters(['layout']), ...mapGetters(['layout']),
}, },
methods: { methods: {
...mapActions(['deleteItem', 'markItemReturned']), ...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem']),
openLightboxModalWith(item) { openLightboxModalWith(item) {
this.lightboxHash = item.file; this.lightboxHash = item.file;
}, },
@ -107,12 +107,15 @@ export default {
this.editingItem = null; this.editingItem = null;
}, },
saveEditingItem() { // Saves the edited copy of the item. saveEditingItem() { // Saves the edited copy of the item.
this.$store.dispatch('updateItem', this.editingItem); this.updateItem(this.editingItem);
this.closeEditingModal(); this.closeEditingModal();
}, },
confirm(message) { confirm(message) {
return window.confirm(message); return window.confirm(message);
} }
},
mounted() {
this.loadEventItems();
} }
}; };
</script> </script>

View file

@ -100,7 +100,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
input{ input {
background-color: var(--dark); background-color: var(--dark);
border: var(--gray) 1px solid;; border: var(--gray) 1px solid;;

View file

@ -6,7 +6,7 @@
<div class="card-header"> <div class="card-header">
<ul class="nav nav-tabs card-header-tabs"> <ul class="nav nav-tabs card-header-tabs">
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" :to="{name: 'admin'}" active-class="active" exact>Dashboard</router-link> <router-link class="nav-link" :to="{name: 'admin'}" active-class="dummy" exact-active-class="active">Dashboard</router-link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link> <router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link>

View file

@ -22,27 +22,13 @@
{{ box.name }} {{ box.name }}
</li> </li>
</ul> </ul>
<h3 class="text-center">Mails</h3>
<!--p>{{ mails }}</p-->
<ul>
<li v-for="mail in mails" :key="mail.id">
{{ mail.id }}
</li>
</ul>
<h3 class="text-center">Issues</h3> <h3 class="text-center">Issues</h3>
<!--p>{{ issues }}</p--> <!--p>{{ issues }}</p-->
<ul> <ul>
<li v-for="issue in issues" :key="issue.id"> <li v-for="issue in tickets" :key="issue.id">
{{ issue.id }} {{ issue.id }}
</li> </li>
</ul> </ul>
<h3 class="text-center">System Events</h3>
<!--p>{{ systemEvents }}</p-->
<ul>
<li v-for="systemEvent in systemEvents" :key="systemEvent.id">
{{ systemEvent.id }}
</li>
</ul>
</div> </div>
</template> </template>
@ -54,19 +40,17 @@ export default {
name: 'Debug', name: 'Debug',
components: {Table}, components: {Table},
computed: { computed: {
...mapState(['events', 'loadedItems', 'loadedBoxes', 'mails', 'issues', 'systemEvents']), ...mapState(['events', 'loadedItems', 'loadedBoxes', 'tickets']),
qr_url() { qr_url() {
return window.location.href; return window.location.href;
} }
}, },
methods: { methods: {
...mapActions(['changeEvent', 'loadMails', 'loadIssues', 'loadSystemEvents']), ...mapActions(['changeEvent', 'loadTickets']),
}, },
mounted() { mounted() {
this.loadMails(); this.loadTickets();
this.loadIssues();
this.loadSystemEvents();
} }
}; };
</script> </script>

31
web/vue.config.js Normal file
View file

@ -0,0 +1,31 @@
// vue.config.js
module.exports = {
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*"
},
proxy: {
'^/media/2': {
target: 'https://staging.c3lf.de/',
changeOrigin: true
},
'^/api/2': {
target: 'https://staging.c3lf.de/',
changeOrigin: true,
},
'^/api/1': {
target: 'https://staging.c3lf.de/',
changeOrigin: true,
},
'^/ws/2': {
target: 'http://127.0.0.1:8082/',
//changeOrigin: true,
ws: true,
logLevel: 'debug',
},
}
}
}