Browse Source

Merge branch 'feature/roles' of b12f/sas-php into develop

develop
Benjamin Bädorf 4 years ago
committed by Gogs
parent
commit
a0d9c19672
  1. 88
      api/app/Http/Controllers/RoleController.php
  2. 9
      api/app/Http/Controllers/StudentCouncilController.php
  3. 2
      api/app/Http/Requests/CreateFacultyRequest.php
  4. 30
      api/app/Http/Requests/CreateStudentCouncilRequest.php
  5. 30
      api/app/Http/Requests/DeleteRoleRequest.php
  6. 30
      api/app/Http/Requests/DeleteStudentCouncilRequest.php
  7. 30
      api/app/Http/Requests/ListRightsRequest.php
  8. 32
      api/app/Http/Requests/RoleRequest.php
  9. 2
      api/app/Http/Requests/UpdateFacultyRequest.php
  10. 30
      api/app/Http/Requests/UpdateStudentCouncilRequest.php
  11. 1
      api/app/Http/Resources/Role.php
  12. 6
      api/app/Models/Role.php
  13. 51
      api/app/Rules/RoleArray.php
  14. 1
      api/routes/api.php
  15. 26
      client/src/api/roles.js
  16. 1
      client/src/assets/scss/components/_list.scss
  17. 10
      client/src/assets/scss/components/_tag-area.scss
  18. 19
      client/src/auth.js
  19. 34
      client/src/components/TagArea.vue
  20. 5
      client/src/components/settings/faculties/Faculty.vue
  21. 1
      client/src/components/settings/faculties/index.vue
  22. 3
      client/src/components/settings/index.vue
  23. 104
      client/src/components/settings/roles/Role.vue
  24. 119
      client/src/components/settings/roles/index.vue
  25. 3
      client/src/main.js
  26. 2
      client/src/store/index.js
  27. 25
      client/src/store/role/Role.js
  28. 34
      client/src/store/role/actions.js
  29. 21
      client/src/store/role/index.js
  30. 22
      client/src/store/role/mutations.js

88
api/app/Http/Controllers/RoleController.php

