Record deletion logic (maybe)

This commit is contained in:
Nicolò P. 2024-11-07 08:06:17 +01:00
parent 36185d0539
commit 3c2b804498
14 changed files with 197 additions and 20 deletions

View File

@ -33,4 +33,3 @@ export default class extends Controller {
location.href = delPath; location.href = delPath;
} }
} }

View File

@ -40,6 +40,7 @@ security:
- { path: ^/bibliography, roles: ROLE_USER } - { path: ^/bibliography, roles: ROLE_USER }
- { path: ^/collection, roles: ROLE_USER } - { path: ^/collection, roles: ROLE_USER }
- { path: ^/collector, roles: ROLE_USER } - { path: ^/collector, roles: ROLE_USER }
- { path: ^/document, roles: ROLE_USER }
- { path: ^/object, roles: ROLE_USER } - { path: ^/object, roles: ROLE_USER }
- { path: ^/site, roles: ROLE_USER } - { path: ^/site, roles: ROLE_USER }

View File

@ -6,19 +6,19 @@ use App\Entity\Bibliography;
use App\Entity\Collection; use App\Entity\Collection;
use App\Entity\Collector; use App\Entity\Collector;
use App\Form\BibliographyType; use App\Form\BibliographyType;
//use App\Security\Voter\VocabVoter; use App\Security\Voter\RecordVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
//use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class BibliographyController extends AbstractController class BibliographyController extends AbstractController
{ {
#[Route('/bibliography/{id<\d+>}', name: 'app_bibliography')] #[Route('/bibliography/{id<\d+>}', name: 'app_bibliography')]
public function index(Bibliography $bibliography, EntityManagerInterface $em): Response public function index(Bibliography $bibliography, EntityManagerInterface $em): Response
{ {
$repo = $em->getRepository(Collection::class); $repo = $em->getRepository(Collection::class);
$collections = $repo->findAllByBibliography($bibliography->getId()); $collections = $repo->findAllByBibliography($bibliography->getId());
$repo = $em->getRepository(Collector::class); $repo = $em->getRepository(Collector::class);
@ -71,15 +71,23 @@ class BibliographyController extends AbstractController
]); ]);
} }
/** /**
* @todo Permissions! * @todo Permissions! Return JSON with 403 when AJAX
*/ */
#[Route('/bibliography/delete/{id<\d+>}', name: 'app_bibliography_del')] #[Route('/bibliography/delete/{id<\d+>}', name: 'app_bibliography_del')]
public function delete(Bibliography $bibliography, EntityManagerInterface $em): Response public function delete(Bibliography $bibliography, EntityManagerInterface $em): Response
{ {
try {
$this->denyAccessUnlessGranted(RecordVoter::DELETE, $bibliography);
}
catch (AccessDeniedException) {
$this->addFlash('warning', 'You are not authorized to delete this record');
return $this->redirectToRoute('app_home');
}
$em->remove($bibliography); $em->remove($bibliography);
$em->flush(); $em->flush();
$this->addFlash('notice', 'Term deleted successfully'); $this->addFlash('notice', 'Record deleted successfully');
return $this->redirectToRoute('app_bibliography_landing'); return $this->redirectToRoute('app_bibliography_landing');
} }

View File

@ -4,10 +4,12 @@ namespace App\Controller;
use App\Entity\Collection; use App\Entity\Collection;
use App\Entity\Bibliography; use App\Entity\Bibliography;
use App\Security\Voter\RecordVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class CollectionController extends AbstractController class CollectionController extends AbstractController
{ {
@ -40,4 +42,23 @@ class CollectionController extends AbstractController
'count' => $count, 'count' => $count,
]); ]);
} }
#[Route('/collection/delete/{id<\d+>}', name: 'app_collection_del')]
public function delete(Collection $collection, EntityManagerInterface $em): Response
{
try {
$this->denyAccessUnlessGranted(RecordVoter::DELETE, $collection);
}
catch (AccessDeniedException) {
$this->addFlash('warning', 'You are not authorized to delete this record');
return $this->redirectToRoute('app_home');
}
$em->remove($collection);
$em->flush();
$this->addFlash('notice', 'Record deleted successfully');
return $this->redirectToRoute('app_collection_landing');
}
} }

View File

