This commit is contained in:
j3d1 2023-12-16 20:11:00 +01:00
parent 81f1f97a6b
commit 674106a8a5
16 changed files with 346 additions and 208 deletions

View file

@ -7,6 +7,7 @@ from django.contrib.auth import login
from django.urls import path from django.urls import path
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.contrib.auth.models import Group
from knox.models import AuthToken from knox.models import AuthToken
from knox.views import LoginView as KnoxLoginView from knox.views import LoginView as KnoxLoginView
@ -36,6 +37,26 @@ class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer serializer_class = UserSerializer
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']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def selfUser(request): def selfUser(request):
@ -85,6 +106,7 @@ class LoginView(KnoxLoginView):
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'users', UserViewSet, basename='users') router.register(r'users', UserViewSet, basename='users')
router.register(r'groups', GroupViewSet, basename='groups')
urlpatterns = router.urls + [ urlpatterns = router.urls + [
path('self/', selfUser), path('self/', selfUser),

View file

@ -132,3 +132,49 @@ class UserApiTest(TestCase):
content_type='application/json') content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue('token' in response.json()) 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')

View file

@ -24,7 +24,7 @@
</router-link> </router-link>
</li> </li>
<li class="nav-item" v-if="checkPermission(getEventSlug, 'delete_event')"> <li class="nav-item" v-if="checkPermission(getEventSlug, 'delete_event')">
<router-link :to="{name: 'admin'}" :class="['nav-link', { active: getActiveView === 'admin' }]"> <router-link :to="{name: 'admin'}" class="nav-link" active-class="active">
Admin Admin
</router-link> </router-link>
</li> </li>

View file

@ -0,0 +1,72 @@
<template>
<table class="table table-striped table-dark">
<thead>
<tr>
<th scope="col" v-for="(column, index) in columns" :key="index">
<div class="input-group">
<div class="input-group-prepend">
<button
:class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
@click="toggleSort(column)"
>
{{ column }}
<span :class="{ 'text-info': column === sortBy }">
<font-awesome-icon :icon="getSortIcon(column)"/>
</span>
</button>
</div>
<input
type="text"
class="form-control"
placeholder="filter"
:value="filters[column]"
@input="changeFilter(column, $event.target.value)"
>
</div>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="item in internalItems" :key="item[keyName]" @click="$emit('itemActivated', item)">
<td v-for="(column, index) in columns" :key="index">{{ item[column] }}</td>
<td>
<slot v-bind:item="item"/>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import DataContainer from '@/mixins/data-container';
import router from '../router';
export default {
name: 'Table3D',
props: {
items: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
},
rows: {
type: Array,
required: true
},
foldedDimension: {
type: Array,
required: true
}
},
};
</script>
<style>
.table-body-move {
transition: transform 1s;
}
</style>

View file

@ -25,8 +25,8 @@
</span> </span>
<div class="new-comment"> <div class="new-comment">
<div class="input-group"> <div class="input-group">
<input type="text" placeholder="Add a comment..." v-model="newMail"> <input type="text" placeholder="reply mail..." v-model="newMail">
<button class="btn" @click="sendMail"> <button class="btn btn-primary" @click="sendMail">
Send Send
</button> </button>
</div> </div>

View file

@ -13,13 +13,13 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p><span v-html="body"></span></p> <p><span v-html="body"></span></p>
<button class="button">👏 2</button> <!--button class="button">👏 2</button>
<button class="button | square"> <button class="button | square">
<font-awesome-icon icon="user"/> <font-awesome-icon icon="user"/>
</button> </button-->
</div> </div>
</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" <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" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
@ -38,7 +38,7 @@
<font-awesome-icon icon="user"/> <font-awesome-icon icon="user"/>
</i> </i>
</span> </span>
</button> </button-->
</div> </div>
</template> </template>

View file

@ -34,14 +34,16 @@ import {
faComment, faComment,
faEnvelope, faEnvelope,
faUser, faUser,
faComments faComments,
faArchive,
} 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'; 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);
Vue.component('font-awesome-icon', FontAwesomeIcon); Vue.component('font-awesome-icon', FontAwesomeIcon);
//import VueQRCodeComponent from 'vue-qrcode-component' //import VueQRCodeComponent from 'vue-qrcode-component'

View file

@ -1,7 +1,6 @@
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 Events from './views/Events';
import Error from './views/Error'; import Error from './views/Error';
import HowTo from './views/HowTo'; import HowTo from './views/HowTo';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
@ -14,36 +13,49 @@ 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 store from "@/store";
import Empty from "@/views/Empty.vue"; import Empty from "@/views/Empty.vue";
import Events from "@/views/admin/Events.vue";
import AccessControl from "@/views/admin/AccessControl.vue";
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
{path: '/', redirect: '/Camp23/items', meta: {requiresAuth: false}}, {path: '/', redirect: '/Camp23/items', meta: {requiresAuth: false}},
{path: '/login', name: 'login', component: Login, meta: {requiresAuth: false}}, {path: '/login/', name: 'login', component: Login, meta: {requiresAuth: false}},
{path: '/register', name: 'register', component: Register, meta: {requiresAuth: false}}, {path: '/register/', name: 'register', component: Register, meta: {requiresAuth: false}},
{path: '/howto', name: 'howto', component: HowTo, meta: {requiresAuth: true}}, {path: '/howto/', name: 'howto', component: HowTo, meta: {requiresAuth: true}},
{path: '/:event/items', name: 'items', component: Items, meta: {path: '/:event/items/', name: 'items', component: Items, meta:
{requiresAuth: true, requiresPermission: 'view_item'}}, {requiresAuth: true, requiresPermission: 'view_item'}},
{path: '/:event/item/:uid', name: 'item', component: Items, meta: {path: '/:event/item/:uid/', name: 'item', component: Items, meta:
{requiresAuth: true, requiresPermission: 'view_item'}}, {requiresAuth: true, requiresPermission: 'view_item'}},
{path: '/:event/boxes', name: 'boxes', component: Boxes, meta: {path: '/:event/boxes/', name: 'boxes', component: Boxes, meta:
{requiresAuth: true, requiresPermission: 'view_container'}}, {requiresAuth: true, requiresPermission: 'view_container'}},
{path: '/:event/box/:uid', name: 'box', component: Boxes, meta: {path: '/:event/box/:uid/', name: 'box', component: Boxes, meta:
{requiresAuth: true, requiresPermission: 'view_container'}}, {requiresAuth: true, requiresPermission: 'view_container'}},
{path: '/:event/tickets', name: 'tickets', component: Tickets, meta: {path: '/:event/tickets/', name: 'tickets', component: Tickets, meta:
{requiresAuth: true, requiresPermission: 'view_issuethread'}}, {requiresAuth: true, requiresPermission: 'view_issuethread'}},
{path: '/:event/ticket/:id', name: 'ticket', component: Ticket, meta: {path: '/:event/ticket/:id/', name: 'ticket', component: Ticket, meta:
{requiresAuth: true, requiresPermission: 'view_issuethread'}}, {requiresAuth: true, requiresPermission: 'view_issuethread'}},
{path: '/admin', name: 'admin', component: Admin, meta: {path: '/admin/', component: Admin, meta:
{requiresAuth: true, requiresPermission: 'delete_event'}}, {requiresAuth: true, requiresPermission: 'delete_event'},
{path: '/admin/files', name: 'files', component: Files, meta: children: [
{requiresAuth: true, requiresPermission: 'delete_event'}}, {
{path: '/admin/events', name: 'events', component: Events, meta: path: 'files/', name: 'files', component: Files, meta:
{requiresAuth: true, requiresPermission: 'delete_event'}}, {requiresAuth: true, requiresPermission: 'delete_event'}
{path: '/admin/debug', name: 'debug', component: Debug, meta: },
{requiresAuth: true, requiresPermission: 'delete_event'}}, {
{path: '/admin/users', name: 'users', component: Events, meta: path: 'events/', name: 'events', component: Events, meta:
{requiresAuth: true, requiresPermission: 'delete_event'}}, {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: '/user', name: 'user', component: Empty, meta: {requiresAuth: true}},
{path: '*', component: Error}, {path: '*', component: Error},
]; ];

View file

@ -67,10 +67,13 @@ const store = new Vuex.Store({
loadedBoxes: [], loadedBoxes: [],
toasts: [], toasts: [],
tickets: [], tickets: [],
users: [],
groups: [],
lastEvent: localStorage.getItem('lf_lastEvent') || '36C3', lastEvent: localStorage.getItem('lf_lastEvent') || '36C3',
lastUsed: JSON.parse(localStorage.getItem('lf_lastUsed') || '{}'), lastUsed: JSON.parse(localStorage.getItem('lf_lastUsed') || '{}'),
remember: false, remember: false,
user: null, user: null,
password: null,
userPermissions: [], userPermissions: [],
token: null, token: null,
token_expiry: null, token_expiry: null,
@ -136,6 +139,12 @@ const store = new Vuex.Store({
replaceTickets(state, tickets) { replaceTickets(state, tickets) {
state.tickets = tickets; state.tickets = tickets;
}, },
replaceUsers(state, users) {
state.users = users;
},
replaceGroups(state, groups) {
state.groups = groups;
},
updateTicket(state, updatedTicket) { updateTicket(state, updatedTicket) {
const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0]; const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0];
Object.assign(ticket, updatedTicket); Object.assign(ticket, updatedTicket);
@ -193,6 +202,7 @@ const store = new Vuex.Store({
if (data.token) { if (data.token) {
commit('setToken', data); commit('setToken', data);
commit('setUser', username); commit('setUser', username);
commit('setPassword', password);
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`; axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
dispatch('afterLogin'); dispatch('afterLogin');
return true; return true;
@ -209,7 +219,7 @@ const store = new Vuex.Store({
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.token}), body: JSON.stringify({username: state.user, password: state.password}),
credentials: 'omit' credentials: 'omit'
}).then(r => r.json()) }).then(r => r.json())
if (data.token) { if (data.token) {
@ -229,6 +239,7 @@ const store = new Vuex.Store({
const items = dispatch('loadEventItems'); const items = dispatch('loadEventItems');
const tickets = dispatch('loadTickets'); const tickets = dispatch('loadTickets');
const user = dispatch('loadUserInfo'); const user = dispatch('loadUserInfo');
await Promise.all([boxes, items, tickets, user]);
}, },
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.token}`}});
@ -301,7 +312,15 @@ const store = new Vuex.Store({
async sendMail({commit, dispatch}, {id, message}) { async sendMail({commit, dispatch}, {id, message}) {
const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message}); const {data} = await axios.post(`/2/tickets/${id}/reply/`, {message});
await dispatch('loadTickets'); 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);
},
} }
}); });