@ -2,9 +2,95 @@
namespace App\Http\Controllers;
use App\Models\Role;
use App\Http\Resources\Role as RoleResource;
use Illuminate\Http\Request;
use App\Http\Requests\RoleRequest;
use App\Http\Requests\DeleteRoleRequest;
use App\Http\Requests\ListRightsRequest;
class RoleController extends Controller
{
//
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$roles = Role::all();
return RoleResource::collection($roles);
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function rights(ListRightsRequest $request)
{
return [
'data' => Role::$availableRights,
];
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(RoleRequest $request)
{
$role = new Role($request->all());
if(!$role->save()) {
throw new HttpException(500);
}
return (new RoleResource($role))
->response()
->setStatusCode(201);
}
/**
* Display the specified resource.
*
* @param \Role $role
* @return \Illuminate\Http\Response
*/
public function show(Role $role)
{
return new RoleResource($role);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \Role $role
* @return \Illuminate\Http\Response
*/
public function update(RoleRequest $request, Role $role)
{
$role->update($request->all());
if(!$role->save()) {
throw new HttpException(500);
}
return new RoleResource($role);
}
/**
* Remove the specified resource from storage.
*
* @param \Role $role
* @return \Illuminate\Http\Response
*/
public function destroy(DeleteRoleRequest $request, Role $role)
{
// TODO: map all users to null role
throw new HttpException(501, 'Not implemented. This needs to be done by hand so existing users don\'t end up without a role');
// if (!$role->destroy()) {
// }
return response(204);
}
}

9
api/app/Http/Controllers/StudentCouncilController.php

@ -6,6 +6,9 @@ use App\Models\StudentCouncil;
use App\Http\Resources\StudentCouncil as StudentCouncilResource;
use App\Http\Resources\StudentCouncils as StudentCouncilCollection;
use Illuminate\Http\Request;
use App\Http\Requests\CreateStudentCouncilRequest;
use App\Http\Requests\UpdateStudentCouncilRequest;
use App\Http\Requests\DeleteStudentCouncilRequest;
class StudentCouncilController extends Controller
{
@ -26,7 +29,7 @@ class StudentCouncilController extends Controller
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
public function store(CreateStudentCouncilRequest $request)
{
$studentCouncil = new StudentCouncil($request->all());
if(!$studentCouncil->save()) {
@ -55,7 +58,7 @@ class StudentCouncilController extends Controller
* @param \StudentCouncil $studentCouncil
* @return \Illuminate\Http\Response
*/
public function update(Request $request, StudentCouncil $studentCouncil)
public function update(UpdateStudentCouncilRequest $request, StudentCouncil $studentCouncil)
{
$studentCouncil->update($request->all());
if(!$studentCouncil->save()) {
@ -70,7 +73,7 @@ class StudentCouncilController extends Controller
* @param \StudentCouncil $studentCouncil
* @return \Illuminate\Http\Response
*/
public function destroy(StudentCouncil $studentCouncil)
public function destroy(DeleteStudentCouncilRequest $request, StudentCouncil $studentCouncil)
{
// TODO: faculty reassign
if (!$studentCouncil->destroy()) {

2
api/app/Http/Requests/CreateFacultyRequest.php

@ -13,7 +13,7 @@ class CreateFacultyRequest extends FormRequest
*/
public function authorize()
{
return \Auth::user()->role->hasRight('STUDENT_COUNCIL_WRITE');
return \Auth::user()->role->hasRight('FACULTY_WRITE');
}
/**

30
api/app/Http/Requests/CreateStudentCouncilRequest.php

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateStudentCouncilRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return \Auth::user()->role->hasRight('STUDENT_COUNCIL_WRITE');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|string',
];
}
}

30
api/app/Http/Requests/DeleteRoleRequest.php

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DeleteRoleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return \Auth::user()->role->hasRight('ROLES_ADMIN');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

30
api/app/Http/Requests/DeleteStudentCouncilRequest.php

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DeleteStudentCouncilRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return \Auth::user()->role->hasRight('STUDENT_COUNCIL_DELETE');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

30
api/app/Http/Requests/ListRightsRequest.php

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ListRightsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return \Auth::user()->role->hasRight('ROLES_ADMIN');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

32
api/app/Http/Requests/RoleRequest.php

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
use App\Rules\RoleArray;
use Illuminate\Foundation\Http\FormRequest;
class RoleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return \Auth::user()->role->hasRight('ROLES_ADMIN');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|string',
'rights'=> [new RoleArray],
];
}
}

2
api/app/Http/Requests/UpdateFacultyRequest.php

@ -13,7 +13,7 @@ class UpdateFacultyRequest extends FormRequest
*/
public function authorize()
{
return \Auth::user()->role->hasRight('STUDENT_COUNCIL_WRITE');
return \Auth::user()->role->hasRight('FACULTY_WRITE');
}
/**

30
api/app/Http/Requests/UpdateStudentCouncilRequest.php

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateStudentCouncilRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return \Auth::user()->role->hasRight('STUDENT_COUNCIL_WRITE');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|string',
];
}
}

1
api/app/Http/Resources/Role.php

@ -16,6 +16,7 @@ class Role extends JsonResource
{
return [
'id' => $this->id,
'name' => $this->name,
'rights' => $this->rights,
];
}

6
api/app/Models/Role.php

@ -57,7 +57,11 @@ class Role extends UuidModel
}
public function getRightsAttribute($rights) {
return explode(',', $rights);
$rights = explode(',', $rights);
if (count($rights) === 1 && $rights[0] === '') {
return [];
}
return $rights;
}
public function hasRight(string $right)

51
api/app/Rules/RoleArray.php

@ -0,0 +1,51 @@
<?php
namespace App\Rules;
use App\Models\Role;
use Illuminate\Contracts\Validation\Rule;
class RoleArray implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
if (!is_array($value)) {
return false;
}
foreach($value as $right) {
if (!in_array($right, Role::$availableRights)) {
return false;
}
}
return true;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'Rights must be an array of right strings';
}
}

1
api/routes/api.php

@ -79,6 +79,7 @@ Route::group(['middleware' => [ 'auth', 'csrf']], function() {
Route::delete('{studentCouncil}', 'StudentCouncilController@destroy');
});
Route::get('/rights', 'RoleController@rights');
Route::group(['prefix' => 'roles'], function() {
Route::get('/', 'RoleController@index');
Route::post('/', 'RoleController@store');

26
client/src/api/roles.js

@ -0,0 +1,26 @@
import axios from 'axios';
export async function getRoles() {
const res = await axios.get('roles');
return res.data.data;
}
export async function getRights() {
const res = await axios.get('rights');
return res.data.data;
}
export async function addRole(role) {
const res = await axios.post('roles', role);
return res.data.data;
}
export async function updateRole(role) {
const res = await axios.put(`roles/${role.id}`, role);
return res.data.data;
}
export async function deleteRole(role) {
const res = await axios.delete(`roles/${role.id}`);
return res.data.data;
}

1
client/src/assets/scss/components/_list.scss

@ -16,6 +16,7 @@
height: 30px;
padding: 0 15px;
transition: background-color 0.2s ease-out;
flex-shrink: 0;
&_medium {
height: 50px;

10
client/src/assets/scss/components/_tag-area.scss

@ -1,10 +1,6 @@
.tag-area {
display: block;
&__selected {
}
&__area {
min-height: 60px;
height: auto;
@ -26,4 +22,10 @@
outline: 0;
}
}
&__results {
max-height: 150px;
overflow-y: auto;
overflow-x: hidden;
}
}

19
client/src/auth.js

@ -0,0 +1,19 @@
import store from './store';
function hasRight(right) {
const user = store.getters.currentUser;
if (!user) {
return false;
}
return user.rights.indexOf(right) !== -1;
}
function hasRights(rights) {
return rights.reduce((total, right) => total && hasRight(right), true);
}
export default {
hasRight,
hasRights,
};

34
client/src/components/TagArea.vue

@ -41,15 +41,22 @@ export default {
},
computed: {
asObjects() {
return !!this.options[0].id;
},
selectedIds() {
return this.selected.map(s => s.id);
},
availableOptions() {
return this.options.filter(o => this.selectedIds.indexOf(o.id) === -1);
if (this.asObjects) {
return this.options.filter(o => this.selectedIds.indexOf(o.id) === -1);
}
return this.options.filter(o => this.selected.indexOf(o) === -1);
},
results() {
return this.availableOptions
.filter(option => option[this.searchField]
.filter(option => (this.asObjects ? option[this.searchField] : option)
.toLowerCase()
.indexOf(this.searchTerm)
!== -1
@ -58,6 +65,20 @@ export default {
},
methods: {
up() {
this.selectedResultIndex -= 1;
if (this.selectedResultIndex < 0) {
this.selectedResultIndex = this.results.length - 1;
}
},
down() {
this.selectedResultIndex += 1;
if (this.selectedResultIndex > this.results.length - 1) {
this.selectedResultIndex = 0;
}
},
select(element) {
this.selected.push(element);
this.$emit('change', this.selected);
@ -101,10 +122,11 @@ export default {
<a
href="#"
v-for="(tag, index) in selected"
:key="asObjects ? tag.id : tag"
class="tag-area__selected tag"
@click.prevent="deselect(index)"
>
{{ tag[searchField] }}
{{ asObjects ? tag[searchField] : tag }}
<fa-icon icon="minus" class="tag__delete"></fa-icon>
</a>
<input
@ -115,6 +137,8 @@ export default {
@keydown="search"
@keydown.enter.prevent="selectCurrent"
@keydown.delete="deselectLast"
@keydown.up="up"
@keydown.down="down"
:placeholder="placeholder"
/>
@ -127,12 +151,12 @@ export default {
<a
href="#"
v-for="(result, index) in results"
:key="result.id"
:key="asObjects ? result.id : result"
@click.prevent="select(result)"
class="list__item"
:class="{ list__item_selected: index === selectedResultIndex }"
>
{{ result[searchField] }}
{{ asObjects ? result[searchField] : result }}
</a>
</div>
</flyout>

5
client/src/components/settings/faculties/Faculty.vue

@ -14,6 +14,10 @@ export default {
methods: {
startEdit() {
if (!this.$auth.hasRight('FACULTY_WRITE')) {
return;
}
this.name = this.faculty.name;
this.number = this.faculty.number;
this.editing = true;
@ -48,6 +52,7 @@ export default {
<button
type="button"
class="btn btn_small"
v-if="$auth.hasRight('FACULTY_DELETE')"
>
<fa-icon icon="trash"></fa-icon>
</button>

1
client/src/components/settings/faculties/index.vue

@ -50,6 +50,7 @@ export default {
type="button"
class="btn btn_small"
@click="startAdd"
v-if="$auth.hasRight('FACULTY_WRITE')"
>
Hinzufügen
</button>

3
client/src/components/settings/index.vue

@ -2,12 +2,14 @@
import Faculties from './faculties'
import SeminarTypes from './seminar-types'
import StudentCouncils from './student-councils'
import Roles from './roles'
export default {
components: {
Faculties,
SeminarTypes,
StudentCouncils,
Roles,
},
};
@ -19,5 +21,6 @@ export default {
<seminar-types></seminar-types>
<faculties></faculties>
<student-councils></student-councils>
<roles v-if="$auth.hasRight('ROLES_ADMIN')"></roles>
</div>
</template>

104
client/src/components/settings/roles/Role.vue

@ -0,0 +1,104 @@
<script>
import Role from '@/store/role/Role';
import TagArea from '@/components/TagArea';
export default {
props: {
role: { type: Role, required: true },
},
components: {
TagArea,
},
data: () => ({
editing: false,
rights: [],
name: '',
}),
computed: {
availableRights() {
return this.$store.getters.rights;
},
},
methods: {
startEdit() {
this.name = this.role.name;
this.rights = this.role.rights.slice();
this.editing = true;
this.$nextTick(() => this.$refs.input.focus());
},
cancelEdit() {
this.editing = false;
},
save() {
this.$store.dispatch('updateRole', {
id: this.role.id,
name: this.name,
rights: this.rights,
});
this.editing = false;
},
},
};
</script>
<template>
<a
v-if="!editing"
href="#"
class="role list__item list__item_large"
@click.prevent="startEdit"
>
<div class="list__item-title">
{{ role.name }}
</div>
<div class="list__item-actions btn-group">
<button
type="button"
class="btn btn_small"
>
<fa-icon icon="trash"></fa-icon>
</button>
</div>
</a>
<form
v-else
@submit.prevent="save"
class="role list__item list__item_large"
>
<div class="list__item-title">
<input
class="input"
type="text"
placeholder="Name"
v-model="name"
ref="input"
/>
</div>
<div class="list__item-actions btn-group">
<button
type="submit"
class="btn btn_small"
>
<fa-icon icon="check"></fa-icon>
</button>
<button
type="button"
class="btn btn_small"
@click="cancelEdit"
>
<fa-icon icon="times"></fa-icon>
</button>
</div>
<div class="list__item-body">
<tag-area
v-model="rights"
:options="availableRights"
placeholder="Rechte auswählen"
></tag-area>
</div>
</form>
</template>

119
client/src/components/settings/roles/index.vue

@ -0,0 +1,119 @@
<script>
import Role from './Role';
import TagArea from '@/components/TagArea';
export default {
components: {
Role,
TagArea,
},
data: () => ({
adding: false,
rights: [],
name: '',
}),
created() {
this.$store.dispatch('getRoles');
this.$store.dispatch('getRights');
},
computed: {
roles() {
return this.$store.getters.roles;
},
availableRights() {
return this.$store.getters.rights;
},
},
methods: {
resetForm() {
this.rights = [];
this.name = '';
},
startAdd() {
this.resetForm();
this.adding = true;
this.$nextTick(() => this.$refs.input.focus());
},
stopAdd() {
this.adding = false;
},
save() {
this.$store.dispatch('addRole', {
rights: this.rights,
name: this.name,
});
this.adding = false;
},
},
};
</script>
<template>
<section class="settings-roles panel">
<div class="panel__heading">
<div class="panel__heading-title">Rollen</div>
<button
type="button"
class="btn btn_small"
@click="startAdd"
>
Hinzufügen
</button>
</div>
<div class="panel__body">
<p>
Rollen werden Benutzer zugewiesen. Jede Rolle hat Rechte, die bestimmen was ein Benutzer mit der jeweiligen Rolle im System tun darf.
</p>
<div class="list">
<form
@submit.prevent="save"
class="list__item list__item_large"
v-if="adding"
>
<div class="list__item-title">
<input
class="input"
type="text"
placeholder="Name"
ref="input"
v-model="name"
/>
</div>
<div class="list__item-actions btn-group">
<button
type="submit"
class="btn btn_small"
>
<fa-icon icon="check"></fa-icon>
</button>
<button
type="button"
class="btn btn_small"
@click="stopAdd"
>
<fa-icon icon="times"></fa-icon>
</button>
</div>
<div class="list__item-body">
<tag-area
v-model="rights"
:options="availableRights"
placeholder="Rechte auswählen"
></tag-area>
</div>
</form>
<role
v-for="role in roles"
:key="role.id"
:role="role"
></role>
</div>
</div>
</section>
</template>

3
client/src/main.js

@ -16,6 +16,7 @@ import VueAxios from 'vue-axios';
import App from './App';
import router from './router/index';
import store from './store';
import auth from './auth';
moment.locale('de');
@ -33,6 +34,8 @@ Vue.config.productionTip = false;
Vue.component('fa-icon', FontAwesomeIcon);
Vue.prototype.$auth = auth;
/* eslint-disable no-new */
new Vue({
el: '#app',

2
client/src/store/index.js

@ -7,6 +7,7 @@ import user from './user';
import seminar from './seminar';
import seminarType from './seminar-type';
import faculty from './faculty';
import role from './role';
import studentCouncil from './student-council';
import academicYear from './academic-year';
@ -34,6 +35,7 @@ export default new Vuex.Store({
seminar,
seminarType,
faculty,
role,
studentCouncil,
academicYear,
},

25
client/src/store/role/Role.js

@ -0,0 +1,25 @@
import store from '@store/index';
export default class Role {
constructor(
{
id,
name,
rights,
},
) {
this.id = id;
this.name = name;
this.rights = rights;
}
update(
{
name,
rights,
},
) {
this.name = name;
this.rights = rights;
}
}

34
client/src/store/role/actions.js

@ -0,0 +1,34 @@
import * as api from '@api/roles';
import Role from './Role';
export async function getRoles({ commit }) {
const response = await api.getRoles();
for (const roleData of response) {
commit('UPDATE_OR_CREATE_ROLE', new Role(roleData));
}
}
export async function getRights({ commit }) {
const response = await api.getRights();
commit('SET_RIGHTS', response);
}
export async function addRole({ commit }, role) {
const data = await api.addRole(role);
commit('UPDATE_OR_CREATE_ROLE', new Role(data));
return data;
}
export async function updateRole({ commit }, role) {
const data = await api.updateRole(role);
commit('UPDATE_OR_CREATE_ROLE', new Role(data));
return data;
}
export async function deleteRole({ commit }, role) {
const data = await api.deleteRole(role);
if (data) {
commit('REMOVE_ROLE', role);
}
}

21
client/src/store/role/index.js

@ -0,0 +1,21 @@
import * as actions from './actions';
import mutations from './mutations';
const baseState = {
roles: [],
rights: [],
};
export const getters = {
roles: state => state.roles,
rolesForStudentCouncil: state => id => state.roles.filter(role => role.studentCouncilId === id),
role: state => id => state.roles.find(role => role.id === id),
rights: state => state.rights,
};
export default {
state: baseState,
getters,
actions,
mutations,
};

22
client/src/store/role/mutations.js

@ -0,0 +1,22 @@
import Role from './Role';
import { updateOrCreate, guardModel, remove } from './../helper';
const mutations = {
UPDATE_OR_CREATE_ROLE(state, role) {
guardModel(role, Role);
updateOrCreate(state.roles, role);
},
REMOVE_ROLE(state, role) {
guardModel(role, Role);
remove(state.roles, role);
},
SET_RIGHTS(state, rights) {
state.rights = rights;
},
};
export default mutations;
Loading…
Cancel
Save