@ -5,10 +5,12 @@ namespace App\Controller;
use App\Entity\Collector; use App\Entity\Collector;
use App\Entity\Collection; use App\Entity\Collection;
use App\Entity\Bibliography; use App\Entity\Bibliography;
use App\Security\Voter\RecordVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class CollectorController extends AbstractController class CollectorController extends AbstractController
{ {
@ -40,4 +42,23 @@ class CollectorController extends AbstractController
'count' => $count, 'count' => $count,
]); ]);
} }
#[Route('/collector/delete/{id<\d+>}', name: 'app_collector_del')]
public function delete(Collector $collector, EntityManagerInterface $em): Response
{
try {
$this->denyAccessUnlessGranted(RecordVoter::DELETE, $collector);
}
catch (AccessDeniedException) {
$this->addFlash('warning', 'You are not authorized to delete this record');
return $this->redirectToRoute('app_home');
}
$em->remove($collector);
$em->flush();
$this->addFlash('notice', 'Record deleted successfully');
return $this->redirectToRoute('app_collector_landing');
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Entity; namespace App\Entity;
use App\RecordInterface;
use App\Repository\BibliographyRepository; use App\Repository\BibliographyRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -11,7 +12,7 @@ use Doctrine\Common\Collections\Collection as DoctrineCollection;
#[ORM\Entity(repositoryClass: BibliographyRepository::class)] #[ORM\Entity(repositoryClass: BibliographyRepository::class)]
#[ORM\Table(name: 'bibliography')] #[ORM\Table(name: 'bibliography')]
class Bibliography class Bibliography implements RecordInterface
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]

View File

@ -2,6 +2,7 @@
namespace App\Entity; namespace App\Entity;
use App\RecordInterface;
use App\Repository\CollectionRepository; use App\Repository\CollectionRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -11,7 +12,7 @@ use Doctrine\Common\Collections\Collection as DoctrineCollection;
#[ORM\Entity(repositoryClass: CollectionRepository::class)] #[ORM\Entity(repositoryClass: CollectionRepository::class)]
#[ORM\Table(name: 'collection')] #[ORM\Table(name: 'collection')]
class Collection class Collection implements RecordInterface
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]

View File

@ -2,6 +2,7 @@
namespace App\Entity; namespace App\Entity;
use App\RecordInterface;
use App\Repository\CollectorRepository; use App\Repository\CollectorRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -11,7 +12,7 @@ use Doctrine\Common\Collections\Collection as DoctrineCollection;
#[ORM\Entity(repositoryClass: CollectorRepository::class)] #[ORM\Entity(repositoryClass: CollectorRepository::class)]
#[ORM\Table(name: 'collector')] #[ORM\Table(name: 'collector')]
class Collector class Collector implements RecordInterface
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]

7
src/RecordInterface.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace App;
interface RecordInterface
{
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
final class RecordVoter extends Voter
{
public const EDIT = 'RECORD_EDIT';
public const DELETE = 'RECORD_DELETE';
public const VIEW = 'RECORD_VIEW';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE])
&& $subject instanceof \App\RecordInterface;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
$roles = $user->getRoles();
// TODO: Better way to check roles?
switch ($attribute) {
case self::EDIT:
case self::DELETE:
return in_array('ROLE_ADMIN', $roles)
|| in_array('ROLE_REVISOR', $roles);
break;
case self::VIEW:
return ! in_array('ROLE_READER', $roles);
break;
}
return false;
}
}

View File

@ -3,7 +3,7 @@
{% block title %}Bibliography | ArCOA{% endblock %} {% block title %}Bibliography | ArCOA{% endblock %}
{% block rightpanel %} {% block rightpanel %}
<div class="container" style="max-width: 60vw"> <div class="container" style="max-width: 60vw" data-controller="delete-record">
<h1 class="is-size-1 mt-0 has-text-centered">Bibliography</h1> <h1 class="is-size-1 mt-0 has-text-centered">Bibliography</h1>
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2> <h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
@ -66,16 +66,39 @@
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</span> </span>
</button> </button>
<button class="button is-small is-danger" title="Delete record"> <a href="{{ path('app_bibliography_del', {'id' : record.id}) }}"
class="button is-small is-danger" title="Delete record"
data-delete-record-target="path" data-action="click->delete-record#show">
<span class="icon"> <span class="icon">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</span> </span>
</button> </a>
</div> </div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div class="modal" data-delete-record-target="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<span class="icon is-large has-text-warning">
<i class="fa fa-warning fa-2x"></i>
</span>
<p class="modal-card-title has-text-danger pl-2"><strong>Delete record?</strong></p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p class="is-size-5">This record will be permanently deleted. Proceed?</p>
</section>
<footer class="modal-card-foot">
<div class="buttons is-right">
<button class="button is-link" data-action="click->delete-record#delete">Confirm</button>
<button class="button is-light" id="cancel">Cancel</button>
</div>
</footer>
</div>
</div>
</div> </div>
<script type="text/javascript" defer> <script type="text/javascript" defer>
const delBtns = document.querySelectorAll('.delete'); const delBtns = document.querySelectorAll('.delete');

View File

@ -3,7 +3,7 @@
{% block title %}Collection | ArCOA{% endblock %} {% block title %}Collection | ArCOA{% endblock %}
{% block rightpanel %} {% block rightpanel %}
<div class="container" style="max-width: 60vw"> <div class="container" style="max-width: 60vw" data-controller="delete-record">
<h1 class="is-size-1 mt-0 has-text-centered">Collection</h1> <h1 class="is-size-1 mt-0 has-text-centered">Collection</h1>
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2> <h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
@ -66,16 +66,39 @@
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</span> </span>
</button> </button>
<button class="button is-small is-danger" title="Delete record"> <a href="{{ path('app_collection_del', {'id' : record.id}) }}"
class="button is-small is-danger" title="Delete record"
data-delete-record-target="path" data-action="click->delete-record#show">
<span class="icon"> <span class="icon">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</span> </span>
</button> </a>
</div> </div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div class="modal" data-delete-record-target="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<span class="icon is-large has-text-warning">
<i class="fa fa-warning fa-2x"></i>
</span>
<p class="modal-card-title has-text-danger pl-2"><strong>Delete record?</strong></p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p class="is-size-5">This record will be permanently deleted. Proceed?</p>
</section>
<footer class="modal-card-foot">
<div class="buttons is-right">
<button class="button is-link" data-action="click->delete-record#delete">Confirm</button>
<button class="button is-light" id="cancel">Cancel</button>
</div>
</footer>
</div>
</div>
</div> </div>
<script type="text/javascript" defer> <script type="text/javascript" defer>
const delBtns = document.querySelectorAll('.delete'); const delBtns = document.querySelectorAll('.delete');

View File

@ -3,7 +3,7 @@
{% block title %}Collector | ArCOA{% endblock %} {% block title %}Collector | ArCOA{% endblock %}
{% block rightpanel %} {% block rightpanel %}
<div class="container" style="max-width: 60vw"> <div class="container" style="max-width: 60vw" data-controller="delete-record">
<h1 class="is-size-1 mt-0 has-text-centered">Collector</h1> <h1 class="is-size-1 mt-0 has-text-centered">Collector</h1>
<h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2> <h2 class="is-size-3 mt-3 has-text-centered">Choose action</h2>
@ -66,16 +66,39 @@
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</span> </span>
</button> </button>
<button class="button is-small is-danger" title="Delete record"> <a href="{{ path('app_collector_del', {'id' : record.id}) }}"
class="button is-small is-danger" title="Delete record"
data-delete-record-target="path" data-action="click->delete-record#show">
<span class="icon"> <span class="icon">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</span> </span>
</button> </a>
</div> </div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<div class="modal" data-delete-record-target="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<span class="icon is-large has-text-warning">
<i class="fa fa-warning fa-2x"></i>
</span>
<p class="modal-card-title has-text-danger pl-2"><strong>Delete record?</strong></p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<p class="is-size-5">This record will be permanently deleted. Proceed?</p>
</section>
<footer class="modal-card-foot">
<div class="buttons is-right">
<button class="button is-link" data-action="click->delete-record#delete">Confirm</button>
<button class="button is-light" id="cancel">Cancel</button>
</div>
</footer>
</div>
</div>
</div> </div>
<script type="text/javascript" defer> <script type="text/javascript" defer>
const delBtns = document.querySelectorAll('.delete'); const delBtns = document.querySelectorAll('.delete');