This commit is contained in:
j3d1 2023-12-16 22:35:48 +01:00
parent 674106a8a5
commit b8c3bcfa3b
8 changed files with 170 additions and 107 deletions

View file

@ -16,11 +16,12 @@ from authentication.models import ExtendedUser
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField() permissions = serializers.SerializerMethodField()
groups = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name')
class Meta: class Meta:
model = ExtendedUser model = ExtendedUser
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions') fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
read_only_fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions') read_only_fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
def get_permissions(self, obj): def get_permissions(self, obj):
return list(set(obj.get_permissions())) return list(set(obj.get_permissions()))

View file

@ -38,10 +38,13 @@ class UserApiTest(TestCase):
self.assertEqual(response.json()[0]['first_name'], '') self.assertEqual(response.json()[0]['first_name'], '')
self.assertEqual(response.json()[0]['last_name'], '') self.assertEqual(response.json()[0]['last_name'], '')
self.assertEqual(response.json()[0]['id'], 1) 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]['username'], 'testuser')
self.assertEqual(response.json()[1]['email'], 'test') self.assertEqual(response.json()[1]['email'], 'test')
self.assertEqual(response.json()[1]['first_name'], '') self.assertEqual(response.json()[1]['first_name'], '')
self.assertEqual(response.json()[1]['last_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): def test_self_user(self):
response = self.client.get('/api/2/self/') response = self.client.get('/api/2/self/')

View file

@ -0,0 +1,45 @@
<template>
<table class="table table-striped table-dark">
<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>

View file

@ -0,0 +1,61 @@
<template>
<table class="table table-striped table-dark">
<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>

View file

@ -1,72 +0,0 @@
<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

@ -36,6 +36,7 @@ import {
faUser, faUser,
faComments, faComments,
faArchive, faArchive,
faMinus,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
@ -43,7 +44,7 @@ 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); faArchive, faMinus);
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

@ -167,6 +167,9 @@ const store = new Vuex.Store({
if (user) if (user)
localStorage.setItem('user', user); localStorage.setItem('user', user);
}, },
setPassword(state, password) {
state.password = password;
},
setPermissions(state, permissions) { setPermissions(state, permissions) {
state.userPermissions = permissions; state.userPermissions = permissions;
if (permissions) if (permissions)
@ -186,6 +189,7 @@ const store = new Vuex.Store({
localStorage.removeItem('permissions'); localStorage.removeItem('permissions');
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('token_expiry'); localStorage.removeItem('token_expiry');
if (router.currentRoute.name !== 'login')
router.push('/login'); router.push('/login');
}, },
}, },
@ -216,6 +220,7 @@ const store = new Vuex.Store({
}, },
async reloadToken({commit, state}) { async reloadToken({commit, state}) {
try { try {
if (data.password) {
const data = await fetch('/api/2/login/', { const data = await fetch('/api/2/login/', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -227,12 +232,12 @@ const store = new Vuex.Store({
axios.defaults.headers.common['Authorization'] = `Token ${data.token}`; axios.defaults.headers.common['Authorization'] = `Token ${data.token}`;
return true; return true;
} }
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
//credentials failed, logout //credentials failed, logout
store.commit('logout'); store.commit('logout');
router.push('/login');
}, },
async afterLogin({dispatch}) { async afterLogin({dispatch}) {
const boxes = dispatch('loadBoxes'); const boxes = dispatch('loadBoxes');

View file

@ -1,40 +1,59 @@
<template> <template>
<div> <div>
<h3 class="text-center">Users</h3> <h3 class="text-center">Users</h3>
<ul>
<Table :items="users" :columns="['username']" :key-name="'id'"> <Table :items="users" :columns="['username']" :key-name="'id'">
<template v-slot:default="{item}"> <template v-slot:default="{item}">
<span>_</span> <span>
{{item.groups.join(', ')}}
</span>
</template> </template>
</Table> </Table>
</ul>
<h3 class="text-center">Groups</h3> <h3 class="text-center">Groups</h3>
<ul> <Table :items="groups" :columns="['name']" :key-name="'id'">
<Table3D :items="groupPermissions" :columns="groups" :rows="permissions" :folded-dimension="events">
<template v-slot:default="{item}"> <template v-slot:default="{item}">
<input type="checkbox" v-model="item" @click="console.log(item)"/> <span>
{{item.members.join(', ')}}
</span>
</template> </template>
</Table3D> </Table>
</ul> <Matrix2D :items="groupPermissions" :columns="groupNames" :rows="permissionNames"/>
<h3 class="text-center">Permissions</h3>
<Matrix3D :items="userPermissions" :columns="userNames" :rows="permissionNames"
:folded-dimension="eventsNames"/>
</div> </div>
</template> </template>
<script> <script>
import {mapActions, mapState} from 'vuex'; import {mapActions, mapState} from 'vuex';
import Table from "@/components/Table.vue"; import Table from "@/components/Table.vue";
import Table3D from "@/components/Table3D.vue"; import Matrix3D from "@/components/Matrix3D.vue";
import Matrix2D from "@/components/Matrix2D.vue";
export default { export default {
name: 'AccessControl', name: 'AccessControl',
components: {Table3D, Table}, components: {Matrix3D, Matrix2D, Table},
computed: { computed: {
...mapState(['users', 'groups', 'events']), ...mapState(['users', 'groups', 'events']),
permissions() { permissionNames() {
return ['test'] 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() { groupPermissions() {
return [[{name: 'test', active: true}], [{name: 'test', active: true}], [{name: 'test', active: true}]] 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']), methods: mapActions(['loadUsers', 'loadGroups']),
mounted() { mounted() {