From a889ff2b7ffdbe4965294d9d73fa8fafa7faef53 Mon Sep 17 00:00:00 2001 From: Levy-Y <65444028+Levy-Y@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:54:26 +0100 Subject: [PATCH 1/5] Removed redundant groups listing from GroupsEndpoint --- .../Endpoints/GroupsEndpoint.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/main/java/io/fleetcoreplatform/Endpoints/GroupsEndpoint.java b/src/main/java/io/fleetcoreplatform/Endpoints/GroupsEndpoint.java index 610f772..905da17 100644 --- a/src/main/java/io/fleetcoreplatform/Endpoints/GroupsEndpoint.java +++ b/src/main/java/io/fleetcoreplatform/Endpoints/GroupsEndpoint.java @@ -8,6 +8,7 @@ import io.fleetcoreplatform.Models.GroupRequestModel; import io.fleetcoreplatform.Models.UpdateGroupOutpostModel; import io.fleetcoreplatform.Services.CoreService; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.faulttolerance.api.RateLimit; import jakarta.inject.Inject; import jakarta.ws.rs.*; @@ -28,24 +29,6 @@ public class GroupsEndpoint { @Inject CoreService coreService; Logger logger = Logger.getLogger(GroupsEndpoint.class.getName()); - @GET - @Path("/list/{outpost_uuid}") - @Produces(MediaType.APPLICATION_JSON) - @RateLimit(value = 10, window = 5, windowUnit = ChronoUnit.SECONDS) - public Response listGroups(@PathParam("outpost_uuid") UUID outpost_uuid) { - try { - List groups = groupMapper.listGroupsByOutpostUuid(outpost_uuid); - if (groups == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - return Response.ok(groups).build(); - } catch (Exception e) { - logger.severe(e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - } - @POST @Path("/create/") @Consumes(MediaType.APPLICATION_JSON) From 7006ef1132381e639567b4b6c5cb57549c9707e2 Mon Sep 17 00:00:00 2001 From: Levy-Y <65444028+Levy-Y@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:11:59 +0100 Subject: [PATCH 2/5] Added superadmin configuration option. Added superadmin annotation to coordinators endpoint. --- .../io/fleetcoreplatform/Endpoints/CoordinatorsEndpoint.java | 2 +- src/main/resources/application.example.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/fleetcoreplatform/Endpoints/CoordinatorsEndpoint.java b/src/main/java/io/fleetcoreplatform/Endpoints/CoordinatorsEndpoint.java index 8b11645..5b756ed 100644 --- a/src/main/java/io/fleetcoreplatform/Endpoints/CoordinatorsEndpoint.java +++ b/src/main/java/io/fleetcoreplatform/Endpoints/CoordinatorsEndpoint.java @@ -17,7 +17,7 @@ @NoCache @Path("/api/v1/coordinators/") -// @RolesAllowed("${allowed.role-name}") +//@RolesAllowed("${allowed.superadmin.role-name}") public class CoordinatorsEndpoint { @Inject CoordinatorMapper coordinatorMapper; @Inject CoreService coreService; diff --git a/src/main/resources/application.example.properties b/src/main/resources/application.example.properties index c566e26..77fe9db 100644 --- a/src/main/resources/application.example.properties +++ b/src/main/resources/application.example.properties @@ -26,6 +26,7 @@ quarkus.oidc.application-type=service quarkus.oidc.authentication.verify-access-token=true quarkus.oidc.roles.role-claim-path=cognito:groups allowed.role-name=aws_cognito_group_name +allowed.superadmin.role-name=system_admin_name # AWS CloudWatch Logging - Configuration # Remove %prod. to enable in dev/testing mode From 40d60f4246da162c5700087234a3f3c20e3d08f9 Mon Sep 17 00:00:00 2001 From: Levy-Y <65444028+Levy-Y@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:46:27 +0100 Subject: [PATCH 3/5] Added nix flake for an easy to install development environment. --- .dockerignore | 3 ++- .envrc | 1 + .gitignore | 3 +++ flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 20 ++++++++++++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.dockerignore b/.dockerignore index a5b4947..74f7d89 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,5 @@ !src/main/docker/* !Makefile !helper.sh -!README.md \ No newline at end of file +!README.md +.direnv/ \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index c306889..cee83ae 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ nb-configuration.xml /.quarkus/cli/plugins/ # TLS Certificates .certs/ + +# Direnv +.direnv/ \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..eee0f29 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1765472234, + "narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..44625f2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,20 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, ...}: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + graalvmPackages.graalvm-ce + quarkus + maven + awscli2 + ]; + }; + }; +} \ No newline at end of file From 18bfbf35049a618df0d1b3a4380e25c34918cef6 Mon Sep 17 00:00:00 2001 From: Levy-Y <65444028+Levy-Y@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:53:29 +0100 Subject: [PATCH 4/5] Refactored missions endpoint to use the cognito identity for accessing resources. Changed the api paths to comply with naming conventions. --- .../Endpoints/MissionsEndpoint.java | 83 ++++++++++++++++--- .../Database/Mappers/CoordinatorMapper.java | 3 + .../Database/Mappers/MissionMapper.java | 9 ++ .../Managers/IoTCore/IotManager.java | 31 ++++--- .../Models/CreateMissionRequestModel.java | 2 +- .../DroneExecutionStatusResponseModel.java | 3 +- .../Models/MissionExecutionStatusModel.java | 7 ++ .../Services/CoreService.java | 34 +++++--- 8 files changed, 134 insertions(+), 38 deletions(-) create mode 100644 src/main/java/io/fleetcoreplatform/Models/MissionExecutionStatusModel.java diff --git a/src/main/java/io/fleetcoreplatform/Endpoints/MissionsEndpoint.java b/src/main/java/io/fleetcoreplatform/Endpoints/MissionsEndpoint.java index cf61b48..20d94b4 100644 --- a/src/main/java/io/fleetcoreplatform/Endpoints/MissionsEndpoint.java +++ b/src/main/java/io/fleetcoreplatform/Endpoints/MissionsEndpoint.java @@ -1,46 +1,61 @@ package io.fleetcoreplatform.Endpoints; +import io.fleetcoreplatform.Managers.Database.DbModels.DbCoordinator; +import io.fleetcoreplatform.Managers.Database.DbModels.DbMission; +import io.fleetcoreplatform.Managers.Database.Mappers.CoordinatorMapper; +import io.fleetcoreplatform.Managers.Database.Mappers.MissionMapper; import io.fleetcoreplatform.Managers.SQS.QueueManager; import io.fleetcoreplatform.Models.CreateMissionRequestModel; import io.fleetcoreplatform.Models.DroneExecutionStatusResponseModel; import io.fleetcoreplatform.Models.MissionCreatedResponseModel; +import io.fleetcoreplatform.Models.MissionExecutionStatusModel; import io.fleetcoreplatform.Services.CoreService; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.faulttolerance.api.RateLimit; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.UUID; import org.jboss.logging.Logger; import software.amazon.awssdk.services.iot.model.IotException; @Path("/api/v1/missions") -// @RolesAllowed("${allowed.role-name}") +@RolesAllowed("${allowed.role-name}") public class MissionsEndpoint { @Inject CoreService coreService; @Inject QueueManager sqsManager; + @Inject MissionMapper missionMapper; + @Inject SecurityIdentity identity; + + @Inject CoordinatorMapper coordinatorMapper; @Inject Logger logger; @POST - @Path("/start") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RateLimit(value = 2, window = 10, windowUnit = ChronoUnit.MINUTES) public Response startOutpostSurvey(CreateMissionRequestModel body) { if (body == null || body.outpost() == null - || body.groupUUID() == null - || body.coordinatorUUID() == null) { + || body.groupUUID() == null) { return Response.status(Response.Status.BAD_REQUEST).build(); } + DbCoordinator coordinator = coordinatorMapper.findByCognitoSub(identity.getAttribute("sub")); + if (coordinator == null) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + try { UUID missionUUID = coreService.createNewMission( body.outpost(), body.groupUUID(), - body.coordinatorUUID(), + coordinator.getUuid(), body.altitude()); return Response.ok(new MissionCreatedResponseModel(missionUUID)).build(); @@ -56,23 +71,67 @@ public Response startOutpostSurvey(CreateMissionRequestModel body) { } } - @POST - @Path("/cancel/{mission_uuid}") + @PATCH + @Path("/cancel/{mission-uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response cancelMission(@PathParam("mission_uuid") UUID missionUUID) { + public Response cancelMission(@PathParam("mission-uuid") UUID missionUUID) { // TODO: Implement mission cancel logic (#17) return Response.status(Response.Status.NOT_IMPLEMENTED).build(); } @GET - @Path("/status/{drone_uuid}") - @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response getMissionStatus(@PathParam("drone_uuid") UUID droneUUID) + public Response getAllMissions() { + String cognitoSub = identity.getPrincipal().getName(); + + try { + List outposts = missionMapper.listByCoordinator(cognitoSub); + if (outposts == null) { + return Response.status(Response.Status.NO_CONTENT).build(); + } + + return Response.ok(outposts).build(); + + } catch (NotFoundException nfe) { + return Response.status(Response.Status.NOT_FOUND).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + @GET + @Path("/{mission-uuid}") + @Produces(MediaType.APPLICATION_JSON) + public Response getMissionStatus(@PathParam("mission-uuid") UUID missionUUID) throws NotFoundException { + String cognitoSub = identity.getPrincipal().getName(); + + try { + MissionExecutionStatusModel status = coreService.getMissionStatus(missionUUID, cognitoSub); + + if (status == null) { + return Response.status(Response.Status.NO_CONTENT).build(); + } + + return Response.ok(status).build(); + + } catch (NotFoundException nfe) { + return Response.status(Response.Status.NOT_FOUND).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + @GET + @Path("/{mission-uuid}/{drone-uuid}") + @Produces(MediaType.APPLICATION_JSON) + public Response getThingMissionStatus(@PathParam("mission-uuid") UUID missionUUID, @PathParam("drone-uuid") UUID droneUUID) + throws NotFoundException { + String cognitoSub = identity.getPrincipal().getName(); + try { - DroneExecutionStatusResponseModel status = coreService.getMissionStatus(droneUUID); + DroneExecutionStatusResponseModel status = coreService.getThingMissionStatus(droneUUID, missionUUID, cognitoSub); if (status == null) { return Response.status(Response.Status.NO_CONTENT).build(); diff --git a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/CoordinatorMapper.java b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/CoordinatorMapper.java index 36ae82b..fb0118d 100644 --- a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/CoordinatorMapper.java +++ b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/CoordinatorMapper.java @@ -24,6 +24,9 @@ void insert( @Select("SELECT * FROM coordinators WHERE uuid = #{uuid, jdbcType=OTHER}") DbCoordinator findByUuid(@Param("uuid") UUID uuid); + @Select("SELECT * FROM coordinators WHERE cognito_sub = #{uuid}") + DbCoordinator findByCognitoSub(@Param("cognito_sub") String sub); + @UpdateProvider(type = DbCoordinatorUpdateProvider.class, method = "update") void update(@Param("uuid") UUID uuid, @Param("coordinator") UpdateCoordinatorModel coordinator); diff --git a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/MissionMapper.java b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/MissionMapper.java index 8794a5e..a702372 100644 --- a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/MissionMapper.java +++ b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/MissionMapper.java @@ -2,6 +2,7 @@ import io.fleetcoreplatform.Managers.Database.DbModels.DbMission; import java.sql.Timestamp; +import java.util.List; import java.util.UUID; import org.apache.ibatis.annotations.*; @@ -36,4 +37,12 @@ void update( @Delete("DELETE FROM missions WHERE uuid = #{uuid, jdbcType=OTHER}") void delete(@Param("uuid") UUID uuid); + + @Select("SELECT m.* FROM missions m INNER JOIN public.coordinators c ON m.created_by = c.uuid " + + "WHERE m.uuid = #{uuid, jdbcType=OTHER} AND c.cognito_sub = #{sub}") + DbMission findByIdAndCoordinator(@Param("uuid") UUID uuid, @Param("sub") String sub); + + @Select("SELECT m.* FROM missions m INNER JOIN public.coordinators c ON m.created_by = c.uuid " + + "WHERE c.cognito_sub = #{sub}") + List listByCoordinator(@Param("sub") String sub); } diff --git a/src/main/java/io/fleetcoreplatform/Managers/IoTCore/IotManager.java b/src/main/java/io/fleetcoreplatform/Managers/IoTCore/IotManager.java index 29f8b11..f41cde7 100644 --- a/src/main/java/io/fleetcoreplatform/Managers/IoTCore/IotManager.java +++ b/src/main/java/io/fleetcoreplatform/Managers/IoTCore/IotManager.java @@ -72,19 +72,9 @@ public void attachPolicyToCertificate(String certificateARN, String policyARN) { iotAsyncClient.attachPolicy(attachPolicyRequest).join(); } - public JobExecution getJobExecutionStatus(String jobId, String thingName) { - DescribeJobExecutionRequest describeJobExecutionRequest = - DescribeJobExecutionRequest.builder().jobId(jobId).thingName(thingName).build(); - - CompletableFuture response = - iotAsyncClient.describeJobExecution(describeJobExecutionRequest); - - return response.join().execution(); - } - - public String getLastJobId(String thingName) { + public JobExecutionSummary getThingJob(String thingName, String jobId) { ListJobExecutionsForThingRequest listJobExecutionsForThingRequest = - ListJobExecutionsForThingRequest.builder().thingName(thingName).build(); + ListJobExecutionsForThingRequest.builder().jobId(jobId).thingName(thingName).build(); CompletableFuture future = iotAsyncClient.listJobExecutionsForThing(listJobExecutionsForThingRequest); @@ -92,7 +82,22 @@ public String getLastJobId(String thingName) { try { ListJobExecutionsForThingResponse response = future.join(); - return response.executionSummaries().getFirst().jobId(); + return response.executionSummaries().getFirst().jobExecutionSummary(); + } catch (Exception e) { + return null; + } + } + + public Job getJob(String jobId) { + DescribeJobRequest request = + DescribeJobRequest.builder().jobId(jobId).build(); + + CompletableFuture future = + iotAsyncClient.describeJob(request); + try { + DescribeJobResponse response = future.join(); + + return response.job(); } catch (Exception e) { return null; } diff --git a/src/main/java/io/fleetcoreplatform/Models/CreateMissionRequestModel.java b/src/main/java/io/fleetcoreplatform/Models/CreateMissionRequestModel.java index 025191e..8463107 100644 --- a/src/main/java/io/fleetcoreplatform/Models/CreateMissionRequestModel.java +++ b/src/main/java/io/fleetcoreplatform/Models/CreateMissionRequestModel.java @@ -3,4 +3,4 @@ import java.util.UUID; public record CreateMissionRequestModel( - String outpost, UUID groupUUID, UUID coordinatorUUID, int altitude) {} + String outpost, UUID groupUUID, int altitude) {} diff --git a/src/main/java/io/fleetcoreplatform/Models/DroneExecutionStatusResponseModel.java b/src/main/java/io/fleetcoreplatform/Models/DroneExecutionStatusResponseModel.java index f765f3b..a4d8afa 100644 --- a/src/main/java/io/fleetcoreplatform/Models/DroneExecutionStatusResponseModel.java +++ b/src/main/java/io/fleetcoreplatform/Models/DroneExecutionStatusResponseModel.java @@ -1,8 +1,9 @@ package io.fleetcoreplatform.Models; +import java.sql.Timestamp; import java.time.Instant; import java.util.UUID; import software.amazon.awssdk.services.iot.model.JobExecutionStatus; public record DroneExecutionStatusResponseModel( - UUID drone_uuid, JobExecutionStatus status, Instant started_at) {} + UUID droneUUID, JobExecutionStatus status, Timestamp startedAtt) {} diff --git a/src/main/java/io/fleetcoreplatform/Models/MissionExecutionStatusModel.java b/src/main/java/io/fleetcoreplatform/Models/MissionExecutionStatusModel.java new file mode 100644 index 0000000..ec4757f --- /dev/null +++ b/src/main/java/io/fleetcoreplatform/Models/MissionExecutionStatusModel.java @@ -0,0 +1,7 @@ +package io.fleetcoreplatform.Models; + +import software.amazon.awssdk.services.iot.model.JobStatus; + +import java.sql.Timestamp; + +public record MissionExecutionStatusModel(JobStatus jobStatus, Timestamp startedAt, Timestamp finishedAt) {} diff --git a/src/main/java/io/fleetcoreplatform/Services/CoreService.java b/src/main/java/io/fleetcoreplatform/Services/CoreService.java index 3fda6a2..184aa54 100644 --- a/src/main/java/io/fleetcoreplatform/Services/CoreService.java +++ b/src/main/java/io/fleetcoreplatform/Services/CoreService.java @@ -3,10 +3,7 @@ import io.fleetcoreplatform.Configs.ApplicationConfig; import io.fleetcoreplatform.Exceptions.GroupNotEmptyException; import io.fleetcoreplatform.Managers.Cognito.CognitoManager; -import io.fleetcoreplatform.Managers.Database.DbModels.DbCoordinator; -import io.fleetcoreplatform.Managers.Database.DbModels.DbDrone; -import io.fleetcoreplatform.Managers.Database.DbModels.DbGroup; -import io.fleetcoreplatform.Managers.Database.DbModels.DbOutpost; +import io.fleetcoreplatform.Managers.Database.DbModels.*; import io.fleetcoreplatform.Managers.Database.Mappers.*; import io.fleetcoreplatform.Managers.IoTCore.IotDataPlaneManager; import io.fleetcoreplatform.Managers.IoTCore.IotManager; @@ -25,7 +22,8 @@ import java.util.*; import org.jboss.logging.Logger; import org.postgis.Geometry; -import software.amazon.awssdk.services.iot.model.JobExecution; +import software.amazon.awssdk.services.iot.model.Job; +import software.amazon.awssdk.services.iot.model.JobExecutionSummary; @Startup @ApplicationScoped @@ -360,8 +358,13 @@ private static ArrayList getDroneIdentities( return droneIdentities; } - public DroneExecutionStatusResponseModel getMissionStatus(UUID droneUuid) + public DroneExecutionStatusResponseModel getThingMissionStatus(UUID droneUuid, UUID missionUuid, String sub) throws NotFoundException { + DbMission mission = missionMapper.findByIdAndCoordinator(missionUuid, sub); + if (mission == null) { + throw new NotFoundException("Mission not found with UUID " + missionUuid.toString()); + } + DbDrone drone = droneMapper.findByUuid(droneUuid); if (drone == null) { throw new NotFoundException("Drone not found with UUID " + droneUuid.toString()); @@ -369,14 +372,23 @@ public DroneExecutionStatusResponseModel getMissionStatus(UUID droneUuid) String thingName = drone.getName(); - String lastJopId = iotManager.getLastJobId(thingName); - if (lastJopId == null) { + JobExecutionSummary executionSummary = iotManager.getThingJob(thingName, mission.getName()); + if (executionSummary == null) { return null; } - JobExecution jobStatus = iotManager.getJobExecutionStatus(lastJopId, thingName); + return new DroneExecutionStatusResponseModel(droneUuid, executionSummary.status(), Timestamp.from(executionSummary.lastUpdatedAt())); + } + + public MissionExecutionStatusModel getMissionStatus(UUID missionUuid, String sub) + throws NotFoundException { + DbMission mission = missionMapper.findByIdAndCoordinator(missionUuid, sub); + if (mission == null) { + throw new NotFoundException("Mission not found with UUID " + missionUuid.toString()); + } + + Job job = iotManager.getJob(mission.getName()); - return new DroneExecutionStatusResponseModel( - droneUuid, jobStatus.status(), jobStatus.startedAt()); + return new MissionExecutionStatusModel(job.status(), mission.getStart_time(), Timestamp.from(job.completedAt())); } } From 66b07b699c3ef3e369f27c90774b2b8fc3d37450 Mon Sep 17 00:00:00 2001 From: Levy-Y <65444028+Levy-Y@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:28:12 +0100 Subject: [PATCH 5/5] Started refactoring endpoints to include row-level authentication. --- .../Endpoints/DronesEndpoint.java | 8 +++- .../Endpoints/OutpostsEndpoint.java | 47 ++++++++++++++----- .../Database/Mappers/DroneMapper.java | 22 +++++++++ .../Database/Mappers/GroupMapper.java | 11 +++++ .../Database/Mappers/OutpostMapper.java | 23 +++++++++ 5 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/fleetcoreplatform/Endpoints/DronesEndpoint.java b/src/main/java/io/fleetcoreplatform/Endpoints/DronesEndpoint.java index a336b6f..5fd58ef 100644 --- a/src/main/java/io/fleetcoreplatform/Endpoints/DronesEndpoint.java +++ b/src/main/java/io/fleetcoreplatform/Endpoints/DronesEndpoint.java @@ -6,6 +6,7 @@ import io.fleetcoreplatform.Models.IoTCertContainer; import io.fleetcoreplatform.Models.SetDroneGroupRequestModel; import io.fleetcoreplatform.Services.CoreService; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.faulttolerance.api.RateLimit; import jakarta.inject.Inject; import jakarta.ws.rs.*; @@ -22,12 +23,15 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.jboss.resteasy.reactive.NoCache; +// TODO: Implement row-level authentication on drones endpoint (#29) + @NoCache @Path("/api/v1/drones/") // @RolesAllowed("${allowed.role-name}") public class DronesEndpoint { @Inject CoreService coreService; @Inject DroneMapper droneMapper; + @Inject SecurityIdentity identity; Logger logger = Logger.getLogger(DronesEndpoint.class.getName()); @GET @@ -53,12 +57,14 @@ public Response listGroupDrones( @DefaultValue("10") @QueryParam("limit") int limit, @PathParam("group_uuid") UUID group_uuid) { + String cognitoSub = identity.getAttribute("sub").toString(); + if (limit <= 0 || limit > 1000) { return Response.status(Response.Status.BAD_REQUEST).build(); } try { - List drones = droneMapper.listDronesByGroupUuid(group_uuid, limit); + List drones = droneMapper.listDronesByGroupAndCoordinator(group_uuid, cognitoSub, limit); if (drones == null) { return Response.status(Response.Status.NOT_FOUND).build(); } diff --git a/src/main/java/io/fleetcoreplatform/Endpoints/OutpostsEndpoint.java b/src/main/java/io/fleetcoreplatform/Endpoints/OutpostsEndpoint.java index 07be183..dc700ea 100644 --- a/src/main/java/io/fleetcoreplatform/Endpoints/OutpostsEndpoint.java +++ b/src/main/java/io/fleetcoreplatform/Endpoints/OutpostsEndpoint.java @@ -1,15 +1,16 @@ package io.fleetcoreplatform.Endpoints; -import io.fleetcoreplatform.Managers.Database.DbModels.DbGroup; import io.fleetcoreplatform.Managers.Database.DbModels.DbOutpost; -import io.fleetcoreplatform.Managers.Database.Mappers.GroupMapper; import io.fleetcoreplatform.Managers.Database.Mappers.OutpostMapper; import io.fleetcoreplatform.Models.CreateOutpostModel; +import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.faulttolerance.api.RateLimit; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; + +import java.net.URI; import java.sql.Timestamp; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -18,16 +19,17 @@ import java.util.stream.Collectors; import org.jboss.logging.Logger; +// TODO: Implement row-level authentication on outposts endpoint (#28) + @Path("/api/v1/outposts") // @RolesAllowed("${allowed.role-name}") public class OutpostsEndpoint { @Inject OutpostMapper outpostMapper; - @Inject GroupMapper groupMapper; + @Inject SecurityIdentity identity; @Inject Logger logger; // TODO: Implement OutpostsEndpoint (#24) @POST - @Path("/create/") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @RateLimit(value = 3, window = 1, windowUnit = ChronoUnit.MINUTES) @@ -61,7 +63,8 @@ public Response createOutpost(CreateOutpostModel body) { areaWkt, body.coordinatorUUID(), timestamp); - return Response.noContent().build(); + + return Response.created(URI.create(String.format("/api/v1/outposts/%s", uuid))).build(); } catch (Exception e) { logger.error("Error while inserting outpost", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); @@ -69,23 +72,43 @@ public Response createOutpost(CreateOutpostModel body) { } @GET - @Path("/list/{outpost_uuid}") + @Path("/{outpost-uuid}") @Produces(MediaType.APPLICATION_JSON) - public Response listOutpostGroups(@PathParam("outpost_uuid") UUID outpostUUID) { + public Response getOutpost(@PathParam("outpost-uuid") UUID outpostUUID) { try { - DbOutpost outpost = outpostMapper.findByUuid(outpostUUID); + String cognitoSub = identity.getPrincipal().getName(); + + DbOutpost outpost = outpostMapper.findByUuidAndCoordinator(outpostUUID, cognitoSub); if (outpost == null) { return Response.status(Response.Status.NOT_FOUND).build(); } - List groupList = groupMapper.listGroupsByOutpostUuid(outpostUUID); - if (groupList.isEmpty()) { + return Response.ok(outpost).build(); + + } catch (Exception e) { + logger.error("Error while listing outpost groups", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response listCoordinatorOutposts() { + try { + String cognitoSub = identity.getPrincipal().getName(); + + List outposts = outpostMapper.listByCoordinator(cognitoSub); + if (outposts == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + if (outposts.isEmpty()) { return Response.noContent().build(); } else { - return Response.ok(groupList).build(); + return Response.ok(outposts).build(); } } catch (Exception e) { - logger.error("Error while listing outpost groups", e); + logger.error("Error while listing outposts", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } } diff --git a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/DroneMapper.java b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/DroneMapper.java index 05ceedf..a145c9e 100644 --- a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/DroneMapper.java +++ b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/DroneMapper.java @@ -47,6 +47,28 @@ public interface DroneMapper { List listDronesByGroupUuid( @Param("groupUuid") UUID groupUuid, @Param("limit") int limit); + @Select( + """ + SELECT d.uuid, d.name, d.group_uuid, d.address, d.manager_version, d.first_discovered, d.home_position + FROM drones d + INNER JOIN groups g ON d.group_uuid = g.uuid + INNER JOIN outposts o ON g.outpost_uuid = o.uuid + INNER JOIN coordinators c ON o.created_by = c.uuid + WHERE d.group_uuid = #{groupUuid, jdbcType=OTHER} + AND c.cognito_sub = #{cognitoSub} + LIMIT ${limit} + """) + @Results({ + @Result( + property = "home_position", + column = "home_position", + typeHandler = GeometryTypeHandler.class) + }) + List listDronesByGroupAndCoordinator( + @Param("groupUuid") UUID groupUuid, + @Param("cognitoSub") String cognitoSub, + @Param("limit") int limit); + @Select( "SELECT uuid, name, group_uuid, address, manager_version, first_discovered, " + "home_position FROM drones WHERE name = #{name}") diff --git a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/GroupMapper.java b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/GroupMapper.java index 455da2a..8b1f09f 100644 --- a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/GroupMapper.java +++ b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/GroupMapper.java @@ -23,6 +23,17 @@ void insert( @Select("SELECT * FROM groups WHERE outpost_uuid = #{outpost_uuid, jdbcType=OTHER}") List listGroupsByOutpostUuid(@Param("outpost_uuid") UUID uuid); + @Select(""" + SELECT g.* FROM groups g + INNER JOIN outposts o ON g.outpost_uuid = o.uuid + INNER JOIN coordinators c ON o.created_by = c.uuid + WHERE g.outpost_uuid = #{outpostUuid, jdbcType=OTHER} + AND c.cognito_sub = #{cognitoSub} + """) + List listGroupsByOutpostUuidAndCoordinator( + @Param("outpostUuid") UUID outpostUuid, + @Param("cognitoSub") String cognitoSub); + @Select("SELECT * FROM groups WHERE name = #{name}") DbGroup findByName(@Param("name") String name); diff --git a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/OutpostMapper.java b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/OutpostMapper.java index 870090c..dd14000 100644 --- a/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/OutpostMapper.java +++ b/src/main/java/io/fleetcoreplatform/Managers/Database/Mappers/OutpostMapper.java @@ -4,6 +4,7 @@ import io.fleetcoreplatform.Managers.Database.TypeHandlers.GeometryTypeHandler; import java.math.BigDecimal; import java.sql.Timestamp; +import java.util.List; import java.util.UUID; import org.apache.ibatis.annotations.*; @@ -28,6 +29,28 @@ void insert( @Results({@Result(property = "area", column = "area", typeHandler = GeometryTypeHandler.class)}) DbOutpost findByUuid(@Param("uuid") UUID uuid); + @Select(""" + SELECT o.uuid, o.name, o.latitude, o.longitude, o.area, o.created_by, o.created_at + FROM outposts o + INNER JOIN coordinators c ON o.created_by = c.uuid + WHERE c.cognito_sub = #{cognitoSub} + """) + @Results({@Result(property = "area", column = "area", typeHandler = GeometryTypeHandler.class)}) + List listByCoordinator( + @Param("cognitoSub") String cognitoSub); + + @Select(""" + SELECT o.uuid, o.name, o.latitude, o.longitude, o.area, o.created_by, o.created_at + FROM outposts o + INNER JOIN coordinators c ON o.created_by = c.uuid + WHERE o.uuid = #{uuid, jdbcType=OTHER} + AND c.cognito_sub = #{cognitoSub} + """) + @Results({@Result(property = "area", column = "area", typeHandler = GeometryTypeHandler.class)}) + DbOutpost findByUuidAndCoordinator( + @Param("uuid") UUID uuid, + @Param("cognitoSub") String cognitoSub); + @Select( "SELECT uuid, name, latitude, longitude, area, created_by," + " created_at FROM outposts WHERE name = #{name}")