| @@ -120,6 +120,7 @@ export class SearchSelectComponent implements OnInit, AfterViewInit { | |||
| text: 'model.zone', | |||
| type: ListComponent.COLUMN_TYPE_TEXT, | |||
| subResource: 'zone', | |||
| sortingSubResource: 'zone', | |||
| field: 'name', | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_TEXT, | |||
| @@ -83,6 +83,14 @@ export class TripListComponent { | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_DATE, | |||
| } as ListColDefinition, | |||
| { | |||
| name: 'completed', | |||
| text: 'trip.completed', | |||
| type: ListComponent.COLUMN_TYPE_BOOLEAN, | |||
| field: 'completed', | |||
| sortable: true, | |||
| filterType: FilterBarComponent.FILTER_TYPE_BOOLEAN, | |||
| } as ListColDefinition, | |||
| { | |||
| name: 'createdAt', | |||
| text: 'common.created_at', | |||
| @@ -41,7 +41,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_DASHBOARD, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: '', component: DashboardComponent}, | |||
| ] | |||
| @@ -49,7 +49,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_BASE_DATA, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: '', component: BaseDataComponent}, | |||
| ] | |||
| @@ -57,7 +57,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_LOCATIONS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: ':id', component: LocationDetailComponent}, | |||
| ] | |||
| @@ -65,7 +65,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_TRIPS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: '', component: TripComponent}, | |||
| ] | |||
| @@ -73,7 +73,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_TRIPS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: ':id', component: TripDetailComponent}, | |||
| ] | |||
| @@ -81,7 +81,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_USER_TRIPS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: '', component: UserTripComponent}, | |||
| ] | |||
| @@ -89,7 +89,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_USER_TRIPS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: ':id', component: UserTripDetailComponent}, | |||
| ] | |||
| @@ -97,7 +97,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_ZONES, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: ':id', component: ZoneDetailComponent}, | |||
| ] | |||
| @@ -105,7 +105,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_VESSELS, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: ':id', component: VesselDetailComponent}, | |||
| ] | |||
| @@ -113,7 +113,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_SHIPPING_COMPANIES, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: ':id', component: ShippingCompanyDetailComponent}, | |||
| ] | |||
| @@ -121,7 +121,7 @@ const routes: Routes = [ | |||
| { | |||
| path: ROUTE_PROFILE, | |||
| component: TwoColumnComponent, | |||
| canActivate: [userGuard], | |||
| canActivate: [adminGuard], | |||
| children: [ | |||
| {path: '', component: ProfileComponent}, | |||
| ] | |||
| @@ -40,7 +40,8 @@ | |||
| "save_itinerary": "Save itinerary", | |||
| "assigned_users": "Assigned Pilots", | |||
| "save_user_assignments": "Save user assignments", | |||
| "events": "Events" | |||
| "events": "Events", | |||
| "completed": "Completed" | |||
| }, | |||
| "user_trip": | |||
| { | |||
| @@ -18,6 +18,7 @@ use App\Filter\CustomJsonFilter; | |||
| use App\Filter\CustomJsonOrderFilter; | |||
| use App\State\EntityClassDtoStateProcessor; | |||
| use App\State\EntityToDtoStateProvider; | |||
| use App\State\UserTripStateProcessor; | |||
| use Symfony\Component\Validator\Constraints as Assert; | |||
| use Symfony\Component\PropertyInfo\Type; | |||
| use Symfony\Component\Validator\Constraints\NotBlank; | |||
| @@ -43,7 +44,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; | |||
| ], | |||
| security: 'is_granted("ROLE_USER")', | |||
| provider: EntityToDtoStateProvider::class, | |||
| processor: EntityClassDtoStateProcessor::class, | |||
| processor: UserTripStateProcessor::class, | |||
| stateOptions: new Options(entityClass: UserTrip::class), | |||
| )] | |||
| #[ApiFilter(SearchFilter::class, properties: [ | |||
| @@ -156,8 +156,20 @@ class Trip | |||
| return $this->completed; | |||
| } | |||
| public function setCompleted(bool $completed): void | |||
| { | |||
| public function setCompleted(): void | |||
| { | |||
| $completed = true; | |||
| if (count($this->userTrips) <= 0) { | |||
| $completed = false; | |||
| } else { | |||
| /** @var UserTrip $userTrip */ | |||
| foreach ($this->userTrips as $userTrip) { | |||
| if (!$userTrip->isCompleted() || !$userTrip->isApproved()) { | |||
| $completed = false; | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| $this->completed = $completed; | |||
| } | |||
| @@ -193,6 +193,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface | |||
| $this->isPilot = $isPilot; | |||
| } | |||
| public function isAdmin() | |||
| { | |||
| return in_array(User::ROLE_ADMIN, $this->roles, true); | |||
| } | |||
| public function getCreatedAt(): ?\DateTimeImmutable | |||
| { | |||
| return $this->createdAt; | |||
| @@ -1,9 +1,4 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @date 18.01.24 | |||
| */ | |||
| namespace App\EventListener; | |||
| @@ -1,15 +1,8 @@ | |||
| <?php | |||
| // src/Security/JwtAuthenticator.php | |||
| namespace App\Security; | |||
| use ApiPlatform\Metadata\Get; | |||
| use ApiPlatform\Metadata\IriConverterInterface; | |||
| use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; | |||
| use ApiPlatform\State\Provider\ContentNegotiationProvider; | |||
| use ApiPlatform\State\SerializerContextBuilderInterface; | |||
| use App\ApiResource\UserApi; | |||
| use App\Entity\MediaObject; | |||
| use App\Entity\User; | |||
| use Doctrine\ORM\EntityManagerInterface; | |||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | |||
| use Symfony\Component\HttpFoundation\JsonResponse; | |||
| use Symfony\Component\HttpFoundation\Request; | |||
| @@ -21,14 +14,12 @@ use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |||
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |||
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | |||
| use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |||
| use Symfony\Component\Serializer\SerializerInterface; | |||
| use Symfony\Contracts\HttpClient\HttpClientInterface; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| class JwtAuthenticator extends AbstractAuthenticator | |||
| { | |||
| public function __construct( | |||
| private JWTTokenManagerInterface $jwtManager | |||
| private JWTTokenManagerInterface $jwtManager, | |||
| private EntityManagerInterface $em | |||
| ) {} | |||
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |||
| @@ -51,20 +42,38 @@ class JwtAuthenticator extends AbstractAuthenticator | |||
| public function authenticate(Request $request): Passport | |||
| { | |||
| $data = json_decode($request->getContent(), true); | |||
| if (!$data) { | |||
| throw new CustomUserMessageAuthenticationException('Invalid JSON.'); | |||
| throw new CustomUserMessageAuthenticationException('Ungültige JSON-Daten.'); | |||
| } | |||
| $email = $data['email'] ?? ''; | |||
| $email = $data['email'] ?? ''; | |||
| $password = $data['password'] ?? ''; | |||
| $userBadge = new UserBadge($email, function(string $userIdentifier) { | |||
| $user = $this->em | |||
| ->getRepository(User::class) | |||
| ->findOneBy(['email' => $userIdentifier]); | |||
| if (!$user) { | |||
| // Standardnachricht für nicht gefundene E-Mail | |||
| throw new CustomUserMessageAuthenticationException('Invalid email or password.'); | |||
| } | |||
| if (!$user->isActive()) { | |||
| // custom Message, erscheint im JSON-Response | |||
| throw new CustomUserMessageAuthenticationException('Your account is currently deactivated.'); | |||
| } | |||
| return $user; | |||
| }); | |||
| return new Passport( | |||
| new UserBadge($email), | |||
| $userBadge, | |||
| new PasswordCredentials($password) | |||
| ); | |||
| } | |||
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |||
| { | |||
| return new JsonResponse([ | |||
| @@ -8,6 +8,7 @@ use ApiPlatform\Doctrine\Orm\State\Options; | |||
| use ApiPlatform\Metadata\DeleteOperationInterface; | |||
| use ApiPlatform\Metadata\Operation; | |||
| use ApiPlatform\State\ProcessorInterface; | |||
| use Doctrine\ORM\EntityManagerInterface; | |||
| use Symfony\Component\DependencyInjection\Attribute\Autowire; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| @@ -16,7 +17,8 @@ class EntityClassDtoStateProcessor implements ProcessorInterface | |||
| public function __construct( | |||
| #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | |||
| #[Autowire(service: RemoveProcessor::class)] private ProcessorInterface $removeProcessor, | |||
| private MicroMapperInterface $microMapper | |||
| private MicroMapperInterface $microMapper, | |||
| protected EntityManagerInterface $em | |||
| ) | |||
| {} | |||
| @@ -0,0 +1,31 @@ | |||
| <?php | |||
| /** | |||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||
| * @date 29.04.25 | |||
| */ | |||
| namespace App\State; | |||
| use ApiPlatform\Metadata\Operation; | |||
| use App\Entity\Trip; | |||
| class UserTripStateProcessor extends EntityClassDtoStateProcessor | |||
| { | |||
| public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |||
| { | |||
| $res = parent::process($data, $operation, $uriVariables, $context); | |||
| /** @var Trip $trip */ | |||
| $trip = $this->em->getRepository(Trip::class)->find($data->tripIri->id); | |||
| $trip->setCompleted(); | |||
| $this->em->flush(); | |||
| return $res; | |||
| } | |||
| } | |||