Compare commits
10 commits
b575926d52
...
906adbe0db
Author | SHA1 | Date | |
---|---|---|---|
906adbe0db | |||
b7a582a86f | |||
b00976e0fa | |||
b8c3bcfa3b | |||
674106a8a5 | |||
81f1f97a6b | |||
125232d1c5 | |||
2c09540a1b | |||
b6ed492382 | |||
6c69948c44 |
30 changed files with 769 additions and 292 deletions
26
TODO.md
26
TODO.md
|
@ -71,8 +71,30 @@
|
|||
* [ ] concept: purge old tickets
|
||||
* [ ] concept: purge old items
|
||||
* [ ] concept: auto email stale after x days
|
||||
* [ ] frontend: reply email field multiline
|
||||
* [ ] concept: customisable autoreply (message, title) + ticket id
|
||||
* [ ] frontend: show from address in issueThread or internal user
|
||||
* [ ] manual ticket creation
|
||||
* [ ] hide citation for incoming mails
|
||||
* [ ] frontend: different icons for send and received mails
|
||||
* [ ] frontend: change ticket status
|
||||
* new mail -> new,
|
||||
* op reply ->
|
||||
* waiting for detail
|
||||
* waiting for address / shipment information
|
||||
* customer reply -> open
|
||||
* needs physical confirmation
|
||||
* needs to be shipped
|
||||
* dhl mail -> closed: shipped
|
||||
* closed: not found
|
||||
* closed: not our problem
|
||||
* closed: timeout (no reply)
|
||||
* closed: duplicate
|
||||
* closed: spam
|
||||
* [ ] concept: split ticket for multiple items
|
||||
* [ ] mail signature
|
||||
* [ ] guru api integration
|
||||
|
||||
## Priority: TODO
|
||||
|
||||
* send mails from web frontend
|
||||
* login / user management
|
||||
* manual ticket creation
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.contrib.auth import login
|
|||
from django.urls import path
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
from django.contrib.auth.models import Group
|
||||
from knox.models import AuthToken
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
|
||||
|
@ -14,9 +15,16 @@ from authentication.models import ExtendedUser
|
|||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
permissions = serializers.SerializerMethodField()
|
||||
groups = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name')
|
||||
|
||||
class Meta:
|
||||
model = ExtendedUser
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name')
|
||||
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
|
||||
read_only_fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
|
||||
|
||||
def get_permissions(self, obj):
|
||||
return list(set(obj.get_permissions()))
|
||||
|
||||
|
||||
@receiver(post_save, sender=ExtendedUser)
|
||||
|
@ -30,7 +38,27 @@ class UserViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = UserSerializer
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
permissions = serializers.SerializerMethodField()
|
||||
members = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = ('id', 'name', 'permissions', 'members')
|
||||
|
||||
def get_permissions(self, obj):
|
||||
return ["*:" + p.codename for p in obj.permissions.all()]
|
||||
|
||||
def get_members(self, obj):
|
||||
return [u.username for u in obj.user_set.all()]
|
||||
|
||||
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def selfUser(request):
|
||||
serializer = UserSerializer(request.user)
|
||||
|
@ -79,6 +107,7 @@ class LoginView(KnoxLoginView):
|
|||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'users', UserViewSet, basename='users')
|
||||
router.register(r'groups', GroupViewSet, basename='groups')
|
||||
|
||||
urlpatterns = router.urls + [
|
||||
path('self/', selfUser),
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-13 02:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0001_initial'),
|
||||
('authentication', '0004_legacy_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventpermission',
|
||||
name='event',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='inventory.event'),
|
||||
),
|
||||
]
|
|
@ -65,7 +65,6 @@ class PermissionsTestCase(TestCase):
|
|||
user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent2'))
|
||||
user.event_permissions.create(permission=Permission.objects.get(codename='add_item'), event=Event.objects.get(slug='testevent1'))
|
||||
user.save()
|
||||
print(user.get_all_permissions())
|
||||
#self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent1')))
|
||||
#self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent2')))
|
||||
#self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent1')))
|
||||
|
|
|
@ -1,17 +1,30 @@
|
|||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import Permission, Group
|
||||
|
||||
from knox.models import AuthToken
|
||||
|
||||
from authentication.models import ExtendedUser
|
||||
from authentication.models import ExtendedUser, EventPermission
|
||||
from core import settings
|
||||
from inventory.models import Event
|
||||
|
||||
|
||||
class UserApiTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.event = Event.objects.create(name='testevent', slug='testevent')
|
||||
self.group1 = Group.objects.create(name='testgroup1')
|
||||
self.group2 = Group.objects.create(name='testgroup2')
|
||||
self.group1.permissions.add(Permission.objects.get(codename='add_item'))
|
||||
self.group1.permissions.add(Permission.objects.get(codename='view_item'))
|
||||
self.group2.permissions.add(Permission.objects.get(codename='view_event'))
|
||||
self.group2.permissions.add(Permission.objects.get(codename='view_item'))
|
||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
||||
self.user.user_permissions.add(*Permission.objects.all())
|
||||
self.user.user_permissions.add(Permission.objects.get(codename='add_event'))
|
||||
self.user.groups.add(self.group1)
|
||||
self.user.groups.add(self.group2)
|
||||
self.user.save()
|
||||
EventPermission.objects.create(event=self.event, user=self.user,
|
||||
permission=Permission.objects.get(codename='delete_item'))
|
||||
self.user.save()
|
||||
self.token = AuthToken.objects.create(user=self.user)
|
||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
||||
|
@ -25,18 +38,28 @@ class UserApiTest(TestCase):
|
|||
self.assertEqual(response.json()[0]['first_name'], '')
|
||||
self.assertEqual(response.json()[0]['last_name'], '')
|
||||
self.assertEqual(response.json()[0]['id'], 1)
|
||||
self.assertEqual(response.json()[0]['groups'], [])
|
||||
self.assertEqual(response.json()[1]['username'], 'testuser')
|
||||
self.assertEqual(response.json()[1]['email'], 'test')
|
||||
self.assertEqual(response.json()[1]['first_name'], '')
|
||||
self.assertEqual(response.json()[1]['last_name'], '')
|
||||
self.assertEqual(response.json()[1]['id'], 2)
|
||||
self.assertEqual(response.json()[1]['groups'], ['testgroup1', 'testgroup2'])
|
||||
|
||||
def test_self_user(self):
|
||||
response = self.client.post('/api/2/self/')
|
||||
response = self.client.get('/api/2/self/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['username'], 'testuser')
|
||||
self.assertEqual(response.json()['email'], 'test')
|
||||
self.assertEqual(response.json()['first_name'], '')
|
||||
self.assertEqual(response.json()['last_name'], '')
|
||||
permissions = response.json()['permissions']
|
||||
self.assertEqual(len(permissions), 5)
|
||||
self.assertTrue('*:add_item' in permissions)
|
||||
self.assertTrue('*:view_item' in permissions)
|
||||
self.assertTrue('*:view_event' in permissions)
|
||||
self.assertTrue('testevent:delete_item' in permissions)
|
||||
self.assertTrue('*:add_event' in permissions)
|
||||
|
||||
def test_register_user(self):
|
||||
anonymous = Client()
|
||||
|
@ -112,3 +135,49 @@ class UserApiTest(TestCase):
|
|||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue('token' in response.json())
|
||||
|
||||
|
||||
class GroupApiTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.event = Event.objects.create(name='testevent', slug='testevent')
|
||||
# Admin, Orga, Team, User are created by default
|
||||
self.group1 = Group.objects.create(name='testgroup1')
|
||||
self.group2 = Group.objects.create(name='testgroup2')
|
||||
self.group1.permissions.add(Permission.objects.get(codename='add_item'))
|
||||
self.group1.permissions.add(Permission.objects.get(codename='view_item'))
|
||||
self.group2.permissions.add(Permission.objects.get(codename='view_event'))
|
||||
self.group2.permissions.add(Permission.objects.get(codename='view_item'))
|
||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
||||
self.user.user_permissions.add(Permission.objects.get(codename='add_event'))
|
||||
self.user.groups.add(self.group1)
|
||||
self.user.groups.add(self.group2)
|
||||
self.user.save()
|
||||
EventPermission.objects.create(event=self.event, user=self.user,
|
||||
permission=Permission.objects.get(codename='delete_item'))
|
||||
self.user.save()
|
||||
self.token = AuthToken.objects.create(user=self.user)
|
||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
||||
|
||||
def test_groups(self):
|
||||
response = self.client.get('/api/2/groups/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.json()), 6)
|
||||
self.assertEqual(response.json()[0]['name'], 'Admin')
|
||||
self.assertEqual(response.json()[1]['name'], 'Orga')
|
||||
self.assertEqual(response.json()[2]['name'], 'Team')
|
||||
self.assertEqual(response.json()[3]['name'], 'User')
|
||||
self.assertEqual(response.json()[4]['name'], 'testgroup1')
|
||||
self.assertEqual(response.json()[5]['name'], 'testgroup2')
|
||||
|
||||
def test_group(self):
|
||||
response = self.client.get('/api/2/groups/5/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['name'], 'testgroup1')
|
||||
permissions = response.json()['permissions']
|
||||
self.assertEqual(len(permissions), 2)
|
||||
self.assertTrue('*:add_item' in permissions)
|
||||
self.assertTrue('*:view_item' in permissions)
|
||||
members = response.json()['members']
|
||||
self.assertEqual(len(members), 1)
|
||||
self.assertEqual(members[0], 'testuser')
|
||||
|
|
|
@ -29,7 +29,7 @@ SECRET_KEY = 'django-insecure-tm*$w_14iqbiy-!7(8#ba7j+_@(7@rf2&a^!=shs&$03b%2*rv
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = [os.getenv('HTTP_HOST', 'localhost')]
|
||||
ALLOWED_HOSTS = [os.getenv('HTTP_HOST', 'localhost'), 'c3lf.de']
|
||||
|
||||
MAIL_DOMAIN = os.getenv('MAIL_DOMAIN', 'localhost')
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ class ItemSerializer(serializers.ModelSerializer):
|
|||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@permission_required('inventory.view_item', raise_exception=True)
|
||||
@permission_required('view_item', raise_exception=True)
|
||||
def search_items(request, event_slug, query):
|
||||
try:
|
||||
event = Event.objects.get(slug=event_slug)
|
||||
|
@ -117,11 +117,11 @@ def item(request, event_slug):
|
|||
try:
|
||||
event = Event.objects.get(slug=event_slug)
|
||||
if request.method == 'GET':
|
||||
if not request.user.has_event_perm(event, 'inventory.view_item'):
|
||||
if not request.user.has_event_perm(event, 'view_item'):
|
||||
return Response(status=403)
|
||||
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
|
||||
elif request.method == 'POST':
|
||||
if not request.user.has_event_perm(event, 'inventory.add_item'):
|
||||
if not request.user.has_event_perm(event, 'add_item'):
|
||||
return Response(status=403)
|
||||
validated_data = ItemSerializer(data=request.data)
|
||||
if validated_data.is_valid():
|
||||
|
@ -131,25 +131,34 @@ def item(request, event_slug):
|
|||
return Response(status=404)
|
||||
|
||||
|
||||
@api_view(['GET', 'PUT', 'DELETE'])
|
||||
@api_view(['GET', 'PUT', 'DELETE', 'PATCH'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def item_by_id(request, event_slug, id):
|
||||
try:
|
||||
event = Event.objects.get(slug=event_slug)
|
||||
item = Item.objects.get(event=event, uid=id)
|
||||
if request.method == 'GET':
|
||||
if not request.user.has_event_perm(event, 'inventory.view_item'):
|
||||
if not request.user.has_event_perm(event, 'view_item'):
|
||||
return Response(status=403)
|
||||
return Response(ItemSerializer(item).data)
|
||||
elif request.method == 'PUT':
|
||||
if not request.user.has_event_perm(event, 'inventory.change_item'):
|
||||
if not request.user.has_event_perm(event, 'change_item'):
|
||||
return Response(status=403)
|
||||
validated_data = ItemSerializer(item, data=request.data)
|
||||
if validated_data.is_valid():
|
||||
validated_data.save()
|
||||
return Response(validated_data.data)
|
||||
return Response(validated_data.errors, status=400)
|
||||
elif request.method == 'PATCH':
|
||||
if not request.user.has_event_perm(event, 'change_item'):
|
||||
return Response(status=403)
|
||||
validated_data = ItemSerializer(item, data=request.data, partial=True)
|
||||
if validated_data.is_valid():
|
||||
validated_data.save()
|
||||
return Response(validated_data.data)
|
||||
return Response(validated_data.errors, status=400)
|
||||
elif request.method == 'DELETE':
|
||||
if not request.user.has_event_perm(event, 'inventory.delete_item'):
|
||||
if not request.user.has_event_perm(event, 'delete_item'):
|
||||
return Response(status=403)
|
||||
item.delete()
|
||||
return Response(status=204)
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import logging
|
||||
|
||||
from django.urls import path
|
||||
from django.urls import re_path
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from rest_framework import routers, viewsets, serializers, status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
from core.settings import MAIL_DOMAIN
|
||||
from mail.models import Email
|
||||
from mail.protocol import send_smtp, make_reply, collect_references
|
||||
from notify_sessions.models import SystemEvent
|
||||
from tickets.models import IssueThread
|
||||
|
||||
|
||||
|
@ -80,9 +82,44 @@ def reply(request, pk):
|
|||
return Response({'status': 'ok'}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
@permission_required('tickets.add_issuethread_manual', raise_exception=True)
|
||||
def manual_ticket(request):
|
||||
if 'name' not in request.data:
|
||||
return Response({'status': 'error', 'message': 'missing name'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'sender' not in request.data:
|
||||
return Response({'status': 'error', 'message': 'missing sender'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'recipient' not in request.data:
|
||||
return Response({'status': 'error', 'message': 'missing recipient'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'body' not in request.data:
|
||||
return Response({'status': 'error', 'message': 'missing body'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
issue = IssueThread.objects.create(
|
||||
name=request.data['name'],
|
||||
manually_created=True,
|
||||
)
|
||||
email = Email.objects.create(
|
||||
issue_thread=issue,
|
||||
sender=request.data['sender'],
|
||||
recipient=request.data['recipient'],
|
||||
subject=request.data['name'],
|
||||
body=request.data['body'],
|
||||
)
|
||||
systemevent = SystemEvent.objects.create(type='email received', reference=email.id)
|
||||
channel_layer = get_channel_layer()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
'general', {"type": "generic.event", "name": "send_message_to_frontend", "event_id": systemevent.id,
|
||||
"message": "email received"}
|
||||
)
|
||||
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r'tickets', IssueViewSet, basename='issues')
|
||||
|
||||
urlpatterns = router.urls + [
|
||||
path('tickets/<int:pk>/reply/', reply, name='reply'),
|
||||
]
|
||||
urlpatterns = ([
|
||||
re_path(r'^tickets/(?P<pk>\d+)/reply/$', reply, name='reply'),
|
||||
re_path(r'^tickets/manual/$', manual_ticket, name='manual_ticket'),
|
||||
] + router.urls)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.7 on 2023-12-22 20:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='issuethread',
|
||||
options={'permissions': [('send_mail', 'Can send mail'), ('add_issuethread_manual', 'Can add issue thread manually')]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuethread',
|
||||
name='manually_created',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -10,10 +10,12 @@ class IssueThread(SoftDeleteModel):
|
|||
state = models.CharField(max_length=255, default='new')
|
||||
assigned_to = models.CharField(max_length=255, null=True)
|
||||
last_activity = models.DateTimeField(auto_now=True)
|
||||
manually_created = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
('send_mail', 'Can send mail'),
|
||||
('add_issuethread_manual', 'Can add issue thread manually'),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.test import TestCase, Client
|
|||
from authentication.models import ExtendedUser
|
||||
from mail.models import Email
|
||||
from tickets.models import IssueThread, StateChange, Comment
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission
|
||||
from knox.models import AuthToken
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ class IssueApiTest(TestCase):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
||||
self.user.user_permissions.add(*Permission.objects.all())
|
||||
self.user.save()
|
||||
self.token = AuthToken.objects.create(user=self.user)
|
||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
||||
|
@ -91,3 +92,19 @@ class IssueApiTest(TestCase):
|
|||
self.assertEqual(response.json()[0]['timeline'][3]['comment'], 'test')
|
||||
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
|
||||
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||
|
||||
def test_manual_creation(self):
|
||||
response = self.client.post('/api/2/tickets/manual/', {'name': 'test issue', 'sender': 'test',
|
||||
'recipient': 'test', 'body': 'test'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json()['state'], 'new')
|
||||
self.assertEqual(response.json()['name'], 'test issue')
|
||||
self.assertEqual(response.json()['assigned_to'], None)
|
||||
timeline = response.json()['timeline']
|
||||
self.assertEqual(len(timeline), 1)
|
||||
self.assertEqual(timeline[0]['type'], 'mail')
|
||||
self.assertEqual(timeline[0]['sender'], 'test')
|
||||
self.assertEqual(timeline[0]['recipient'], 'test')
|
||||
self.assertEqual(timeline[0]['subject'], 'test issue')
|
||||
self.assertEqual(timeline[0]['body'], 'test')
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<AddItemModal v-if="addModalOpen && isLoggedIn" @close="closeAddModal()" isModal="true"/>
|
||||
<Navbar v-if="isLoggedIn" @addClicked="openAddModal()"/>
|
||||
<AddItemModal v-if="addItemModalOpen && isLoggedIn" @close="closeAddItemModal()" isModal="true"/>
|
||||
<AddTicketModal v-if="addTicketModalOpen && isLoggedIn" @close="closeAddTicketModal()" 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 in toasts" :key="toast" :title="toast.title" :message="toast.message"
|
||||
<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>
|
||||
|
@ -18,27 +19,35 @@ 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";
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {Toast, Navbar, AddItemModal},
|
||||
components: {Toast, Navbar, AddItemModal, AddTicketModal},
|
||||
computed: {
|
||||
...mapState(['loadedItems', 'layout', 'toasts']),
|
||||
...mapGetters(['isLoggedIn']),
|
||||
},
|
||||
data: () => ({
|
||||
addModalOpen: false,
|
||||
addItemModalOpen: false,
|
||||
addTicketModalOpen: false,
|
||||
notify_socket: null,
|
||||
socket_toast: null,
|
||||
}),
|
||||
methods: {
|
||||
...mapMutations(['removeToast', 'createToast']),
|
||||
...mapActions(['loadEventItems', 'loadTickets']),
|
||||
openAddModal() {
|
||||
this.addModalOpen = true;
|
||||
openAddItemModal() {
|
||||
this.addItemModalOpen = true;
|
||||
},
|
||||
closeAddModal() {
|
||||
this.addModalOpen = false;
|
||||
openAddTicketModal() {
|
||||
this.addTicketModalOpen = true;
|
||||
},
|
||||
closeAddItemModal() {
|
||||
this.addItemModalOpen = false;
|
||||
},
|
||||
closeAddTicketModal() {
|
||||
this.addTicketModalOpen = false;
|
||||
},
|
||||
tryConnect() {
|
||||
if (!this.notify_socket || this.notify_socket.readyState !== WebSocket.OPEN) {
|
||||
|
|
49
web/src/components/AddTicketModal.vue
Normal file
49
web/src/components/AddTicketModal.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div>
|
||||
<Modal v-if="isModal" title="Add Item" @close="$emit('close')">
|
||||
<template #body>
|
||||
<div>
|
||||
<input type="text" class="form-control" placeholder="Sender" v-model="ticket.sender">
|
||||
<input type="text" class="form-control" placeholder="Title" v-model="ticket.title">
|
||||
<textarea class="form-control" placeholder="Message" v-model="ticket.message"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<button type="button" class="btn btn-secondary" @click="$emit('close')">Cancel</button>
|
||||
<button type="button" class="btn btn-success" @click="saveNewTicket()">Save new Item</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '@/components/Modal';
|
||||
import EditItem from '@/components/EditItem';
|
||||
|
||||
export default {
|
||||
name: 'AddTicketModal',
|
||||
components: {Modal, EditItem},
|
||||
props: ['isModal'],
|
||||
data: () => ({
|
||||
ticket: {
|
||||
sender: '',
|
||||
message: '',
|
||||
title: '',
|
||||
}
|
||||
}),
|
||||
created() {
|
||||
this.ticket = {box: this.$store.state.lastUsed.box || '', cid: this.$store.state.lastUsed.cid || ''};
|
||||
},
|
||||
methods: {
|
||||
saveNewTicket() {
|
||||
this.$store.dispatch('postManualTicket', this.ticket).then(() => {
|
||||
this.$emit('close');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
45
web/src/components/Matrix2D.vue
Normal file
45
web/src/components/Matrix2D.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<table class="table table-striped table-dark" style="table-layout: fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col" v-for="(column, index) in columns" :key="index">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in rows" :key="i">
|
||||
<td>{{ row }}</td>
|
||||
<td v-for="(column, j) in columns" :key="j">
|
||||
<font-awesome-icon v-if="items[j][i]" icon="check" class="text-success"/>
|
||||
<font-awesome-icon v-else icon="times" class="text-danger"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Matrix2D',
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
61
web/src/components/Matrix3D.vue
Normal file
61
web/src/components/Matrix3D.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<table class="table table-striped table-dark" style="table-layout: fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col" v-for="(column, index) in columns" :key="index">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in rows" :key="i">
|
||||
<td>{{ row }}</td>
|
||||
<td v-for="(column, j) in columns" :key="j">
|
||||
<font-awesome-icon v-if="triState(items[j][i]) === 'on'" icon="check" class="text-success"/>
|
||||
<font-awesome-icon v-else-if="triState(items[j][i]) === 'off'" icon="times" class="text-danger"/>
|
||||
<font-awesome-icon v-else icon="minus" class="text-warning"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Matrix3D',
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
foldedDimension: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
triState(list) {
|
||||
if(list.every(e => e === true)) {
|
||||
return 'on';
|
||||
} else if(list.every(e => e === false)) {
|
||||
return 'off';
|
||||
} else {
|
||||
return 'partial';
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -11,25 +11,25 @@
|
|||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs flex-nowrap">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" v-if="checkPermission(getEventSlug, 'view_item')">
|
||||
<router-link :to="{name: 'items', params: {event: getEventSlug}}"
|
||||
:class="['nav-link', { active: getActiveView === 'items' || getActiveView === 'item' }]">
|
||||
:class="['nav-link', { active: isItemView() }]">
|
||||
Items
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item" v-if="checkRole('team')">
|
||||
<li class="nav-item" v-if="checkPermission(getEventSlug, 'view_issuethread')">
|
||||
<router-link :to="{name: 'tickets', params: {event: getEventSlug}}"
|
||||
:class="['nav-link', { active: getActiveView === 'tickets' || getActiveView === 'ticket' }]">
|
||||
:class="['nav-link', { active: isTicketView() }]">
|
||||
Tickets
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav-item" v-if="checkRole('admin')">
|
||||
<router-link :to="{name: 'admin'}" :class="['nav-link', { active: getActiveView === 'admin' }]">
|
||||
<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">
|
||||
<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"
|
||||
|
@ -39,8 +39,8 @@
|
|||
disabled
|
||||
>
|
||||
</form>
|
||||
<div class="custom-control-inline mr-1">
|
||||
<div class="btn-group btn-group-toggle mx-1">
|
||||
<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>
|
||||
|
@ -48,9 +48,15 @@
|
|||
<font-awesome-icon icon="list"/>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn text-nowrap btn-success" @click="$emit('addClicked')">
|
||||
<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"> Add</span>
|
||||
<span class="d-none d-md-inline"> 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"> Add Ticket</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
|
||||
|
@ -77,9 +83,13 @@
|
|||
{{ 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>
|
||||
|
||||
|
@ -99,14 +109,21 @@ export default {
|
|||
]
|
||||
}),
|
||||
computed: {
|
||||
...mapState(['events', 'activeEvent', 'layout']),
|
||||
...mapGetters(['getEventSlug', 'getActiveView', "checkRole"]),
|
||||
...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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
</span>
|
||||
<div class="new-comment">
|
||||
<div class="input-group">
|
||||
<input type="text" placeholder="Add a comment..." v-model="newMail">
|
||||
<button class="btn" @click="sendMail">
|
||||
<input type="text" placeholder="reply mail..." v-model="newMail">
|
||||
<button class="btn btn-primary" @click="sendMail">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<p><span v-html="body"></span></p>
|
||||
<button class="button">👏 2</button>
|
||||
<!--button class="button">👏 2</button>
|
||||
<button class="button | square">
|
||||
<font-awesome-icon icon="user"/>
|
||||
</button>
|
||||
</button-->
|
||||
</div>
|
||||
</div>
|
||||
<button class="show-replies">
|
||||
<!--button class="show-replies">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
|
||||
width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
|
@ -38,7 +38,7 @@
|
|||
<font-awesome-icon icon="user"/>
|
||||
</i>
|
||||
</span>
|
||||
</button>
|
||||
</button-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -34,14 +34,17 @@ import {
|
|||
faComment,
|
||||
faEnvelope,
|
||||
faUser,
|
||||
faComments
|
||||
faComments,
|
||||
faArchive,
|
||||
faMinus,
|
||||
} 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);
|
||||
faWindowClose, faCamera, faStop, faPen, faCheck, faTimes, faSave, faEye, faComment, faUser, faComments, faEnvelope,
|
||||
faArchive, faMinus);
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||
|
||||
//import VueQRCodeComponent from 'vue-qrcode-component'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Items from './views/Items';
|
||||
import Boxes from './views/Boxes';
|
||||
import Files from './views/Files';
|
||||
import Events from './views/Events';
|
||||
import Error from './views/Error';
|
||||
import HowTo from './views/HowTo';
|
||||
import VueRouter from 'vue-router';
|
||||
|
@ -13,25 +12,51 @@ 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";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{path: '/', redirect: '/Camp23/items', meta: {requiresAuth: false}},
|
||||
{path: '/login', name: 'login', component: Login, meta: {requiresAuth: false}},
|
||||
{path: '/register', name: 'register', component: Register, meta: {requiresAuth: false}},
|
||||
{path: '/howto', name: 'howto', component: HowTo, meta: {requiresAuth: true}},
|
||||
{path: '/:event/boxes', name: 'boxes', component: Boxes, meta: {requiresAuth: true}},
|
||||
{path: '/:event/items', name: 'items', component: Items, meta: {requiresAuth: true}},
|
||||
{path: '/:event/box/:uid', name: 'box', component: Boxes, meta: {requiresAuth: true}},
|
||||
{path: '/:event/item/:uid', name: 'item', component: Items, meta: {requiresAuth: true}},
|
||||
{path: '/:event/tickets', name: 'tickets', component: Tickets, meta: {requiresAuth: true}},
|
||||
{path: '/:event/ticket/:id', name: 'ticket', component: Ticket, meta: {requiresAuth: true}},
|
||||
{path: '/admin', name: 'admin', component: Admin, meta: {requiresAuth: true}},
|
||||
{path: '/admin/files', name: 'files', component: Files, meta: {requiresAuth: true}},
|
||||
{path: '/admin/events', name: 'events', component: Events, meta: {requiresAuth: true}},
|
||||
{path: '/admin/debug', name: 'debug', component: Debug, meta: {requiresAuth: true}},
|
||||
{path: '/admin/users', name: 'users', component: Events, meta: {requiresAuth: true}},
|
||||
{path: '/login/', name: 'login', component: Login, meta: {requiresAuth: false}},
|
||||
{path: '/register/', name: 'register', component: Register, meta: {requiresAuth: false}},
|
||||
{path: '/howto/', name: 'howto', component: HowTo, meta: {requiresAuth: true}},
|
||||
{path: '/:event/items/', name: 'items', component: Items, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_item'}},
|
||||
{path: '/:event/item/:uid/', name: 'item', component: Items, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_item'}},
|
||||
{path: '/:event/boxes/', name: 'boxes', component: Boxes, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_container'}},
|
||||
{path: '/:event/box/:uid/', name: 'box', component: Boxes, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_container'}},
|
||||
{path: '/:event/tickets/', name: 'tickets', component: Tickets, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_issuethread'}},
|
||||
{path: '/:event/ticket/:id/', name: 'ticket', component: Ticket, meta:
|
||||
{requiresAuth: true, requiresPermission: 'view_issuethread'}},
|
||||
{path: '/admin/', component: Admin, meta:
|
||||
{requiresAuth: true, requiresPermission: 'delete_event'},
|
||||
children: [
|
||||
{
|
||||
path: 'files/', name: 'files', component: Files, meta:
|
||||
{requiresAuth: true, requiresPermission: 'delete_event'}
|
||||
},
|
||||
{
|
||||
path: 'events/', name: 'events', component: Events, meta:
|
||||
{requiresAuth: true, requiresPermission: 'delete_event'}
|
||||
},
|
||||
{
|
||||
path: '', name: 'admin', component: Debug, meta:
|
||||
{requiresAuth: true, requiresPermission: 'delete_event'}
|
||||
},
|
||||
{
|
||||
path: 'users/', name: 'users', component: AccessControl, meta:
|
||||
{requiresAuth: true, requiresPermission: 'delete_event'}
|
||||
},
|
||||
]
|
||||
},
|
||||
{path: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}},
|
||||
{path: '*', component: Error},
|
||||
];
|
||||
|
||||
|
@ -57,11 +82,16 @@ const router = new VueRouter({
|
|||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth && !store.getters.isLoggedIn) {
|
||||
//console.log("Not logged in, redirecting to login page")
|
||||
console.log("Not logged in, redirecting to login page")
|
||||
next({
|
||||
name: 'login',
|
||||
query: {redirect: to.fullPath},
|
||||
})
|
||||
} else if (to.meta.requiresPermission && !store.getters.checkPermission(to.params.event || "*", to.meta.requiresPermission)) {
|
||||
console.log("Not enough permissions, redirecting to empty page")
|
||||
next({
|
||||
path: '/user',
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
|
|
@ -63,14 +63,18 @@ const store = new Vuex.Store({
|
|||
events: [],
|
||||
layout: 'cards',
|
||||
loadedItems: [],
|
||||
itemCache: {},
|
||||
loadedBoxes: [],
|
||||
toasts: [],
|
||||
tickets: [],
|
||||
userRoles: ['admin', 'team', 'orga', 'user'],
|
||||
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,
|
||||
|
@ -80,11 +84,13 @@ const store = new Vuex.Store({
|
|||
getActiveView: state => state.route.name || 'items',
|
||||
getFilters: state => state.route.query,
|
||||
getBoxes: state => state.loadedBoxes,
|
||||
checkRole: state => role => state.userRoles.includes(role),
|
||||
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
|
||||
|
@ -111,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;
|
||||
},
|
||||
|
@ -130,6 +139,12 @@ const store = new Vuex.Store({
|
|||
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);
|
||||
|
@ -152,6 +167,14 @@ const store = new Vuex.Store({
|
|||
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;
|
||||
|
@ -163,8 +186,10 @@ const store = new Vuex.Store({
|
|||
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');
|
||||
},
|
||||
},
|
||||
|
@ -181,6 +206,7 @@ const store = new Vuex.Store({
|
|||
if (data.token) {
|
||||
commit('setToken', data);
|
||||
commit('setUser', username);
|
||||
commit('setPassword', password);
|
||||
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
|
||||
dispatch('afterLogin');
|
||||
return true;
|
||||
|
@ -194,10 +220,11 @@ const store = new Vuex.Store({
|
|||
},
|
||||
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.token}),
|
||||
body: JSON.stringify({username: state.user, password: state.password}),
|
||||
credentials: 'omit'
|
||||
}).then(r => r.json())
|
||||
if (data.token) {
|
||||
|
@ -205,28 +232,34 @@ const store = new Vuex.Store({
|
|||
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
//credentials failed, logout
|
||||
store.commit('logout');
|
||||
router.push('/login');
|
||||
},
|
||||
async afterLogin({dispatch}) {
|
||||
await dispatch('loadBoxes');
|
||||
await dispatch('loadEventItems');
|
||||
await dispatch('loadTickets');
|
||||
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('/2/events/');
|
||||
commit('replaceEvents', data);
|
||||
},
|
||||
changeEvent({dispatch, getters, commit}, eventName) {
|
||||
router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
|
||||
commit('replaceLoadedItems', []);
|
||||
dispatch('loadEventItems');
|
||||
},
|
||||
changeView({getters}, link) {
|
||||
|
@ -235,10 +268,16 @@ const store = new Vuex.Store({
|
|||
showBoxContent({getters}, box) {
|
||||
router.push({path: `/${getters.getEventSlug}/items/`, query: {box}});
|
||||
},
|
||||
async loadEventItems({commit, getters}) {
|
||||
async loadEventItems({commit, getters, state}) {
|
||||
try {
|
||||
const {data} = await axios.get(`/2/${getters.getEventSlug}/items/`);
|
||||
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");
|
||||
}
|
||||
|
@ -259,7 +298,7 @@ const store = new Vuex.Store({
|
|||
commit('updateItem', data);
|
||||
},
|
||||
async markItemReturned({commit, getters}, item) {
|
||||
await axios.put(`/2/${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) {
|
||||
|
@ -278,7 +317,19 @@ const store = new Vuex.Store({
|
|||
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);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
|
29
web/src/views/Empty.vue
Normal file
29
web/src/views/Empty.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<div class="card bg-dark text-light mb-2" id="filters">
|
||||
<div class="card-header">
|
||||
<h3 class="text-center">User: {{user}}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Your Account is not yet activated. Please contact an admin.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'Empty',
|
||||
computed: mapState(['user']),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,5 +1,18 @@
|
|||
<template>
|
||||
<p>Error</p>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<div class="card bg-dark text-light mb-2" id="filters">
|
||||
<div class="card-header">
|
||||
<h3 class="text-center">Error</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Something went wrong. <a href="/">Go back to the start page </a>or contact an admin.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<Table
|
||||
:columns="['slug', 'name']"
|
||||
:items="events"
|
||||
:keyName="'slug'"
|
||||
v-slot="{ item }"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" @click.stop="changeEvent(item)">
|
||||
<font-awesome-icon icon="archive"/>
|
||||
use
|
||||
</button>
|
||||
<button class="btn btn-danger" @click.stop="">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
delete
|
||||
</button>
|
||||
</div>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapState} from 'vuex';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
export default {
|
||||
name: 'Events',
|
||||
components: {Table},
|
||||
computed: mapState(['events']),
|
||||
methods: mapActions(['changeEvent']),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
68
web/src/views/admin/AccessControl.vue
Normal file
68
web/src/views/admin/AccessControl.vue
Normal file
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3 class="text-center">Users</h3>
|
||||
<Table :items="users" :columns="['username']" :key-name="'id'">
|
||||
<template v-slot:default="{item}">
|
||||
<span>
|
||||
{{item.groups.join(', ')}}
|
||||
</span>
|
||||
</template>
|
||||
</Table>
|
||||
<h3 class="text-center">Groups</h3>
|
||||
<Table :items="groups" :columns="['name']" :key-name="'id'">
|
||||
<template v-slot:default="{item}">
|
||||
<span>
|
||||
{{item.members.join(', ')}}
|
||||
</span>
|
||||
</template>
|
||||
</Table>
|
||||
<Matrix2D :items="groupPermissions" :columns="groupNames" :rows="permissionNames"/>
|
||||
<h3 class="text-center">Permissions</h3>
|
||||
<Matrix3D :items="userPermissions" :columns="userNames" :rows="permissionNames"
|
||||
:folded-dimension="eventsNames"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapState} from 'vuex';
|
||||
import Table from "@/components/Table.vue";
|
||||
import Matrix3D from "@/components/Matrix3D.vue";
|
||||
import Matrix2D from "@/components/Matrix2D.vue";
|
||||
|
||||
export default {
|
||||
name: 'AccessControl',
|
||||
components: {Matrix3D, Matrix2D, Table},
|
||||
computed: {
|
||||
...mapState(['users', 'groups', 'events']),
|
||||
permissionNames() {
|
||||
return this.groups.map(g => g.permissions).flat().map(p => p.split(":")[1])
|
||||
},
|
||||
groupNames() {
|
||||
return this.groups.map(g => g.name)
|
||||
},
|
||||
eventsNames() {
|
||||
return this.events.map(e => e.slug)
|
||||
},
|
||||
userNames() {
|
||||
return this.users.map(u => u.username)
|
||||
},
|
||||
groupPermissions() {
|
||||
return this.groups.map(g => this.permissionNames.map(p => g.permissions.includes("*:" + p)))
|
||||
},
|
||||
userPermissions() {
|
||||
return this.users.map(u => this.permissionNames.map(p => this.events.map(e =>
|
||||
u.permissions.includes("*:" + p) || u.permissions.includes(e.slug + ":" + p)
|
||||
)))
|
||||
},
|
||||
},
|
||||
methods: mapActions(['loadUsers', 'loadGroups']),
|
||||
mounted() {
|
||||
this.loadUsers();
|
||||
this.loadGroups();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -3,22 +3,23 @@
|
|||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<div class="card bg-dark text-light mb-2" id="filters">
|
||||
<h3 class="text-center">Admin</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{name: 'debug'}">Debug</router-link>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'boxes', params: {event: getEventSlug}}">Boxes</router-link>
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'events'}">Events</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{name: 'users'}">Users</router-link>
|
||||
<li class="nav-item">
|
||||
<router-link class="nav-link" :to="{name: 'users'}" active-class="active">Access Control</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,9 +27,11 @@
|
|||
|
||||
<script>
|
||||
import {mapGetters} from 'vuex';
|
||||
import Cards from "@/components/Cards.vue";
|
||||
|
||||
export default {
|
||||
name: 'Admin',
|
||||
components: {Cards},
|
||||
computed: {
|
||||
...mapGetters(['getEventSlug']),
|
||||
},
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<Table
|
||||
:columns="['cid', 'name','itemCount']"
|
||||
:items="loadedBoxes"
|
||||
|
@ -17,9 +14,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<div>
|
||||
<!--qr-code :text="qr_url" color="#000" bg-color="#fff" error-level="H" class="qr-code"></qr-code-->
|
||||
<h3 class="text-center">Events</h3>
|
||||
<!--p>{{ events }}</p-->
|
||||
|
@ -46,18 +44,15 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapState} from 'vuex';
|
||||
import Table from '@/components/Table';
|
||||
import Events from "@/views/Events.vue";
|
||||
|
||||
export default {
|
||||
name: 'Debug',
|
||||
components: {Events, Table},
|
||||
components: {Table},
|
||||
computed: {
|
||||
...mapState(['events', 'loadedItems', 'loadedBoxes', 'mails', 'issues', 'systemEvents']),
|
||||
qr_url() {
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<Table
|
||||
:columns="['slug', 'name']"
|
||||
:items="events"
|
||||
|
@ -19,9 +16,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<h3 class="text-center">Users</h3>
|
||||
<p>{{ users }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapState} from 'vuex';
|
||||
import Table from '@/components/Table';
|
||||
import Events from "@/views/Events.vue";
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
computed: mapState(['users']),
|
||||
methods: mapActions(['loadUsers']),
|
||||
mounted() {
|
||||
this.loadUsers();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
Loading…
Reference in a new issue