View file

@ -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>

View file

@ -0,0 +1,49 @@
<template>
<div>
<h3 class="text-center">Users</h3>
<ul>
<Table :items="users" :columns="['username']" :key-name="'id'">
<template v-slot:default="{item}">
<span>_</span>
</template>
</Table>
</ul>
<h3 class="text-center">Groups</h3>
<ul>
<Table3D :items="groupPermissions" :columns="groups" :rows="permissions" :folded-dimension="events">
<template v-slot:default="{item}">
<input type="checkbox" v-model="item" @click="console.log(item)"/>
</template>
</Table3D>
</ul>
</div>
</template>
<script>
import {mapActions, mapState} from 'vuex';
import Table from "@/components/Table.vue";
import Table3D from "@/components/Table3D.vue";
export default {
name: 'AccessControl',
components: {Table3D, Table},
computed: {
...mapState(['users', 'groups', 'events']),
permissions() {
return ['test']
},
groupPermissions() {
return [[{name: 'test', active: true}], [{name: 'test', active: true}], [{name: 'test', active: true}]]
}
},
methods: mapActions(['loadUsers', 'loadGroups']),
mounted() {
this.loadUsers();
this.loadGroups();
}
};
</script>
<style scoped>
</style>

View file

@ -3,22 +3,23 @@
<div class="row"> <div class="row">
<div class="col-xl-8 offset-xl-2"> <div class="col-xl-8 offset-xl-2">
<div class="card bg-dark text-light mb-2" id="filters"> <div class="card bg-dark text-light mb-2" id="filters">
<h3 class="text-center">Admin</h3> <div class="card-header">
<ul> <ul class="nav nav-tabs card-header-tabs">
<li> <li class="nav-item">
<router-link :to="{name: 'debug'}">Debug</router-link> <router-link class="nav-link" :to="{name: 'admin'}" active-class="active" exact>Dashboard</router-link>
</li> </li>
<li> <li class="nav-item">
<router-link :to="{name: 'boxes', params: {event: getEventSlug}}">Boxes</router-link> <router-link class="nav-link" :to="{name: 'events'}" active-class="active">Events</router-link>
</li> </li>
<li> <li class="nav-item">
<router-link :to="{name: 'events'}">Events</router-link> <router-link class="nav-link" :to="{name: 'users'}" active-class="active">Access Control</router-link>
</li>
<li>
<router-link :to="{name: 'users'}">Users</router-link>
</li> </li>
</ul> </ul>
</div> </div>
<div class="card-body">
<router-view></router-view>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -26,9 +27,11 @@
<script> <script>
import {mapGetters} from 'vuex'; import {mapGetters} from 'vuex';
import Cards from "@/components/Cards.vue";
export default { export default {
name: 'Admin', name: 'Admin',
components: {Cards},
computed: { computed: {
...mapGetters(['getEventSlug']), ...mapGetters(['getEventSlug']),
}, },

View file

@ -1,7 +1,4 @@
<template> <template>
<div class="container-fluid px-xl-5 mt-3">
<div class="row">
<div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['cid', 'name','itemCount']" :columns="['cid', 'name','itemCount']"
:items="loadedBoxes" :items="loadedBoxes"
@ -17,9 +14,6 @@
</button> </button>
</div> </div>
</Table> </Table>
</div>
</div>
</div>
</template> </template>
<script> <script>

View file

@ -1,7 +1,5 @@
<template> <template>
<div class="container-fluid px-xl-5 mt-3"> <div>
<div class="row">
<div class="col-xl-8 offset-xl-2">
<!--qr-code :text="qr_url" color="#000" bg-color="#fff" error-level="H" class="qr-code"></qr-code--> <!--qr-code :text="qr_url" color="#000" bg-color="#fff" error-level="H" class="qr-code"></qr-code-->
<h3 class="text-center">Events</h3> <h3 class="text-center">Events</h3>
<!--p>{{ events }}</p--> <!--p>{{ events }}</p-->
@ -46,18 +44,15 @@
</li> </li>
</ul> </ul>
</div> </div>
</div>
</div>
</template> </template>
<script> <script>
import {mapActions, mapState} from 'vuex'; import {mapActions, mapState} from 'vuex';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Events from "@/views/Events.vue";
export default { export default {
name: 'Debug', name: 'Debug',
components: {Events, Table}, components: {Table},
computed: { computed: {
...mapState(['events', 'loadedItems', 'loadedBoxes', 'mails', 'issues', 'systemEvents']), ...mapState(['events', 'loadedItems', 'loadedBoxes', 'mails', 'issues', 'systemEvents']),
qr_url() { qr_url() {

View file

@ -1,7 +1,4 @@
<template> <template>
<div class="container-fluid px-xl-5 mt-3">
<div class="row">
<div class="col-xl-8 offset-xl-2">
<Table <Table
:columns="['slug', 'name']" :columns="['slug', 'name']"
:items="events" :items="events"
@ -19,9 +16,6 @@
</button> </button>
</div> </div>
</Table> </Table>
</div>
</div>
</div>
</template> </template>
<script> <script>

View file

@ -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>