diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index c72e27b9b6..08bbd0165e 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -32,13 +32,19 @@ #[ApiResource( operations: [ new Get( - security: 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_PROTOTYPE", object)' + security: 'is_granted("CHECKLIST_IS_PROTOTYPE", object) or + is_granted("CAMP_IS_PROTOTYPE", object) or + is_granted("CAMP_COLLABORATOR", object) + ' ), new Patch( - security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' + security: '(is_granted("CHECKLIST_IS_PROTOTYPE", object) and is_granted("ROLE_ADMIN")) or + (is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)) + ' ), new Delete( - security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', + security: '(is_granted("CHECKLIST_IS_PROTOTYPE", object) and is_granted("ROLE_ADMIN")) or + (is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object))', validate: true, validationContext: ['groups' => ['delete']], ), @@ -47,7 +53,9 @@ ), new Post( denormalizationContext: ['groups' => ['write', 'create']], - securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object) or object.checklist === null' + securityPostDenormalize: '(is_granted("CHECKLIST_IS_PROTOTYPE", object) and is_granted("ROLE_ADMIN")) or + (!is_granted("CHECKLIST_IS_PROTOTYPE", object) and (is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object) or object.checklist === null)) + ' ), new GetCollection( uriTemplate: self::CHECKLIST_SUBRESOURCE_URI_TEMPLATE, @@ -55,7 +63,9 @@ 'checklistId' => new Link( fromClass: Checklist::class, toProperty: 'checklist', - security: 'is_granted("CAMP_COLLABORATOR", checklist) or is_granted("CAMP_IS_PROTOTYPE", checklist)' + security: 'is_granted("CHECKLIST_IS_PROTOTYPE", checklist) or + is_granted("CAMP_IS_PROTOTYPE", checklist) or + is_granted("CAMP_COLLABORATOR", checklist)' ), ], ), diff --git a/api/src/Security/Voter/ChecklistIsPrototypeVoter.php b/api/src/Security/Voter/ChecklistIsPrototypeVoter.php index 05441ada4a..ec7f4c49b3 100644 --- a/api/src/Security/Voter/ChecklistIsPrototypeVoter.php +++ b/api/src/Security/Voter/ChecklistIsPrototypeVoter.php @@ -3,22 +3,33 @@ namespace App\Security\Voter; use App\Entity\Checklist; +use App\Entity\ChecklistItem; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** - * @extends Voter + * @extends Voter */ class ChecklistIsPrototypeVoter extends Voter { public function __construct() {} protected function supports($attribute, $subject): bool { - return 'CHECKLIST_IS_PROTOTYPE' === $attribute && $subject instanceof Checklist; + return 'CHECKLIST_IS_PROTOTYPE' === $attribute + && ($subject instanceof Checklist || $subject instanceof ChecklistItem); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { - /** @var Checklist $checklist */ - $checklist = $subject; + if ($subject instanceof Checklist) { + $checklist = $subject; + } + + if ($subject instanceof ChecklistItem) { + $checklist = $subject->checklist; + } + + if (!$checklist) { + return false; + } return $checklist->isPrototype; } diff --git a/frontend/src/components/campAdmin/DialogCategoryCreate.vue b/frontend/src/components/campAdmin/DialogCategoryCreate.vue index fab2603455..d12377e6ed 100644 --- a/frontend/src/components/campAdmin/DialogCategoryCreate.vue +++ b/frontend/src/components/campAdmin/DialogCategoryCreate.vue @@ -263,12 +263,12 @@ export default { url = url.substring(window.location.origin.length) const match = router.matcher.match(url) - if (match.name === 'activity') { + if (match.name === 'camp/activity') { const scheduleEntry = await this.api .get() .scheduleEntries({ id: match.params['scheduleEntryId'] }) return await scheduleEntry.activity() - } else if (match.name === 'admin/activity/category') { + } else if (match.name === 'camp/admin/activity/category') { return await this.api.get().categories({ id: match.params['categoryId'] }) } } diff --git a/frontend/src/components/campAdmin/ErrorExistingActivitiesList.vue b/frontend/src/components/campAdmin/ErrorExistingActivitiesList.vue index dcc1ed139a..b594587211 100644 --- a/frontend/src/components/campAdmin/ErrorExistingActivitiesList.vue +++ b/frontend/src/components/campAdmin/ErrorExistingActivitiesList.vue @@ -13,7 +13,7 @@ > - - - - - - + + + + + + diff --git a/frontend/src/components/dashboard/ActivityRow.vue b/frontend/src/components/dashboard/ActivityRow.vue index c62093df91..f1123a24af 100644 --- a/frontend/src/components/dashboard/ActivityRow.vue +++ b/frontend/src/components/dashboard/ActivityRow.vue @@ -136,7 +136,7 @@ export default { }, routerLink() { return { - name: 'activity', + name: 'camp/activity', params: { campId: this.scheduleEntry.period().camp().id, scheduleEntryId: this.scheduleEntry.id, diff --git a/frontend/src/components/navigation/UserMeta.vue b/frontend/src/components/navigation/UserMeta.vue index e806a019b7..e2ca0b4b10 100644 --- a/frontend/src/components/navigation/UserMeta.vue +++ b/frontend/src/components/navigation/UserMeta.vue @@ -97,6 +97,17 @@ + + mdi-coffee + {{ $tc('components.navigation.userMeta.admin') }} + import('./views/dev/Debug.vue'), + default: () => import('./views/admin/Debug.vue'), }, }, + { + path: '/admin', + beforeEnter: all([requireAuth, requireAdmin]), + components: { + navigation: NavigationDefault, + default: GenericPage, + aside: () => import('./views/admin/SideBarAdmin.vue'), + }, + children: [ + { + path: '', + redirect: 'debug', + }, + { + path: 'debug', + name: 'admin/debug', + components: { + default: () => import('./views/admin/Debug.vue'), + }, + }, + ...(getEnv().FEATURE_CHECKLIST + ? [ + { + path: 'checklists', + name: 'admin/checklists', + components: { + default: () => import('./views/admin/Checklists.vue'), + }, + }, + { + path: 'checklist/:checklistId/:checklistName?', + name: 'admin/checklists/checklist', + components: { + default: () => import('./views/admin/Checklist.vue'), + }, + props: { + default: (route) => ({ + checklist: checklistFromRoute(route), + }), + }, + }, + ] + : []), + ], + }, + { path: '/register', name: 'register', @@ -272,12 +318,12 @@ export default new Router({ ], }, { - name: 'material/all', + name: 'camp/material/all', path: '/camps/:campId/:campShortTitle?/material/all', components: { navigation: NavigationCamp, - default: () => import('./views/material/MaterialOverview.vue'), - aside: () => import('./views/material/SideBarMaterialLists.vue'), + default: () => import('./views/camp/material/MaterialOverview.vue'), + aside: () => import('./views/camp/material/SideBarMaterialLists.vue'), }, beforeEnter: all([requireAuth, requireCamp]), props: { @@ -289,11 +335,11 @@ export default new Router({ }, }, { - name: 'material/lists', // Only used on mobile + name: 'camp/material/lists', // Only used on mobile path: '/camps/:campId/:campShortTitle?/material/lists', components: { navigation: NavigationCamp, - default: () => import('./views/material/MaterialLists.vue'), + default: () => import('./views/camp/material/MaterialLists.vue'), }, beforeEnter: all([requireAuth, requireCamp]), props: { @@ -304,12 +350,12 @@ export default new Router({ }, }, { - name: 'material/detail', + name: 'camp/material/detail', path: '/camps/:campId/:campShortTitle?/material/:materialId/:materialName?', components: { navigation: NavigationCamp, - default: () => import('./views/material/MaterialDetail.vue'), - aside: () => import('./views/material/SideBarMaterialLists.vue'), + default: () => import('./views/camp/material/MaterialDetail.vue'), + aside: () => import('./views/camp/material/SideBarMaterialLists.vue'), }, beforeEnter: all([requireAuth, requireCamp, requireMaterialList]), props: { @@ -322,12 +368,12 @@ export default new Router({ }, }, { - name: 'admin/activity/category', + name: 'camp/admin/activity/category', path: '/camps/:campId/:campShortTitle?/category/:categoryId/:categoryName?', components: { navigation: NavigationCamp, - default: () => import('./views/category/Category.vue'), - aside: () => import('./views/category/SideBarCategory.vue'), + default: () => import('./views/camp/category/Category.vue'), + aside: () => import('./views/camp/category/SideBarCategory.vue'), }, beforeEnter: all([requireAuth, requireCamp, requireCategory]), props: { @@ -343,12 +389,12 @@ export default new Router({ ? [ // Checklist-Pages: { - name: 'admin/checklists/checklist', + name: 'camp/admin/checklists/checklist', path: '/camps/:campId/:campTitle?/admin/checklist/:checklistId/:checklistName?', components: { navigation: NavigationCamp, - default: () => import('./views/checklist/Checklist.vue'), - aside: () => import('./views/checklist/SideBarChecklist.vue'), + default: () => import('./views/camp/checklist/Checklist.vue'), + aside: () => import('./views/camp/checklist/SideBarChecklist.vue'), }, beforeEnter: all([requireAuth, requireCamp, requireChecklist]), props: { @@ -367,7 +413,7 @@ export default new Router({ components: { navigation: NavigationCamp, default: GenericPage, - aside: () => import('./views/admin/SideBarAdmin.vue'), + aside: () => import('./views/camp/admin/SideBarAdmin.vue'), }, beforeEnter: all([requireAuth, requireCamp]), props: { @@ -381,31 +427,31 @@ export default new Router({ children: [ { path: 'info', - name: 'admin/info', - component: () => import('./views/admin/Info.vue'), + name: 'camp/admin/info', + component: () => import('./views/camp/admin/Info.vue'), props: (route) => ({ camp: campFromRoute(route) }), }, { path: 'activity', - name: 'admin/activity', - component: () => import('./views/admin/Activity.vue'), + name: 'camp/admin/activity', + component: () => import('./views/camp/admin/Activity.vue'), props: (route) => ({ camp: campFromRoute(route) }), }, { path: 'collaborators', - name: 'admin/collaborators', - component: () => import('./views/admin/Collaborators.vue'), + name: 'camp/admin/collaborators', + component: () => import('./views/camp/admin/Collaborators.vue'), props: () => ({ layout: 'normal' }), }, { path: 'material', - name: 'admin/material', - component: () => import('./views/admin/AdminMaterialLists.vue'), + name: 'camp/admin/material', + component: () => import('./views/camp/admin/AdminMaterialLists.vue'), }, { path: 'print', - name: 'admin/print', - component: () => import('./views/admin/Print.vue'), + name: 'camp/admin/print', + component: () => import('./views/camp/admin/Print.vue'), props: (route) => ({ camp: campFromRoute(route) }), }, ...(getEnv().FEATURE_CHECKLIST @@ -413,8 +459,8 @@ export default new Router({ // Checklist-Pages: { path: 'checklists', - name: 'admin/checklists', - component: () => import('./views/admin/Checklists.vue'), + name: 'camp/admin/checklists', + component: () => import('./views/camp/admin/Checklists.vue'), props: (route) => ({ camp: campFromRoute(route) }), }, ] @@ -422,22 +468,22 @@ export default new Router({ { path: 'materiallists', name: 'camp/material', - redirect: { name: 'admin/material' }, + redirect: { name: 'camp/admin/material' }, }, { path: '', name: 'camp/admin', - redirect: { name: 'admin/info' }, + redirect: { name: 'camp/admin/info' }, }, ], }, { path: '/camps/:campId/:campShortTitle/program/activities/:scheduleEntryId/:activityName?', - name: 'activity', + name: 'camp/activity', components: { navigation: NavigationCamp, - default: () => import('./views/activity/Activity.vue'), - aside: () => import('./views/activity/SideBarProgram.vue'), + default: () => import('./views/camp/activity/Activity.vue'), + aside: () => import('./views/camp/activity/SideBarProgram.vue'), }, beforeEnter: all([requireAuth, requireCamp, requireScheduleEntry]), props: { @@ -495,6 +541,18 @@ function requireAuth(to, from, next) { } } +function requireAdmin(to, from, next) { + if (isAdmin()) { + next() + } else { + next({ + name: 'PageNotFound', + params: [to.fullPath, ''], + replace: true, + }) + } +} + async function requireCamp(to, from, next) { const camp = await campFromRoute(to) if (camp === undefined) { @@ -654,16 +712,16 @@ export function checklistFromRoute(route) { function getContentLayout(route) { switch (route.name) { case 'camp/period/program': - case 'admin/print': - case 'admin/activity/category': + case 'camp/admin/print': + case 'camp/admin/activity/category': return 'full' case 'camp/print': case 'camp/material': - case 'admin/info': - case 'admin/activity': + case 'camp/admin/info': + case 'camp/admin/activity': return 'wide' - case 'admin/collaborators': - case 'admin/material': + case 'camp/admin/collaborators': + case 'camp/admin/material': default: return 'normal' } @@ -696,14 +754,14 @@ export function materialListRoute(camp, materialListOrRoute = '/all', query = {} if (camp._meta.loading) return {} if (typeof materialListOrRoute === 'string') { return { - name: `material${materialListOrRoute}`, + name: `camp/material${materialListOrRoute}`, params: { campId: camp.id, campShortTitle: slugify(campShortTitle(camp)) }, query, } } if (!materialListOrRoute?._meta || materialListOrRoute.meta?.loading) return {} return { - name: 'material/detail', + name: 'camp/material/detail', params: { campId: camp.id, campShortTitle: slugify(campShortTitle(camp)), @@ -722,7 +780,7 @@ export function materialListRoute(camp, materialListOrRoute = '/all', query = {} export function adminRoute(camp, subroute = 'info', query = {}) { if (camp._meta.loading) return {} return { - name: 'admin/' + subroute, + name: 'camp/admin/' + subroute, params: { campId: camp.id, campShortTitle: slugify(campShortTitle(camp)) }, query, } @@ -755,7 +813,7 @@ export function scheduleEntryRoute(scheduleEntry, query = {}) { // if (camp._meta.loading) return {} return { - name: 'activity', + name: 'camp/activity', params: { campId: camp.id, campShortTitle: slugify(campShortTitle(camp)), @@ -769,7 +827,7 @@ export function scheduleEntryRoute(scheduleEntry, query = {}) { export function categoryRoute(camp, category, query = {}) { if (camp._meta.loading || category._meta.loading) return {} return { - name: 'admin/activity/category', + name: 'camp/admin/activity/category', params: { campId: camp.id, campShortTitle: slugify(campShortTitle(camp)), @@ -781,9 +839,21 @@ export function categoryRoute(camp, category, query = {}) { } export function checklistRoute(camp, checklist, query = {}) { - if (camp._meta.loading || checklist._meta.loading) return {} + if (camp?._meta.loading || checklist._meta.loading) return {} + + if (!camp) { + return { + name: 'admin/checklists/checklist', + params: { + checklistId: checklist.id, + checklistName: slugify(checklist.name), + }, + query, + } + } + return { - name: 'admin/checklists/checklist', + name: 'camp/admin/checklists/checklist', params: { campId: camp.id, campTitle: slugify(camp.title), @@ -811,6 +881,6 @@ async function redirectToPeriod(to, from, next, routeName) { next(periodRoute(period, routeName, to.query)) } else { const camp = await apiStore.get().camps({ id: to.params.campId }) - next(campRoute(camp, 'admin', to.query)) + next(campRoute(camp, 'camp/admin', to.query)) } } diff --git a/frontend/src/views/Camps.vue b/frontend/src/views/Camps.vue index 2105c2f671..a476c34b6d 100644 --- a/frontend/src/views/Camps.vue +++ b/frontend/src/views/Camps.vue @@ -27,13 +27,13 @@ - +

