mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-07-10 02:14:31 +02:00
Merge branch 'master' into permission_rework
This commit is contained in:
commit
33f8d2ba9e
109 changed files with 5095 additions and 2860 deletions
|
@ -81,6 +81,8 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
|||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
abstract class BaseAdminController extends AbstractController
|
||||
{
|
||||
protected $entity_class = '';
|
||||
|
@ -419,7 +421,7 @@ abstract class BaseAdminController extends AbstractController
|
|||
/** @var AbstractPartsContainingRepository $repo */
|
||||
$repo = $this->entityManager->getRepository($this->entity_class);
|
||||
if ($repo->getPartsCount($entity) > 0) {
|
||||
$this->addFlash('error', 'entity.delete.must_not_contain_parts');
|
||||
$this->addFlash('error', t('entity.delete.must_not_contain_parts', ['%PATH%' => $entity->getFullPath()]));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -442,7 +444,18 @@ abstract class BaseAdminController extends AbstractController
|
|||
|
||||
//Check if we need to remove recursively
|
||||
if ($entity instanceof AbstractStructuralDBElement && $request->get('delete_recursive', false)) {
|
||||
$recursionHelper->delete($entity, false);
|
||||
$can_delete = true;
|
||||
//Check if any of the children can not be deleted, cause it contains parts
|
||||
$recursionHelper->execute($entity, function (AbstractStructuralDBElement $element) use (&$can_delete) {
|
||||
if(!$this->deleteCheck($element)) {
|
||||
$can_delete = false;
|
||||
}
|
||||
});
|
||||
if($can_delete) {
|
||||
$recursionHelper->delete($entity, false);
|
||||
} else {
|
||||
return $this->redirectToRoute($this->route_base.'_edit', ['id' => $entity->getID()]);
|
||||
}
|
||||
} else {
|
||||
if ($entity instanceof AbstractStructuralDBElement) {
|
||||
$parent = $entity->getParent();
|
||||
|
|
|
@ -80,6 +80,8 @@ class PartListsController extends AbstractController
|
|||
*/
|
||||
public function tableAction(Request $request, PartsTableActionHandler $actionHandler): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@parts.edit');
|
||||
|
||||
$redirect = $request->request->get('redirect_back');
|
||||
$ids = $request->request->get('ids');
|
||||
$action = $request->request->get('action');
|
||||
|
@ -137,6 +139,8 @@ class PartListsController extends AbstractController
|
|||
*/
|
||||
protected function showListWithFilter(Request $request, string $template, ?callable $filter_changer = null, ?callable $form_changer = null, array $additonal_template_vars = [], array $additional_table_vars = []): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@parts.read');
|
||||
|
||||
$formRequest = clone $request;
|
||||
$formRequest->setMethod('GET');
|
||||
$filter = new PartFilter($this->nodesListBuilder);
|
||||
|
|
|
@ -113,7 +113,10 @@ class UserController extends AdminPages\BaseAdminController
|
|||
$entity->setGoogleAuthenticatorSecret(null);
|
||||
$entity->setBackupCodes([]);
|
||||
//Remove all U2F keys
|
||||
foreach ($entity->getU2FKeys() as $key) {
|
||||
foreach ($entity->getLegacyU2FKeys() as $key) {
|
||||
$em->remove($key);
|
||||
}
|
||||
foreach ($entity->getWebAuthnKeys() as $key) {
|
||||
$em->remove($key);
|
||||
}
|
||||
//Invalidate trusted devices
|
||||
|
|
|
@ -44,6 +44,7 @@ namespace App\Controller;
|
|||
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use App\Events\SecurityEvent;
|
||||
use App\Events\SecurityEvents;
|
||||
use App\Form\TFAGoogleSettingsType;
|
||||
|
@ -130,6 +131,7 @@ class UserSettingsController extends AbstractController
|
|||
}
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
|
||||
//Handle U2F key removal
|
||||
if ($request->request->has('key_id')) {
|
||||
$key_id = $request->request->get('key_id');
|
||||
$key_repo = $entityManager->getRepository(U2FKey::class);
|
||||
|
@ -138,14 +140,14 @@ class UserSettingsController extends AbstractController
|
|||
if (null === $u2f) {
|
||||
$this->addFlash('danger', 'tfa_u2f.u2f_delete.not_existing');
|
||||
|
||||
throw new RuntimeException('Key not existing!');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
//User can only delete its own U2F keys
|
||||
if ($u2f->getUser() !== $user) {
|
||||
$this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied');
|
||||
|
||||
throw new RuntimeException('You can only delete your own U2F keys!');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
$backupCodeManager->disableBackupCodesIfUnused($user);
|
||||
|
@ -153,6 +155,31 @@ class UserSettingsController extends AbstractController
|
|||
$entityManager->flush();
|
||||
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
|
||||
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
|
||||
} else if ($request->request->has('webauthn_key_id')) {
|
||||
$key_id = $request->request->get('webauthn_key_id');
|
||||
$key_repo = $entityManager->getRepository(WebauthnKey::class);
|
||||
/** @var WebauthnKey|null $key */
|
||||
$key = $key_repo->find($key_id);
|
||||
if (null === $key) {
|
||||
$this->addFlash('error', 'tfa_u2f.u2f_delete.not_existing');
|
||||
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
//User can only delete its own U2F keys
|
||||
if ($key->getUser() !== $user) {
|
||||
$this->addFlash('error', 'tfa_u2f.u2f_delete.access_denied');
|
||||
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
$backupCodeManager->disableBackupCodesIfUnused($user);
|
||||
$entityManager->remove($key);
|
||||
$entityManager->flush();
|
||||
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
|
||||
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
|
||||
}
|
||||
|
|
60
src/Controller/WebauthnKeyRegistrationController.php
Normal file
60
src/Controller/WebauthnKeyRegistrationController.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
class WebauthnKeyRegistrationController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/webauthn/register", name="webauthn_register")
|
||||
*/
|
||||
public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em)
|
||||
{
|
||||
|
||||
//If form was submitted, check the auth response
|
||||
if ($request->getMethod() === 'POST') {
|
||||
$webauthnResponse = $request->request->get('_auth_code');
|
||||
|
||||
//Retrieve other data from the form, that you want to store with the key
|
||||
$keyName = $request->request->get('keyName');
|
||||
if (empty($keyName)) {
|
||||
$keyName = 'Key ' . date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
//Check the response
|
||||
try {
|
||||
$new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse);
|
||||
} catch (\Exception $exception) {
|
||||
$this->addFlash('error', t('tfa_u2f.add_key.registration_error'));
|
||||
return $this->redirectToRoute('webauthn_register');
|
||||
}
|
||||
|
||||
$keyEntity = WebauthnKey::fromRegistration($new_key);
|
||||
$keyEntity->setName($keyName);
|
||||
$keyEntity->setUser($this->getUser());
|
||||
|
||||
$em->persist($keyEntity);
|
||||
$em->flush();
|
||||
|
||||
|
||||
$this->addFlash('success', 'Key registered successfully');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
|
||||
return $this->render(
|
||||
'Security/Webauthn/webauthn_register.html.twig',
|
||||
[
|
||||
'registrationRequest' => $registrationHelper->generateRegistrationRequestAsJSON(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -226,7 +226,7 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
|
||||
private function getQuery(QueryBuilder $builder): void
|
||||
{
|
||||
$builder->distinct()->select('attachment')
|
||||
$builder->select('attachment')
|
||||
->addSelect('attachment_type')
|
||||
//->addSelect('element')
|
||||
->from(Attachment::class, 'attachment')
|
||||
|
|
|
@ -297,7 +297,7 @@ class LogDataTable implements DataTableTypeInterface
|
|||
|
||||
protected function getQuery(QueryBuilder $builder, array $options): void
|
||||
{
|
||||
$builder->distinct()->select('log')
|
||||
$builder->select('log')
|
||||
->addSelect('user')
|
||||
->from(AbstractLogEntry::class, 'log')
|
||||
->leftJoin('log.user', 'user');
|
||||
|
|
|
@ -292,8 +292,8 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
|
||||
private function getQuery(QueryBuilder $builder): void
|
||||
{
|
||||
|
||||
$builder->distinct()->select('part')
|
||||
//Distinct is very slow here, do not add this here (also I think this is not needed here, as the id column is always distinct)
|
||||
$builder->select('part')
|
||||
->addSelect('category')
|
||||
->addSelect('footprint')
|
||||
->addSelect('manufacturer')
|
||||
|
|
|
@ -34,7 +34,12 @@ use LogicException;
|
|||
* Class Attachment.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\AttachmentRepository")
|
||||
* @ORM\Table(name="`attachments`")
|
||||
* @ORM\Table(name="`attachments`", indexes={
|
||||
* @ORM\Index(name="attachments_idx_id_element_id_class_name", columns={"id", "element_id", "class_name"}),
|
||||
* @ORM\Index(name="attachments_idx_class_name_id", columns={"class_name", "id"}),
|
||||
* @ORM\Index(name="attachment_name_idx", columns={"name"}),
|
||||
* @ORM\Index(name="attachment_element_idx", columns={"class_name", "element_id"})
|
||||
* })
|
||||
* @ORM\InheritanceType("SINGLE_TABLE")
|
||||
* @ORM\DiscriminatorColumn(name="class_name", type="string")
|
||||
* @ORM\DiscriminatorMap({
|
||||
|
@ -104,7 +109,7 @@ abstract class Attachment extends AbstractNamedDBElement
|
|||
/**
|
||||
* @var AttachmentType
|
||||
* @ORM\ManyToOne(targetEntity="AttachmentType", inversedBy="attachments_with_type")
|
||||
* @ORM\JoinColumn(name="type_id", referencedColumnName="id")
|
||||
* @ORM\JoinColumn(name="type_id", referencedColumnName="id", nullable=false)
|
||||
* @Selectable()
|
||||
* @Assert\NotNull(message="validator.attachment.must_not_be_null")
|
||||
*/
|
||||
|
|
|
@ -34,7 +34,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class AttachmentType.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\StructuralDBElementRepository")
|
||||
* @ORM\Table(name="`attachment_types`")
|
||||
* @ORM\Table(name="`attachment_types`", indexes={
|
||||
* @ORM\Index(name="attachment_types_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="attachment_types_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class AttachmentType extends AbstractStructuralDBElement
|
||||
{
|
||||
|
|
|
@ -93,7 +93,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
|
|||
* @NoneOfItsChildren()
|
||||
* @Groups({"include_parents"})
|
||||
*/
|
||||
protected $parent;
|
||||
protected $parent = null;
|
||||
|
||||
/** @var string[] all names of all parent elements as a array of strings,
|
||||
* the last array element is the name of the element itself
|
||||
|
@ -271,16 +271,17 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
|
|||
*/
|
||||
public function getSubelements(): iterable
|
||||
{
|
||||
return $this->children;
|
||||
return $this->children ?? new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see getSubelements()
|
||||
* @return Collection<static>|iterable
|
||||
* @psalm-return Collection<int, static>
|
||||
*/
|
||||
public function getChildren(): iterable
|
||||
{
|
||||
return $this->children;
|
||||
return $this->getSubelements();
|
||||
}
|
||||
|
||||
public function isNotSelectable(): bool
|
||||
|
|
|
@ -78,12 +78,12 @@ class DevicePart extends AbstractDBElement
|
|||
* @ORM\ManyToOne(targetEntity="Device", inversedBy="parts")
|
||||
* @ORM\JoinColumn(name="id_device", referencedColumnName="id")
|
||||
*/
|
||||
protected Device $device;
|
||||
protected ?Device $device = null;
|
||||
|
||||
/**
|
||||
* @var Part
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part")
|
||||
* @ORM\JoinColumn(name="id_part", referencedColumnName="id")
|
||||
*/
|
||||
protected Part $part;
|
||||
protected ?Part $part = null;
|
||||
}
|
||||
|
|
|
@ -71,7 +71,11 @@ use Psr\Log\LogLevel;
|
|||
* This entity describes a entry in the event log.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\LogEntryRepository")
|
||||
* @ORM\Table("log")
|
||||
* @ORM\Table("log", indexes={
|
||||
* @ORM\Index(name="log_idx_type", columns={"type"}),
|
||||
* @ORM\Index(name="log_idx_type_target", columns={"type", "target_type", "target_id"}),
|
||||
* @ORM\Index(name="log_idx_datetime", columns={"datetime"}),
|
||||
* })
|
||||
* @ORM\InheritanceType("SINGLE_TABLE")
|
||||
* @ORM\DiscriminatorColumn(name="type", type="smallint")
|
||||
* @ORM\DiscriminatorMap({
|
||||
|
|
|
@ -34,7 +34,11 @@ use function sprintf;
|
|||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @ORM\Table("parameters")
|
||||
* @ORM\Table("parameters", indexes={
|
||||
* @ORM\Index(name="parameter_name_idx", columns={"name"}),
|
||||
* @ORM\Index(name="parameter_group_idx", columns={"param_group"}),
|
||||
* @ORM\Index(name="parameter_type_element_idx", columns={"type", "element_id"})
|
||||
* })
|
||||
* @ORM\InheritanceType("SINGLE_TABLE")
|
||||
* @ORM\DiscriminatorColumn(name="type", type="smallint")
|
||||
* @ORM\DiscriminatorMap({
|
||||
|
@ -91,7 +95,6 @@ abstract class AbstractParameter extends AbstractNamedDBElement
|
|||
|
||||
/**
|
||||
* @var string The unit in which the value values are given (e.g. V)
|
||||
* @Assert\Length(max=5)
|
||||
* @ORM\Column(type="string", nullable=false)
|
||||
*/
|
||||
protected string $unit = '';
|
||||
|
|
|
@ -33,7 +33,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class AttachmentType.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\Parts\CategoryRepository")
|
||||
* @ORM\Table(name="`categories`")
|
||||
* @ORM\Table(name="`categories`", indexes={
|
||||
* @ORM\Index(name="category_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="category_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Category extends AbstractPartsContainingDBElement
|
||||
{
|
||||
|
|
|
@ -61,7 +61,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class Footprint.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\Parts\FootprintRepository")
|
||||
* @ORM\Table("`footprints`")
|
||||
* @ORM\Table("`footprints`", indexes={
|
||||
* @ORM\Index(name="footprint_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="footprint_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Footprint extends AbstractPartsContainingDBElement
|
||||
{
|
||||
|
|
|
@ -61,7 +61,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class Manufacturer.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\Parts\ManufacturerRepository")
|
||||
* @ORM\Table("`manufacturers`")
|
||||
* @ORM\Table("`manufacturers`", indexes={
|
||||
* @ORM\Index(name="manufacturer_name", columns={"name"}),
|
||||
* @ORM\Index(name="manufacturer_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Manufacturer extends AbstractCompany
|
||||
{
|
||||
|
|
|
@ -55,7 +55,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* This could be something like N, grams, meters, etc...
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\Parts\MeasurementUnitRepository")
|
||||
* @ORM\Table(name="`measurement_units`")
|
||||
* @ORM\Table(name="`measurement_units`", indexes={
|
||||
* @ORM\Index(name="unit_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="unit_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
* @UniqueEntity("unit")
|
||||
*/
|
||||
class MeasurementUnit extends AbstractPartsContainingDBElement
|
||||
|
@ -66,7 +69,7 @@ class MeasurementUnit extends AbstractPartsContainingDBElement
|
|||
* @ORM\Column(type="string", name="unit", nullable=true)
|
||||
* @Assert\Length(max=10)
|
||||
*/
|
||||
protected string $unit;
|
||||
protected ?string $unit = null;
|
||||
|
||||
/**
|
||||
* @var bool Determines if the amount value associated with this unit should be treated as integer.
|
||||
|
|
|
@ -74,7 +74,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Otherwise this class would be too big, to be maintained.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\PartRepository")
|
||||
* @ORM\Table("`parts`")
|
||||
* @ORM\Table("`parts`", indexes={
|
||||
* @ORM\Index(name="parts_idx_datet_name_last_id_needs", columns={"datetime_added", "name", "last_modified", "id", "needs_review"}),
|
||||
* @ORM\Index(name="parts_idx_name", columns={"name"}),
|
||||
* })
|
||||
*/
|
||||
class Part extends AttachmentContainingDBElement
|
||||
{
|
||||
|
|
|
@ -58,7 +58,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* It is the connection between a part and its store locations.
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="part_lots")
|
||||
* @ORM\Table(name="part_lots", indexes={
|
||||
* @ORM\Index(name="part_lots_idx_instock_un_expiration_id_part", columns={"instock_unknown", "expiration_date", "id_part"}),
|
||||
* @ORM\Index(name="part_lots_idx_needs_refill", columns={"needs_refill"}),
|
||||
* })
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @ValidPartLot()
|
||||
*/
|
||||
|
|
|
@ -79,7 +79,7 @@ trait ManufacturerTrait
|
|||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
* @Assert\Choice({"announced", "active", "nrfnd", "eol", "discontinued", ""})
|
||||
*/
|
||||
protected string $manufacturing_status = '';
|
||||
protected ?string $manufacturing_status = '';
|
||||
|
||||
/**
|
||||
* Get the link to the website of the article on the manufacturers website
|
||||
|
|
|
@ -79,7 +79,7 @@ trait OrderTrait
|
|||
* @ORM\OneToOne(targetEntity="App\Entity\PriceInformations\Orderdetail")
|
||||
* @ORM\JoinColumn(name="order_orderdetails_id", referencedColumnName="id")
|
||||
*/
|
||||
protected Orderdetail $order_orderdetail;
|
||||
protected ?Orderdetail $order_orderdetail = null;
|
||||
|
||||
/**
|
||||
* Get the selected order orderdetails of this part.
|
||||
|
|
|
@ -61,7 +61,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class Store location.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\Parts\StorelocationRepository")
|
||||
* @ORM\Table("`storelocations`")
|
||||
* @ORM\Table("`storelocations`", indexes={
|
||||
* @ORM\Index(name="location_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="location_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Storelocation extends AbstractPartsContainingDBElement
|
||||
{
|
||||
|
|
|
@ -65,7 +65,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class Supplier.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\Parts\SupplierRepository")
|
||||
* @ORM\Table("`suppliers`")
|
||||
* @ORM\Table("`suppliers`", indexes={
|
||||
* @ORM\Index(name="supplier_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="supplier_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Supplier extends AbstractCompany
|
||||
{
|
||||
|
|
|
@ -59,7 +59,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
*
|
||||
* @UniqueEntity("iso_code")
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="currencies")
|
||||
* @ORM\Table(name="currencies", indexes={
|
||||
* @ORM\Index(name="currency_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="currency_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Currency extends AbstractStructuralDBElement
|
||||
{
|
||||
|
@ -78,7 +81,7 @@ class Currency extends AbstractStructuralDBElement
|
|||
* @ORM\Column(type="string")
|
||||
* @Assert\Currency()
|
||||
*/
|
||||
protected string $iso_code;
|
||||
protected string $iso_code = "";
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="Currency", mappedBy="parent", cascade={"persist"})
|
||||
|
|
|
@ -66,7 +66,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
/**
|
||||
* Class Orderdetail.
|
||||
*
|
||||
* @ORM\Table("`orderdetails`")
|
||||
* @ORM\Table("`orderdetails`", indexes={
|
||||
* @ORM\Index(name="orderdetails_supplier_part_nr", columns={"supplierpartnr"}),
|
||||
* })
|
||||
* @ORM\Entity()
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @UniqueEntity({"supplierpartnr", "supplier", "part"})
|
||||
|
|
|
@ -66,7 +66,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* Class Pricedetail.
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table("`pricedetails`")
|
||||
* @ORM\Table("`pricedetails`", indexes={
|
||||
* @ORM\Index(name="pricedetails_idx_min_discount", columns={"min_discount_quantity"}),
|
||||
* @ORM\Index(name="pricedetails_idx_min_discount_price_qty", columns={"min_discount_quantity", "price_related_quantity"}),
|
||||
* })
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @UniqueEntity(fields={"min_discount_quantity", "orderdetail"})
|
||||
*/
|
||||
|
|
|
@ -56,7 +56,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* This entity represents an user group.
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table("`groups`")
|
||||
* @ORM\Table("`groups`", indexes={
|
||||
* @ORM\Index(name="group_idx_name", columns={"name"}),
|
||||
* @ORM\Index(name="group_idx_parent_name", columns={"parent_id", "name"}),
|
||||
* })
|
||||
*/
|
||||
class Group extends AbstractStructuralDBElement implements HasPermissionsInterface
|
||||
{
|
||||
|
@ -75,8 +78,9 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
|
|||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="User", mappedBy="group")
|
||||
* @var Collection<User>
|
||||
*/
|
||||
protected Collection $users;
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @var bool If true all users associated with this group must have enabled some kind of 2 factor authentication
|
||||
|
@ -85,7 +89,7 @@ class Group extends AbstractStructuralDBElement implements HasPermissionsInterfa
|
|||
protected $enforce2FA = false;
|
||||
/**
|
||||
* @var Collection<int, GroupAttachment>
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\ManufacturerAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\Attachments\GroupAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true)
|
||||
* @ORM\OrderBy({"name" = "ASC"})
|
||||
* @Assert\Valid()
|
||||
*/
|
||||
|
|
|
@ -44,8 +44,7 @@ namespace App\Entity\UserSystem;
|
|||
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface;
|
||||
use u2flib_server\Registration;
|
||||
use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
|
@ -56,7 +55,7 @@ use u2flib_server\Registration;
|
|||
* })
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
class U2FKey implements TwoFactorKeyInterface
|
||||
class U2FKey implements LegacyU2FKeyInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
|
@ -110,14 +109,6 @@ class U2FKey implements TwoFactorKeyInterface
|
|||
**/
|
||||
protected ?User $user = null;
|
||||
|
||||
public function fromRegistrationData(Registration $data): void
|
||||
{
|
||||
$this->keyHandle = $data->keyHandle;
|
||||
$this->publicKey = $data->publicKey;
|
||||
$this->certificate = $data->certificate;
|
||||
$this->counter = $data->counter;
|
||||
}
|
||||
|
||||
public function getKeyHandle(): string
|
||||
{
|
||||
return $this->keyHandle;
|
||||
|
|
|
@ -57,7 +57,9 @@ use App\Entity\PriceInformations\Currency;
|
|||
use App\Security\Interfaces\HasPermissionsInterface;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use App\Validator\Constraints\ValidPermission;
|
||||
use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use function count;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
@ -65,8 +67,6 @@ use Doctrine\Common\Collections\Collection;
|
|||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Exception;
|
||||
use function in_array;
|
||||
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorInterface as U2FTwoFactorInterface;
|
||||
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface;
|
||||
use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
|
||||
use Scheb\TwoFactorBundle\Model\PreferredProviderInterface;
|
||||
|
@ -74,17 +74,20 @@ use Scheb\TwoFactorBundle\Model\TrustedDeviceInterface;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface;
|
||||
|
||||
/**
|
||||
* This entity represents a user, which can log in and have permissions.
|
||||
* Also this entity is able to save some informations about the user, like the names, email-address and other info.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
|
||||
* @ORM\Table("`users`")
|
||||
* @ORM\Table("`users`", indexes={
|
||||
* @ORM\Index(name="user_idx_username", columns={"name"})
|
||||
* })
|
||||
* @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"})
|
||||
* @UniqueEntity("name", message="validator.user.username_already_used")
|
||||
*/
|
||||
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, U2FTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface
|
||||
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
//use MasterAttachmentTrait;
|
||||
|
||||
|
@ -146,7 +149,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
|
||||
/**
|
||||
* @var Group|null the group this user belongs to
|
||||
* @ORM\ManyToOne(targetEntity="Group", inversedBy="users", fetch="EAGER")
|
||||
* DO NOT PUT A fetch eager here! Otherwise you can not unset the group of a user! This seems to be some kind of bug in doctrine. Maybe this is fixed in future versions.
|
||||
* @ORM\ManyToOne(targetEntity="Group", inversedBy="users")
|
||||
* @ORM\JoinColumn(name="group_id", referencedColumnName="id")
|
||||
* @Selectable()
|
||||
*/
|
||||
|
@ -239,11 +243,17 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
*/
|
||||
protected ?DateTime $backupCodesGenerationDate = null;
|
||||
|
||||
/** @var Collection<int, TwoFactorKeyInterface>
|
||||
/** @var Collection<int, LegacyU2FKeyInterface>
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true)
|
||||
*/
|
||||
protected $u2fKeys;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WebauthnKey>
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\UserSystem\WebauthnKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true)
|
||||
*/
|
||||
protected $webauthn_keys;
|
||||
|
||||
/**
|
||||
* @var Currency|null The currency the user wants to see prices in.
|
||||
* Dont use fetch=EAGER here, this will cause problems with setting the currency setting.
|
||||
|
@ -272,6 +282,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
parent::__construct();
|
||||
$this->permissions = new PermissionsEmbed();
|
||||
$this->u2fKeys = new ArrayCollection();
|
||||
$this->webauthn_keys = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -762,7 +773,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
*/
|
||||
public function isBackupCode(string $code): bool
|
||||
{
|
||||
return in_array($code, $this->backupCodes, true);
|
||||
return in_array($code, $this->getBackupCodes(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -772,7 +783,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
*/
|
||||
public function invalidateBackupCode(string $code): void
|
||||
{
|
||||
$key = array_search($code, $this->backupCodes, true);
|
||||
$key = array_search($code, $this->getBackupCodes(), true);
|
||||
if (false !== $key) {
|
||||
unset($this->backupCodes[$key]);
|
||||
}
|
||||
|
@ -836,48 +847,48 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
++$this->trustedDeviceCookieVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if U2F is enabled.
|
||||
*/
|
||||
public function isU2FAuthEnabled(): bool
|
||||
{
|
||||
return count($this->u2fKeys) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all U2F Keys that are associated with this user.
|
||||
*
|
||||
* @psalm-return Collection<int, TwoFactorKeyInterface>
|
||||
*/
|
||||
public function getU2FKeys(): Collection
|
||||
{
|
||||
return $this->u2fKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a U2F key to this user.
|
||||
*/
|
||||
public function addU2FKey(TwoFactorKeyInterface $key): void
|
||||
{
|
||||
$this->u2fKeys->add($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a U2F key from this user.
|
||||
*/
|
||||
public function removeU2FKey(TwoFactorKeyInterface $key): void
|
||||
{
|
||||
$this->u2fKeys->removeElement($key);
|
||||
}
|
||||
|
||||
public function getPreferredTwoFactorProvider(): ?string
|
||||
{
|
||||
//If U2F is available then prefer it
|
||||
if ($this->isU2FAuthEnabled()) {
|
||||
return 'u2f_two_factor';
|
||||
//if ($this->isU2FAuthEnabled()) {
|
||||
// return 'u2f_two_factor';
|
||||
//}
|
||||
|
||||
if ($this->isWebAuthnAuthenticatorEnabled()) {
|
||||
return 'webauthn_two_factor_provider';
|
||||
}
|
||||
|
||||
//Otherwise use other methods
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isWebAuthnAuthenticatorEnabled(): bool
|
||||
{
|
||||
return count($this->u2fKeys) > 0
|
||||
|| count($this->webauthn_keys) > 0;
|
||||
}
|
||||
|
||||
public function getLegacyU2FKeys(): iterable
|
||||
{
|
||||
return $this->u2fKeys;
|
||||
}
|
||||
|
||||
public function getWebAuthnUser(): PublicKeyCredentialUserEntity
|
||||
{
|
||||
return new PublicKeyCredentialUserEntity(
|
||||
$this->getUsername(),
|
||||
(string) $this->getId(),
|
||||
$this->getFullName(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getWebauthnKeys(): iterable
|
||||
{
|
||||
return $this->webauthn_keys;
|
||||
}
|
||||
|
||||
public function addWebauthnKey(WebauthnKey $webauthnKey): void
|
||||
{
|
||||
$this->webauthn_keys->add($webauthnKey);
|
||||
}
|
||||
}
|
||||
|
|
98
src/Entity/UserSystem/WebauthnKey.php
Normal file
98
src/Entity/UserSystem/WebauthnKey.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity\UserSystem;
|
||||
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="webauthn_keys")
|
||||
* @ORM\Entity()
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
class WebauthnKey extends BasePublicKeyCredentialSource
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
protected int $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", inversedBy="webauthn_keys")
|
||||
**/
|
||||
protected ?User $user = null;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return WebauthnKey
|
||||
*/
|
||||
public function setName(string $name): WebauthnKey
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user
|
||||
* @return WebauthnKey
|
||||
*/
|
||||
public function setUser(?User $user): WebauthnKey
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static function fromRegistration(BasePublicKeyCredentialSource $registration): self
|
||||
{
|
||||
return new self(
|
||||
$registration->getPublicKeyCredentialId(),
|
||||
$registration->getType(),
|
||||
$registration->getTransports(),
|
||||
$registration->getAttestationType(),
|
||||
$registration->getTrustPath(),
|
||||
$registration->getAaguid(),
|
||||
$registration->getCredentialPublicKey(),
|
||||
$registration->getUserHandle(),
|
||||
$registration->getCounter(),
|
||||
$registration->getOtherUI()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -75,6 +75,11 @@ class TreeCacheInvalidationListener
|
|||
if ($element instanceof AbstractStructuralDBElement || $element instanceof LabelProfile) {
|
||||
$secure_class_name = str_replace('\\', '_', get_class($element));
|
||||
$this->cache->invalidateTags([$secure_class_name]);
|
||||
|
||||
//Trigger a sidebar reload for all users (see SidebarTreeUpdater service)
|
||||
if(!$element instanceof LabelProfile) {
|
||||
$this->cache->invalidateTags(['sidebar_tree_update']);
|
||||
}
|
||||
}
|
||||
|
||||
//If a user change, then invalidate all cached trees for him
|
||||
|
|
|
@ -146,7 +146,7 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface
|
|||
*/
|
||||
public static function TFARedirectNeeded(User $user): bool
|
||||
{
|
||||
$tfa_enabled = $user->isU2FAuthEnabled() || $user->isGoogleAuthenticatorEnabled();
|
||||
$tfa_enabled = $user->isWebAuthnAuthenticatorEnabled() || $user->isGoogleAuthenticatorEnabled();
|
||||
|
||||
return null !== $user->getGroup() && $user->getGroup()->isEnforce2FA() && !$tfa_enabled;
|
||||
}
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber\UserSystem;
|
||||
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Events\SecurityEvent;
|
||||
use App\Events\SecurityEvents;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use R\U2FTwoFactorBundle\Event\RegisterEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* This subscriber is used to write U2F keys to DB, after user added them via GUI.
|
||||
*/
|
||||
final class RegisterU2FSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private bool $demo_mode;
|
||||
private FlashBagInterface $flashBag;
|
||||
private UrlGeneratorInterface $router;
|
||||
|
||||
/**
|
||||
* @var EventDispatcher
|
||||
*/
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $router, EntityManagerInterface $entityManager, SessionInterface $session, EventDispatcherInterface $eventDispatcher, bool $demo_mode)
|
||||
{
|
||||
/** @var Session $session */
|
||||
$this->router = $router;
|
||||
$this->em = $entityManager;
|
||||
$this->demo_mode = $demo_mode;
|
||||
$this->flashBag = $session->getFlashBag();
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'r_u2f_two_factor.register' => 'onRegister',
|
||||
];
|
||||
}
|
||||
|
||||
public function onRegister(RegisterEvent $event): void
|
||||
{
|
||||
//Skip adding of U2F key on demo mode
|
||||
if (!$this->demo_mode) {
|
||||
$user = $event->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new \InvalidArgumentException('Only User objects can be registered for U2F!');
|
||||
}
|
||||
|
||||
$registration = $event->getRegistration();
|
||||
$newKey = new U2FKey();
|
||||
$newKey->fromRegistrationData($registration);
|
||||
$newKey->setUser($user);
|
||||
$newKey->setName($event->getKeyName());
|
||||
|
||||
// persist the new key
|
||||
$this->em->persist($newKey);
|
||||
$this->em->flush();
|
||||
$this->flashBag->add('success', 'tfa_u2f.key_added_successful');
|
||||
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_ADDED);
|
||||
}
|
||||
|
||||
// generate new response, here we redirect the user to the fos user
|
||||
// profile
|
||||
$response = new RedirectResponse($this->router->generate('user_settings'));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
|
@ -106,6 +106,7 @@ class LabelOptionsType extends AbstractType
|
|||
'mode' => 'html-label',
|
||||
'attr' => [
|
||||
'rows' => 4,
|
||||
'data-ck-class' => 'ck-html-label'
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
|
@ -253,7 +253,7 @@ class StructuralEntityType extends AbstractType
|
|||
$html .= $this->getElementNameWithLevelWhitespace($choice, $options, '<span class="picker-level"></span>');
|
||||
|
||||
if ($options['show_fullpath_in_subtext'] && null !== $choice->getParent()) {
|
||||
$html .= '<span class="ms-3 badge rounded-pill bg-secondary float-end"><i class="fa-solid fa-folder-tree"></i> ' . trim(htmlspecialchars($choice->getParent()->getFullPath())) . '</span>';
|
||||
$html .= '<span class="ms-3 badge rounded-pill bg-secondary float-end picker-us"><i class="fa-solid fa-folder-tree"></i> ' . trim(htmlspecialchars($choice->getParent()->getFullPath())) . '</span>';
|
||||
}
|
||||
|
||||
if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) {
|
||||
|
|
|
@ -45,6 +45,23 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
|
|||
*/
|
||||
abstract public function getPartsCount(object $element): int;
|
||||
|
||||
/**
|
||||
* Returns the count of the parts associated with this element and all its children.
|
||||
* Please be aware that this function is pretty slow on large trees!
|
||||
* @param AbstractPartsContainingDBElement $element
|
||||
* @return int
|
||||
*/
|
||||
public function getPartsCountRecursive(AbstractPartsContainingDBElement $element): int
|
||||
{
|
||||
$count = $this->getPartsCount($element);
|
||||
|
||||
foreach ($element->getChildren() as $child) {
|
||||
$count += $this->getPartsCountRecursive($child);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
protected function getPartsByField(object $element, array $order_by, string $field_name): array
|
||||
{
|
||||
if (!$element instanceof AbstractPartsContainingDBElement) {
|
||||
|
|
|
@ -27,16 +27,50 @@ use App\Entity\LabelSystem\LabelOptions;
|
|||
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
|
||||
use Com\Tecnick\Barcode\Barcode;
|
||||
use InvalidArgumentException;
|
||||
use PhpParser\Node\Stmt\Label;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
use Twig\Extra\Html\HtmlExtension;
|
||||
|
||||
final class BarcodeGenerator
|
||||
{
|
||||
private BarcodeContentGenerator $barcodeContentGenerator;
|
||||
|
||||
|
||||
public function __construct(BarcodeContentGenerator $barcodeContentGenerator)
|
||||
{
|
||||
$this->barcodeContentGenerator = $barcodeContentGenerator;
|
||||
}
|
||||
|
||||
public function generateHTMLBarcode(LabelOptions $options, object $target): ?string
|
||||
{
|
||||
$svg = $this->generateSVG($options, $target);
|
||||
$base64 = $this->dataUri($svg, 'image/svg+xml');
|
||||
return '<img src="'.$base64.'" width="100%" style="min-height: 25px;" alt="'. $this->getContent($options, $target) . '" />';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a data URI (RFC 2397).
|
||||
* Based on the Twig implementaion from HTMLExtension
|
||||
*
|
||||
* Length validation is not performed on purpose, validation should
|
||||
* be done before calling this filter.
|
||||
*
|
||||
* @return string The generated data URI
|
||||
*/
|
||||
private function dataUri(string $data, string $mime): string
|
||||
{
|
||||
$repr = 'data:';
|
||||
|
||||
$repr .= $mime;
|
||||
if (0 === strpos($mime, 'text/')) {
|
||||
$repr .= ','.rawurlencode($data);
|
||||
} else {
|
||||
$repr .= ';base64,'.base64_encode($data);
|
||||
}
|
||||
|
||||
return $repr;
|
||||
}
|
||||
|
||||
public function generateSVG(LabelOptions $options, object $target): ?string
|
||||
{
|
||||
$barcode = new Barcode();
|
||||
|
|
|
@ -71,7 +71,7 @@ final class LabelTextReplacer
|
|||
public function replace(string $lines, object $target): string
|
||||
{
|
||||
$patterns = [
|
||||
'/(\[\[[A-Z_]+\]\])/' => function ($match) use ($target) {
|
||||
'/(\[\[[A-Z_0-9]+\]\])/' => function ($match) use ($target) {
|
||||
return $this->handlePlaceholder($match[0], $target);
|
||||
},
|
||||
];
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\LabelSystem\PlaceholderProviders;
|
||||
|
||||
use App\Entity\LabelSystem\LabelOptions;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Services\LabelSystem\BarcodeGenerator;
|
||||
use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator;
|
||||
|
||||
final class BarcodeProvider implements PlaceholderProviderInterface
|
||||
{
|
||||
private BarcodeGenerator $barcodeGenerator;
|
||||
private BarcodeContentGenerator $barcodeContentGenerator;
|
||||
|
||||
public function __construct(BarcodeGenerator $barcodeGenerator, BarcodeContentGenerator $barcodeContentGenerator)
|
||||
{
|
||||
$this->barcodeGenerator = $barcodeGenerator;
|
||||
$this->barcodeContentGenerator = $barcodeContentGenerator;
|
||||
}
|
||||
|
||||
public function replace(string $placeholder, object $label_target, array $options = []): ?string
|
||||
{
|
||||
if ('[[1D_CONTENT]]' === $placeholder) {
|
||||
try {
|
||||
return $this->barcodeContentGenerator->get1DBarcodeContent($label_target);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return 'ERROR!';
|
||||
}
|
||||
}
|
||||
|
||||
if ('[[2D_CONTENT]]' === $placeholder) {
|
||||
try {
|
||||
return $this->barcodeContentGenerator->getURLContent($label_target);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return 'ERROR!';
|
||||
}
|
||||
}
|
||||
|
||||
if ('[[BARCODE_QR]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType('qr');
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C39]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType('code39');
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
if ('[[BARCODE_C128]]' === $placeholder) {
|
||||
$label_options = new LabelOptions();
|
||||
$label_options->setBarcodeType('code128');
|
||||
return $this->barcodeGenerator->generateHTMLBarcode($label_options, $label_target);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -27,6 +27,8 @@ use App\Entity\UserSystem\User;
|
|||
use DateTime;
|
||||
use IntlDateFormatter;
|
||||
use Locale;
|
||||
use Symfony\Component\Routing\Generator\UrlGenerator;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
|
@ -36,11 +38,13 @@ final class GlobalProviders implements PlaceholderProviderInterface
|
|||
{
|
||||
private string $partdb_title;
|
||||
private Security $security;
|
||||
private UrlGeneratorInterface $url_generator;
|
||||
|
||||
public function __construct(string $partdb_title, Security $security)
|
||||
public function __construct(string $partdb_title, Security $security, UrlGeneratorInterface $url_generator)
|
||||
{
|
||||
$this->partdb_title = $partdb_title;
|
||||
$this->security = $security;
|
||||
$this->url_generator = $url_generator;
|
||||
}
|
||||
|
||||
public function replace(string $placeholder, object $label_target, array $options = []): ?string
|
||||
|
@ -101,6 +105,10 @@ final class GlobalProviders implements PlaceholderProviderInterface
|
|||
return $formatter->format($now);
|
||||
}
|
||||
|
||||
if ('[[INSTANCE_URL]]' === $placeholder) {
|
||||
return $this->url_generator->generate('homepage', [], UrlGenerator::ABSOLUTE_URL);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ use Doctrine\Common\Collections\Collection;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use DoctrineExtensions\Query\Mysql\Date;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
|
@ -171,6 +172,22 @@ class TimeTravel
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function decodes the array which is created during the json_encode of a datetime object and returns a DateTime object.
|
||||
* @param array $input
|
||||
* @return DateTime
|
||||
* @throws Exception
|
||||
*/
|
||||
private function dateTimeDecode(?array $input): ?\DateTime
|
||||
{
|
||||
//Allow null values
|
||||
if ($input === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new \DateTime($input['date'], new \DateTimeZone($input['timezone']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the changeset in the given LogEntry to the element.
|
||||
*
|
||||
|
@ -195,6 +212,10 @@ class TimeTravel
|
|||
$data = BigDecimal::of($data);
|
||||
}
|
||||
|
||||
if (!$data instanceof DateTime && ('datetime' === $metadata->getFieldMapping($field)['type'])) {
|
||||
$data = $this->dateTimeDecode($data);
|
||||
}
|
||||
|
||||
$this->setField($element, $field, $data);
|
||||
}
|
||||
if ($metadata->hasAssociation($field)) {
|
||||
|
|
|
@ -76,6 +76,10 @@ class BackupCodeManager
|
|||
return;
|
||||
}
|
||||
|
||||
if ($user->isWebAuthnAuthenticatorEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->setBackupCodes([]);
|
||||
}
|
||||
|
||||
|
|
37
src/Services/Trees/SidebarTreeUpdater.php
Normal file
37
src/Services/Trees/SidebarTreeUpdater.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Trees;
|
||||
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
use Symfony\Contracts\Cache\TagAwareCacheInterface;
|
||||
|
||||
final class SidebarTreeUpdater
|
||||
{
|
||||
private const CACHE_KEY = 'sidebar_tree_updated';
|
||||
private const TTL = 60 * 60 * 24; // 24 hours
|
||||
|
||||
private CacheInterface $cache;
|
||||
|
||||
public function __construct(TagAwareCacheInterface $treeCache)
|
||||
{
|
||||
$this->cache = $treeCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when the sidebar tree was updated the last time.
|
||||
* The frontend uses this information to reload the sidebar tree.
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getLastTreeUpdate(): \DateTimeInterface
|
||||
{
|
||||
return $this->cache->get(self::CACHE_KEY, function (ItemInterface $item) {
|
||||
$item->expiresAfter(self::TTL);
|
||||
|
||||
//This tag and therfore this whole cache gets cleared by TreeCacheInvalidationListener when a structural element is changed
|
||||
$item->tag('sidebar_tree_update');
|
||||
|
||||
return new \DateTime();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -73,13 +73,9 @@ class UserCacheKeyGenerator
|
|||
$user = $this->security->getUser();
|
||||
}
|
||||
|
||||
if (!$user instanceof User){
|
||||
throw new \RuntimeException('UserCacheKeyGenerator::generateKey() was called without a user, but no user is logged in!');
|
||||
}
|
||||
|
||||
//If the user is null, then treat it as anonymous user.
|
||||
//When the anonymous user is passed as user then use this path too.
|
||||
if (null === $user || User::ID_ANONYMOUS === $user->getID()) {
|
||||
if (null === $user || !($user instanceof User) || User::ID_ANONYMOUS === $user->getID()) {
|
||||
return 'user$_'.User::ID_ANONYMOUS;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue