| @@ -120,6 +120,7 @@ export class SearchSelectComponent implements OnInit, AfterViewInit { | |||||
| text: 'model.zone', | text: 'model.zone', | ||||
| type: ListComponent.COLUMN_TYPE_TEXT, | type: ListComponent.COLUMN_TYPE_TEXT, | ||||
| subResource: 'zone', | subResource: 'zone', | ||||
| sortingSubResource: 'zone', | |||||
| field: 'name', | field: 'name', | ||||
| sortable: true, | sortable: true, | ||||
| filterType: FilterBarComponent.FILTER_TYPE_TEXT, | filterType: FilterBarComponent.FILTER_TYPE_TEXT, | ||||
| @@ -83,6 +83,14 @@ export class TripListComponent { | |||||
| sortable: true, | sortable: true, | ||||
| filterType: FilterBarComponent.FILTER_TYPE_DATE, | filterType: FilterBarComponent.FILTER_TYPE_DATE, | ||||
| } as ListColDefinition, | } 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', | name: 'createdAt', | ||||
| text: 'common.created_at', | text: 'common.created_at', | ||||
| @@ -41,7 +41,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_DASHBOARD, | path: ROUTE_DASHBOARD, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: '', component: DashboardComponent}, | {path: '', component: DashboardComponent}, | ||||
| ] | ] | ||||
| @@ -49,7 +49,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_BASE_DATA, | path: ROUTE_BASE_DATA, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: '', component: BaseDataComponent}, | {path: '', component: BaseDataComponent}, | ||||
| ] | ] | ||||
| @@ -57,7 +57,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_LOCATIONS, | path: ROUTE_LOCATIONS, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: ':id', component: LocationDetailComponent}, | {path: ':id', component: LocationDetailComponent}, | ||||
| ] | ] | ||||
| @@ -65,7 +65,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_TRIPS, | path: ROUTE_TRIPS, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: '', component: TripComponent}, | {path: '', component: TripComponent}, | ||||
| ] | ] | ||||
| @@ -73,7 +73,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_TRIPS, | path: ROUTE_TRIPS, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: ':id', component: TripDetailComponent}, | {path: ':id', component: TripDetailComponent}, | ||||
| ] | ] | ||||
| @@ -81,7 +81,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_USER_TRIPS, | path: ROUTE_USER_TRIPS, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: '', component: UserTripComponent}, | {path: '', component: UserTripComponent}, | ||||
| ] | ] | ||||
| @@ -89,7 +89,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_USER_TRIPS, | path: ROUTE_USER_TRIPS, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: ':id', component: UserTripDetailComponent}, | {path: ':id', component: UserTripDetailComponent}, | ||||
| ] | ] | ||||
| @@ -97,7 +97,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_ZONES, | path: ROUTE_ZONES, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: ':id', component: ZoneDetailComponent}, | {path: ':id', component: ZoneDetailComponent}, | ||||
| ] | ] | ||||
| @@ -105,7 +105,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_VESSELS, | path: ROUTE_VESSELS, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: ':id', component: VesselDetailComponent}, | {path: ':id', component: VesselDetailComponent}, | ||||
| ] | ] | ||||
| @@ -113,7 +113,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_SHIPPING_COMPANIES, | path: ROUTE_SHIPPING_COMPANIES, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: ':id', component: ShippingCompanyDetailComponent}, | {path: ':id', component: ShippingCompanyDetailComponent}, | ||||
| ] | ] | ||||
| @@ -121,7 +121,7 @@ const routes: Routes = [ | |||||
| { | { | ||||
| path: ROUTE_PROFILE, | path: ROUTE_PROFILE, | ||||
| component: TwoColumnComponent, | component: TwoColumnComponent, | ||||
| canActivate: [userGuard], | |||||
| canActivate: [adminGuard], | |||||
| children: [ | children: [ | ||||
| {path: '', component: ProfileComponent}, | {path: '', component: ProfileComponent}, | ||||
| ] | ] | ||||
| @@ -40,7 +40,8 @@ | |||||
| "save_itinerary": "Save itinerary", | "save_itinerary": "Save itinerary", | ||||
| "assigned_users": "Assigned Pilots", | "assigned_users": "Assigned Pilots", | ||||
| "save_user_assignments": "Save user assignments", | "save_user_assignments": "Save user assignments", | ||||
| "events": "Events" | |||||
| "events": "Events", | |||||
| "completed": "Completed" | |||||
| }, | }, | ||||
| "user_trip": | "user_trip": | ||||
| { | { | ||||
| @@ -18,6 +18,7 @@ use App\Filter\CustomJsonFilter; | |||||
| use App\Filter\CustomJsonOrderFilter; | use App\Filter\CustomJsonOrderFilter; | ||||
| use App\State\EntityClassDtoStateProcessor; | use App\State\EntityClassDtoStateProcessor; | ||||
| use App\State\EntityToDtoStateProvider; | use App\State\EntityToDtoStateProvider; | ||||
| use App\State\UserTripStateProcessor; | |||||
| use Symfony\Component\Validator\Constraints as Assert; | use Symfony\Component\Validator\Constraints as Assert; | ||||
| use Symfony\Component\PropertyInfo\Type; | use Symfony\Component\PropertyInfo\Type; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | use Symfony\Component\Validator\Constraints\NotBlank; | ||||
| @@ -43,7 +44,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; | |||||
| ], | ], | ||||
| security: 'is_granted("ROLE_USER")', | security: 'is_granted("ROLE_USER")', | ||||
| provider: EntityToDtoStateProvider::class, | provider: EntityToDtoStateProvider::class, | ||||
| processor: EntityClassDtoStateProcessor::class, | |||||
| processor: UserTripStateProcessor::class, | |||||
| stateOptions: new Options(entityClass: UserTrip::class), | stateOptions: new Options(entityClass: UserTrip::class), | ||||
| )] | )] | ||||
| #[ApiFilter(SearchFilter::class, properties: [ | #[ApiFilter(SearchFilter::class, properties: [ | ||||
| @@ -156,8 +156,20 @@ class Trip | |||||
| return $this->completed; | 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; | $this->completed = $completed; | ||||
| } | } | ||||
| @@ -193,6 +193,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface | |||||
| $this->isPilot = $isPilot; | $this->isPilot = $isPilot; | ||||
| } | } | ||||
| public function isAdmin() | |||||
| { | |||||
| return in_array(User::ROLE_ADMIN, $this->roles, true); | |||||
| } | |||||
| public function getCreatedAt(): ?\DateTimeImmutable | public function getCreatedAt(): ?\DateTimeImmutable | ||||
| { | { | ||||
| return $this->createdAt; | return $this->createdAt; | ||||
| @@ -1,9 +1,4 @@ | |||||
| <?php | <?php | ||||
| /** | |||||
| * @author Daniel Knudsen <d.knudsen@spawntree.de> | |||||
| * @date 18.01.24 | |||||
| */ | |||||
| namespace App\EventListener; | namespace App\EventListener; | ||||
| @@ -1,15 +1,8 @@ | |||||
| <?php | <?php | ||||
| // src/Security/JwtAuthenticator.php | |||||
| namespace App\Security; | 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 App\Entity\User; | ||||
| use Doctrine\ORM\EntityManagerInterface; | |||||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Symfony\Component\HttpFoundation\Request; | 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\Badge\UserBadge; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | ||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | 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 | class JwtAuthenticator extends AbstractAuthenticator | ||||
| { | { | ||||
| public function __construct( | public function __construct( | ||||
| private JWTTokenManagerInterface $jwtManager | |||||
| private JWTTokenManagerInterface $jwtManager, | |||||
| private EntityManagerInterface $em | |||||
| ) {} | ) {} | ||||
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | ||||
| @@ -51,20 +42,38 @@ class JwtAuthenticator extends AbstractAuthenticator | |||||
| public function authenticate(Request $request): Passport | public function authenticate(Request $request): Passport | ||||
| { | { | ||||
| $data = json_decode($request->getContent(), true); | $data = json_decode($request->getContent(), true); | ||||
| if (!$data) { | if (!$data) { | ||||
| throw new CustomUserMessageAuthenticationException('Invalid JSON.'); | |||||
| throw new CustomUserMessageAuthenticationException('Ungültige JSON-Daten.'); | |||||
| } | } | ||||
| $email = $data['email'] ?? ''; | |||||
| $email = $data['email'] ?? ''; | |||||
| $password = $data['password'] ?? ''; | $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( | return new Passport( | ||||
| new UserBadge($email), | |||||
| $userBadge, | |||||
| new PasswordCredentials($password) | new PasswordCredentials($password) | ||||
| ); | ); | ||||
| } | } | ||||
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | ||||
| { | { | ||||
| return new JsonResponse([ | return new JsonResponse([ | ||||
| @@ -8,6 +8,7 @@ use ApiPlatform\Doctrine\Orm\State\Options; | |||||
| use ApiPlatform\Metadata\DeleteOperationInterface; | use ApiPlatform\Metadata\DeleteOperationInterface; | ||||
| use ApiPlatform\Metadata\Operation; | use ApiPlatform\Metadata\Operation; | ||||
| use ApiPlatform\State\ProcessorInterface; | use ApiPlatform\State\ProcessorInterface; | ||||
| use Doctrine\ORM\EntityManagerInterface; | |||||
| use Symfony\Component\DependencyInjection\Attribute\Autowire; | use Symfony\Component\DependencyInjection\Attribute\Autowire; | ||||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | use Symfonycasts\MicroMapper\MicroMapperInterface; | ||||
| @@ -16,7 +17,8 @@ class EntityClassDtoStateProcessor implements ProcessorInterface | |||||
| public function __construct( | public function __construct( | ||||
| #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | #[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | ||||
| #[Autowire(service: RemoveProcessor::class)] private ProcessorInterface $removeProcessor, | #[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; | |||||
| } | |||||
| } | |||||