{{ $tc('views.camps.prototypeCamps') }} @@ -91,6 +91,7 @@ export default { data: function () { return { loading: true, + isAdmin: false, } }, computed: { @@ -119,10 +120,10 @@ export default { }, async mounted() { this.loadCamps() + this.isAdmin = isAdmin() }, methods: { campRoute, - isAdmin, async loadCamps() { // Only reload camps if they were loaded before, to avoid console error if (this.camps._meta.self !== null) { diff --git a/frontend/src/views/admin/Checklist.vue b/frontend/src/views/admin/Checklist.vue new file mode 100644 index 0000000000..41062f25fc --- /dev/null +++ b/frontend/src/views/admin/Checklist.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/views/admin/Checklists.vue b/frontend/src/views/admin/Checklists.vue index 4bba314b0c..168dec3ef9 100644 --- a/frontend/src/views/admin/Checklists.vue +++ b/frontend/src/views/admin/Checklists.vue @@ -1,56 +1,19 @@ diff --git a/frontend/src/views/dev/Debug.vue b/frontend/src/views/admin/Debug.vue similarity index 99% rename from frontend/src/views/dev/Debug.vue rename to frontend/src/views/admin/Debug.vue index 4a790669b9..56797b93e5 100644 --- a/frontend/src/views/dev/Debug.vue +++ b/frontend/src/views/admin/Debug.vue @@ -39,7 +39,7 @@ import LanguageSwitcher from '@/components/layout/LanguageSwitcher.vue' import { parseTemplate } from 'url-template' export default { - name: 'Debug', + name: 'AdminDebug', components: { LanguageSwitcher, Coffee, diff --git a/frontend/src/views/admin/SideBarAdmin.vue b/frontend/src/views/admin/SideBarAdmin.vue index b2c4684c7a..d66effec27 100644 --- a/frontend/src/views/admin/SideBarAdmin.vue +++ b/frontend/src/views/admin/SideBarAdmin.vue @@ -1,37 +1,11 @@