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

View file

@ -5,40 +5,30 @@
<AddBoxModal v-if="showAddBoxModal && isLoggedIn" @close="closeAddBoxModal()" isModal="true"/>
<Navbar v-if="isLoggedIn" @addItemClicked="openAddItemModal()" @addTicketClicked="openAddTicketModal()"/>
<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>
</template>
<script>
import Navbar from '@/components/Navbar';
import AddItemModal from '@/components/AddItemModal';
import Toast from './components/Toast';
import {mapState, mapMutations, mapActions, mapGetters} from 'vuex';
import AddTicketModal from "@/components/AddTicketModal.vue";
import AddBoxModal from "@/components/AddBoxModal.vue";
export default {
name: 'app',
components: {AddBoxModal, Toast, Navbar, AddItemModal, AddTicketModal},
components: {AddBoxModal, Navbar, AddItemModal, AddTicketModal},
computed: {
...mapState(['loadedItems', 'layout', 'toasts', 'showAddBoxModal']),
...mapGetters(['isLoggedIn']),
},
data: () => ({
addItemModalOpen: false,
addTicketModalOpen: false,
notify_socket: null,
socket_toast: null,
addTicketModalOpen: false
}),
methods: {
...mapMutations(['removeToast', 'createToast', 'closeAddBoxModal', 'openAddBoxModal']),
...mapActions(['loadEventItems', 'loadTickets']),
...mapActions(['loadEvents']),
openAddItemModal() {
this.addItemModalOpen = true;
},
@ -50,72 +40,10 @@ export default {
},
closeAddTicketModal() {
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 () {
this.tryConnect();
document.title = document.location.hostname;
}
};
</script>

View file

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

View file

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

View file

@ -1,31 +1,6 @@
<template>
<div class="row">
<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 class="col-lg-9 col-xl-8">
<div class="w-100"
@ -48,6 +23,8 @@
<script>
import {mapGetters} from "vuex";
export default {
name: 'CollapsableCards',
props: {
@ -75,25 +52,18 @@ export default {
};
},
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) {
this.collapsed = this.unpackInt(parseInt(query), this.sections.length);
} else {
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: {
grouped_items() {
return this.sections.map(section => this.items.filter(item => item[this.keyName] === section.slug));
},
...mapGetters(['route']),
},
methods: {
packInt(arr) {
@ -112,8 +82,11 @@ export default {
collapsed: {
handler() {
const encoded = this.packInt(this.collapsed).toString()
if (this.$router.currentRoute.query.collapsed !== encoded)
this.$router.push({...this.$router.currentRoute, query: {...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}
});
},
deep: true,
},

View file

@ -119,13 +119,13 @@ export default {
emits: ['addItemClicked', 'addTicketClicked'],
computed: {
...mapState(['events']),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout"]),
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]),
},
methods: {
...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapMutations(['logout']),
navigateTo(link) {
if (this.$router.currentRoute.path !== link)
if (this.route.path !== link)
this.$router.push(link);
},
isItemView() {
@ -135,9 +135,9 @@ export default {
return this.getActiveView === 'tickets' || this.getActiveView === 'ticket';
},
setLayout(layout) {
if (this.$router.currentRoute.query.layout === layout)
if (this.route.query.layout === layout)
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 {sync} from 'vuex-router-sync';
import store from './store';
import router from './router';
// bootstrap
import 'jquery/dist/jquery.min.js';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.min.js';
@ -46,20 +43,12 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
import vueDebounce from 'vue-debounce';
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,
faArchive, faMinus, faExclamation, faHourglass, faClipboard, faTasks, faAngleDown, faAngleRight);
Vue.component('font-awesome-icon', FontAwesomeIcon);
sync(store, router);
new Vue({
el: '#app',
store,
router,
render: h => h(App),
});
const app = createApp(App).use(store).use(router);
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 Boxes from './views/Boxes';
import Files from './views/Files';
import Error from './views/Error';
import HowTo from './views/HowTo';
import VueRouter from 'vue-router';
import Vue from 'vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
import Debug from "@/views/admin/Debug.vue";
import Tickets from "@/views/Tickets.vue";
import Ticket from "@/views/Ticket.vue";
import Admin from "@/views/admin/Admin.vue";
import store from "@/store";
import Empty from "@/views/Empty.vue";
import Events from "@/views/admin/Events.vue";
import AccessControl from "@/views/admin/AccessControl.vue";
import {default as BoxesAdmin} from "@/views/admin/Boxes.vue"
Vue.use(VueRouter);
const routes = [
{path: '/', redirect: '/37C3/items', 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: '*', component: Error},
];
const router = new VueRouter({
mode: 'history',
const router = createRouter({
history: createWebHistory(),
linkActiveClass: "active",
routes,
});
@ -101,13 +98,10 @@ router.beforeEach((to, from, next) => {
});
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);
store.commit('updateLastEvent', to.params.event);
}
if (to.query.layout !== from.query.layout) {
store.commit('triggerLayoutChange', to.query.layout);
}
});
export default router;

View file

@ -1,63 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import AxiosBootstrap from 'axios';
import * as _ from 'lodash/fp';
import router from '../router';
import {createStore} from 'vuex';
import router from './router';
import * as base64 from 'base-64';
import * as utf8 from 'utf8';
import {ticketStateColorLookup, ticketStateIconLookup} from "@/utils";
import createMutationsSharer from "vuex-shared-mutations";
import {ticketStateColorLookup, ticketStateIconLookup, http} from "@/utils";
Vue.use(Vuex);
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({
const store = createStore({
state: {
keyIncrement: 0,
events: [],
@ -68,26 +16,30 @@ const store = new Vuex.Store({
tickets: [],
users: [],
groups: [],
state_options: [],
lastEvent: localStorage.getItem('lf_lastEvent') || '37C3',
lastUsed: JSON.parse(localStorage.getItem('lf_lastUsed') || '{}'),
remember: false,
user: null,
password: null,
userPermissions: [],
token: null,
state_options: [],
token_expiry: null,
user: {
username: null,
password: null,
permissions: [],
token: null,
expiry: null,
},
local_loaded: false,
showAddBoxModal: false,
toggle: false,
},
getters: {
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,
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.userPermissions.includes(`${event}:${perm}`) || state.userPermissions.includes(`*:${perm}`),
hasPermissions: state => state.userPermissions.length > 0,
checkPermission: state => (event, perm) => state.user.permissions.includes(`${event}:${perm}`) || state.user.permissions.includes(`*:${perm}`),
hasPermissions: state => state.user.permissions.length > 0,
stateInfo: state => (slug) => {
const obj = state.state_options.filter((s) => s.value === slug)[0];
if (obj) {
@ -107,9 +59,8 @@ const store = new Vuex.Store({
}
},
layout: (state, getters) => {
state.toggle = !state.toggle;
if (router.currentRoute.query.layout)
return router.currentRoute.query.layout;
if (router.currentRoute.value.query.layout)
return router.currentRoute.value.query.layout;
if (getters.getActiveView === 'items')
return 'cards';
if (getters.getActiveView === 'tickets')
@ -118,21 +69,20 @@ const store = new Vuex.Store({
isLoggedIn(state) {
if (!state.local_loaded) {
state.remember = localStorage.getItem('remember') === 'true';
state.user = localStorage.getItem('user');
state.user.username = localStorage.getItem('user');
//state.password = localStorage.getItem('password');
state.userPermissions = JSON.parse(localStorage.getItem('permissions') || '[]');
state.token = localStorage.getItem('token');
state.token_expiry = localStorage.getItem('token_expiry');
state.user.permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
state.user.token = localStorage.getItem('token');
state.user.expiry = localStorage.getItem('token_expiry');
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: {
updateLastUsed(state, diff) {
state.lastUsed = _.extend(state.lastUsed, diff);
state.lastUsed = {...state.lastUsed, ...diff};
localStorage.setItem('lf_lastUsed', JSON.stringify(state.lastUsed));
},
updateLastEvent(state, slug) {
@ -200,38 +150,39 @@ const store = new Vuex.Store({
localStorage.setItem('remember', remember);
},
setUser(state, user) {
state.user = user;
state.user.username = user;
if (user)
localStorage.setItem('user', user);
},
setPassword(state, password) {
state.password = password;
state.user.password = password;
},
setPermissions(state, permissions) {
state.userPermissions = permissions;
state.user.permissions = permissions;
if (permissions)
localStorage.setItem('permissions', JSON.stringify(permissions));
},
setToken(state, {token, expiry}) {
state.token = token;
state.token_expiry = expiry;
const user = {...state.user};
user.token = token;
user.expiry = expiry;
state.user = user;
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');
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;
},
triggerLayoutChange(state) {
state.toggle = !state.toggle;
}
},
actions: {
async login({commit, dispatch, state}, {username, password, remember}) {
@ -247,7 +198,6 @@ const store = new Vuex.Store({
commit('setToken', data);
commit('setUser', username);
commit('setPassword', password);
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
dispatch('afterLogin');
return true;
} else {
@ -258,18 +208,17 @@ const store = new Vuex.Store({
return false;
}
},
async reloadToken({commit, state}) {
async reloadToken({commit, state, getters}) {
try {
if (state.password) {
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, password: state.password}),
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);
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
return true;
}
}
@ -280,29 +229,33 @@ const store = new Vuex.Store({
store.commit('logout');
},
//async verifyToken({commit, state}) {
async afterLogin({dispatch}) {
const boxes = dispatch('loadBoxes');
const states = dispatch('fetchTicketStates');
const items = dispatch('loadEventItems');
const tickets = dispatch('loadTickets');
const user = dispatch('loadUserInfo');
await Promise.all([boxes, items, tickets, user, states]);
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 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}) {
const {data} = await axios.get('/2/self/');
commit('setUser', data.username);
async loadUserInfo({commit, state}) {
const {data, success} = await http.get('/2/self/', state.user.token);
commit('setPermissions', data.permissions);
},
async loadEvents({commit}) {
const {data} = await axios.get('/2/events/');
commit('replaceEvents', data);
async loadEvents({commit, state}) {
const {data, success} = await http.get('/2/events/', state.user.token);
if (data && success)
commit('replaceEvents', data);
},
async fetchTicketStates({commit}) {
const {data} = await axios.get('/2/tickets/states/');
commit('replaceTicketStates', data);
async fetchTicketStates({commit, state}) {
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}/`});
@ -321,121 +274,121 @@ const store = new Vuex.Store({
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});
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}, query) {
async searchEventItems({commit, getters, state}, query) {
const foo = utf8.encode(query);
const bar = base64.encode(foo);
const {data} = await axios.get(`/2/${getters.getEventSlug}/items/${bar}/`);
commit('replaceLoadedItems', data);
const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${bar}/`, state.user.token);
if (data && success)
commit('replaceLoadedItems', data);
},
async loadBoxes({commit}) {
const {data} = await axios.get('/2/boxes/');
commit('replaceBoxes', data);
async loadBoxes({commit, state}) {
const {data, success} = await http.get('/2/boxes/', state.user.token);
if (data && success)
commit('replaceBoxes', data);
},
async createBox({commit, dispatch}, box) {
const {data} = await axios.post('/2/boxes/', box);
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}, box_id) {
await axios.delete(`/2/boxes/${box_id}/`);
async deleteBox({commit, dispatch, state}, box_id) {
await http.delete(`/2/boxes/${box_id}/`, state.user.token);
dispatch('loadBoxes');
},
async updateItem({commit, getters}, item) {
const {data} = await axios.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item);
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}, item) {
await axios.patch(`/2/${getters.getEventSlug}/item/${item.uid}/`, {returned: true});
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}, item) {
await axios.delete(`/2/${getters.getEventSlug}/item/${item.uid}/`, 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}, item) {
async postItem({commit, getters, state}, item) {
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);
},
async loadTickets({commit}) {
const {data} = await axios.get('/2/tickets/');
commit('replaceTickets', data);
async loadTickets({commit, state}) {
const {data, success} = await http.get('/2/tickets/', state.user.token);
if (data && success)
commit('replaceTickets', data);
},
async sendMail({commit, dispatch}, {id, message}) {
const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message});
await dispatch('loadTickets');
async sendMail({commit, dispatch, state}, {id, message}) {
const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token);
if (data && success) {
await dispatch('loadTickets');
}
},
async postManualTicket({commit, dispatch}, {sender, message, title,}) {
const {data} = await axios.post(`/2/tickets/manual/`, {
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}, {id, message}) {
const {data} = await axios.post(`/2/tickets/${id}/comment/`, {comment: message});
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) {
await dispatch('loadTickets');
}
},
async loadUsers({commit}) {
const {data} = await axios.get('/2/users/');
commit('replaceUsers', data);
async loadUsers({commit, state}) {
const {data, success} = await http.get('/2/users/', state.user.token);
if (data && success)
commit('replaceUsers', data);
},
async loadGroups({commit}) {
const {data} = await axios.get('/2/groups/');
commit('replaceGroups', data);
async loadGroups({commit, state}) {
const {data, success} = await http.get('/2/groups/', state.user.token);
if (data && success)
commit('replaceGroups', data);
},
async updateTicket({commit}, ticket) {
const {data} = await axios.put(`/2/tickets/${ticket.id}/`, ticket);
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}, {id, ...ticket}) {
const {data} = await axios.patch(`/2/tickets/${id}/`, ticket);
async updateTicketPartial({commit, state}, {id, ...ticket}) {
const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token);
commit('updateTicket', data);
}
},
plugins: [createMutationsSharer({
predicate: [
'replaceLoadedItems',
'setItemCache',
'setLayout',
'replaceBoxes',
'updateItem',
'removeItem',
'appendItem',
'replaceTickets',
'replaceUsers',
'replaceGroups',
'updateTicket',
'openAddBoxModal',
'closeAddBoxModal',
'createToast',
'removeToast',
'setRemember',
'setUser',
'setPermissions',
'setToken',
'logout',
]
})],
});
store.watch((state) => state.user, (user) => {
console.log('user changed', 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;
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';
}
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']),
},
methods: {
...mapActions(['deleteItem', 'markItemReturned']),
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'updateItem']),
openLightboxModalWith(item) {
this.lightboxHash = item.file;
},
@ -107,12 +107,15 @@ export default {
this.editingItem = null;
},
saveEditingItem() { // Saves the edited copy of the item.
this.$store.dispatch('updateItem', this.editingItem);
this.updateItem(this.editingItem);
this.closeEditingModal();
},
confirm(message) {
return window.confirm(message);
}
},
mounted() {
this.loadEventItems();
}
};
</script>

View file

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

View file

@ -6,7 +6,7 @@
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<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 class="nav-item">
<router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link>

View file

@ -22,27 +22,13 @@
{{ box.name }}
</li>
</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>
<!--p>{{ issues }}</p-->
<ul>
<li v-for="issue in issues" :key="issue.id">
<li v-for="issue in tickets" :key="issue.id">
{{ issue.id }}
</li>
</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>
</template>
@ -54,19 +40,17 @@ export default {
name: 'Debug',
components: {Table},
computed: {
...mapState(['events', 'loadedItems', 'loadedBoxes', 'mails', 'issues', 'systemEvents']),
...mapState(['events', 'loadedItems', 'loadedBoxes', 'tickets']),
qr_url() {
return window.location.href;
}
},
methods: {
...mapActions(['changeEvent', 'loadMails', 'loadIssues', 'loadSystemEvents']),
...mapActions(['changeEvent', 'loadTickets']),
},
mounted() {
this.loadMails();
this.loadIssues();
this.loadSystemEvents();
this.loadTickets();
}
};
</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',
},
}
}
}