diff --git a/pom.xml b/pom.xml
index d0131965f..26fce5f3e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,6 +125,13 @@
2.7
+
+
+ org.apache.commons
+ commons-lang3
+ 3.17.0
+
+
org.slf4j
diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java
index 096518d9e..fdde0ef11 100644
--- a/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java
+++ b/src/main/java/org/opentripplanner/middleware/controllers/api/OtpUserController.java
@@ -7,6 +7,7 @@
import org.eclipse.jetty.http.HttpStatus;
import org.opentripplanner.middleware.auth.Auth0Connection;
import org.opentripplanner.middleware.auth.RequestingUser;
+import org.opentripplanner.middleware.models.MobilityProfileLite;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.tripmonitor.TrustedCompanion;
@@ -25,6 +26,7 @@
import static io.github.manusant.ss.descriptor.MethodDescriptor.path;
import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.ACCEPT_KEY;
+import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.DEPENDENT_USER_IDS;
import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.USER_LOCALE;
import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.ensureRelatedUserIntegrity;
import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.manageAcceptDependentEmail;
@@ -100,6 +102,13 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {
.withResponseType(OtpUser.class),
TrustedCompanion::acceptDependent
)
+ .get(path(ROOT_ROUTE + "/getdependentmobilityprofile")
+ .withDescription("Retrieve the mobility profile for each valid dependent user id provided.")
+ .withResponses(SwaggerUtils.createStandardResponses(MobilityProfileLite.class))
+ .withPathParam().withName(DEPENDENT_USER_IDS).withRequired(true).withDescription("A comma separated list of dependent user ids.").and()
+ .withResponseAsCollection(MobilityProfileLite.class),
+ TrustedCompanion::getDependentMobilityProfile, JsonUtils::toJson
+ )
.get(path(ROOT_ROUTE + String.format(VERIFY_ROUTE_TEMPLATE, ID_PARAM, VERIFY_PATH, PHONE_PARAM))
.withDescription("Request an SMS verification to be sent to an OtpUser's phone number.")
.withPathParam().withName(ID_PARAM).withRequired(true).withDescription("The id of the OtpUser.").and()
diff --git a/src/main/java/org/opentripplanner/middleware/models/MobilityProfileLite.java b/src/main/java/org/opentripplanner/middleware/models/MobilityProfileLite.java
new file mode 100644
index 000000000..0f6268d0f
--- /dev/null
+++ b/src/main/java/org/opentripplanner/middleware/models/MobilityProfileLite.java
@@ -0,0 +1,40 @@
+package org.opentripplanner.middleware.models;
+
+import java.util.Objects;
+
+import static org.apache.commons.lang3.ObjectUtils.isNotEmpty;
+
+public class MobilityProfileLite {
+ public String userId;
+ public String mobilityMode;
+ public String email;
+ public String name;
+
+ /** This no-arg constructor exists to make MongoDB happy. */
+ public MobilityProfileLite() {
+ }
+
+ public MobilityProfileLite(OtpUser user) {
+ this.userId = user.id;
+ this.mobilityMode = isNotEmpty(user.mobilityProfile) ? user.mobilityProfile.mobilityMode : null;
+ this.email = user.email;
+ this.name = user.name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MobilityProfileLite that = (MobilityProfileLite) o;
+ return
+ Objects.equals(userId, that.userId) &&
+ Objects.equals(mobilityMode, that.mobilityMode) &&
+ Objects.equals(email, that.email) &&
+ Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(userId, mobilityMode, email, name);
+ }
+}
diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java
index c42d90f6d..061a31326 100644
--- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java
+++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java
@@ -96,7 +96,7 @@ public enum Notification {
/** Companions and observers of this user. */
public List relatedUsers = new ArrayList<>();
- /** Users that are dependent on this user. */
+ /** A list of users (their ids only) that are dependent on this user. */
public List dependents = new ArrayList<>();
/** This user's name */
diff --git a/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java b/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java
index aeb23d611..cad8bb17b 100644
--- a/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java
+++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java
@@ -1,9 +1,13 @@
package org.opentripplanner.middleware.tripmonitor;
+import com.mongodb.client.FindIterable;
import com.mongodb.client.model.Filters;
import org.apache.logging.log4j.util.Strings;
+import org.eclipse.jetty.http.HttpStatus;
import org.opentripplanner.middleware.OtpMiddlewareMain;
+import org.opentripplanner.middleware.auth.Auth0Connection;
import org.opentripplanner.middleware.i18n.Message;
+import org.opentripplanner.middleware.models.MobilityProfileLite;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.models.RelatedUser;
import org.opentripplanner.middleware.persistence.Persistence;
@@ -15,19 +19,26 @@
import spark.Response;
import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static com.mongodb.client.model.Filters.eq;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.lang3.ObjectUtils.isEmpty;
+import static org.apache.commons.lang3.ObjectUtils.isNotEmpty;
import static org.opentripplanner.middleware.tripmonitor.jobs.CheckMonitoredTrip.SETTINGS_PATH;
import static org.opentripplanner.middleware.utils.I18nUtils.getLocaleFromString;
import static org.opentripplanner.middleware.utils.I18nUtils.label;
+import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt;
public class TrustedCompanion {
@@ -43,6 +54,7 @@ private TrustedCompanion() {
public static final String ACCEPT_KEY = "acceptKey";
public static final String USER_LOCALE = "userLocale";
public static final String EMAIL_FIELD_NAME = "email";
+ public static final String DEPENDENT_USER_IDS = "dependentuserids";
/** Note: This path is excluded from security checks, see {@link OtpMiddlewareMain#initializeHttpEndpoints()}. */
public static final String ACCEPT_DEPENDENT_PATH = "api/secure/user/acceptdependent";
@@ -235,4 +247,58 @@ public static void removeDependent(OtpUser dependent, RelatedUser relatedUser) {
Persistence.otpUsers.replace(user.id, user);
}
}
-}
+
+ /**
+ * Retrieve the mobility profile for a dependent providing the requesting user is a trusted companion.
+ */
+ public static List getDependentMobilityProfile(Request request, Response response) {
+ var relatedUser = Auth0Connection.getUserFromRequest(request).otpUser;
+
+ if (isEmpty(relatedUser)) {
+ logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Related user not provided or unknown.");
+ }
+
+ var dependentUserIds = HttpUtils.getQueryParamFromRequest(request, DEPENDENT_USER_IDS, false);
+ if (isEmpty(dependentUserIds)) {
+ logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Required list of dependent user ids not provided.");
+ }
+
+ var validDependentUserIds = getValidDependents(relatedUser, dependentUserIds);
+ if (validDependentUserIds.isEmpty()) {
+ logMessageAndHalt(
+ request,
+ HttpStatus.FORBIDDEN_403,
+ "Related user is not a trusted companion of any provided dependents!"
+ );
+ }
+
+ if (isNotEmpty(relatedUser) && !validDependentUserIds.isEmpty()) {
+ List profiles = new ArrayList<>();
+ FindIterable validDependentUsers = Persistence
+ .otpUsers
+ .getFiltered(Filters.in("_id", validDependentUserIds));
+ validDependentUsers.forEach(user -> profiles.add(new MobilityProfileLite(user)));
+ return profiles;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * From the list of dependent user ids, extract all that have the related user as their trusted companion.
+ */
+ private static Set getValidDependents(OtpUser relatedUser, String dependentUserIds) {
+ // In case only one user id is provided with no comma.
+ String[] userIds = dependentUserIds.contains(",")
+ ? dependentUserIds.split(",")
+ : new String[] { dependentUserIds };
+
+ if (isEmpty(userIds)) {
+ return Collections.emptySet();
+ }
+
+ return Arrays
+ .stream(userIds)
+ .filter(userId -> relatedUser.dependents.contains(userId))
+ .collect(Collectors.toSet());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml
index ab497fc4b..bfae6f238 100644
--- a/src/main/resources/latest-spark-swagger-output.yaml
+++ b/src/main/resources/latest-spark-swagger-output.yaml
@@ -1675,6 +1675,41 @@ paths:
description: "An error occurred while performing the request. Contact an\
\ API administrator for more information."
examples: {}
+ /api/secure/user/getdependentmobilityprofile:
+ get:
+ tags:
+ - "api/secure/user"
+ description: "Retrieve the mobility profile for each valid dependent user id\
+ \ provided."
+ parameters: []
+ responses:
+ "200":
+ description: "Successful operation"
+ examples: {}
+ schema:
+ $ref: "#/definitions/MobilityProfileLite"
+ responseSchema:
+ $ref: "#/definitions/MobilityProfileLite"
+ "400":
+ description: "The request was not formed properly (e.g., some required parameters\
+ \ may be missing). See the details of the returned response to determine\
+ \ the exact issue."
+ examples: {}
+ "401":
+ description: "The server was not able to authenticate the request. This\
+ \ can happen if authentication headers are missing or malformed, or the\
+ \ authentication server cannot be reached."
+ examples: {}
+ "403":
+ description: "The requesting user is not allowed to perform the request."
+ examples: {}
+ "404":
+ description: "The requested item was not found."
+ examples: {}
+ "500":
+ description: "An error occurred while performing the request. Contact an\
+ \ API administrator for more information."
+ examples: {}
/api/secure/user/{id}/verify_sms/{phoneNumber}:
get:
tags:
diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java
index 1f62260db..a1d9f0c9e 100644
--- a/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java
+++ b/src/test/java/org/opentripplanner/middleware/controllers/api/OtpUserControllerTest.java
@@ -1,5 +1,6 @@
package org.opentripplanner.middleware.controllers.api;
+import com.auth0.json.mgmt.users.User;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.AfterAll;
@@ -9,6 +10,8 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.opentripplanner.middleware.models.MobilityProfile;
+import org.opentripplanner.middleware.models.MobilityProfileLite;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.models.RelatedUser;
import org.opentripplanner.middleware.persistence.Persistence;
@@ -22,6 +25,7 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
@@ -29,14 +33,16 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
-
+import static org.opentripplanner.middleware.auth.Auth0Connection.restoreDefaultAuthDisabled;
+import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled;
+import static org.opentripplanner.middleware.auth.Auth0Users.createAuth0UserForEmail;
+import static org.opentripplanner.middleware.testutils.ApiTestUtils.TEMP_AUTH0_USER_PASSWORD;
import static org.opentripplanner.middleware.testutils.ApiTestUtils.getMockHeaders;
import static org.opentripplanner.middleware.testutils.ApiTestUtils.makeGetRequest;
import static org.opentripplanner.middleware.testutils.ApiTestUtils.makeRequest;
import static org.opentripplanner.middleware.testutils.ApiTestUtils.mockAuthenticatedGet;
-import static org.opentripplanner.middleware.auth.Auth0Connection.restoreDefaultAuthDisabled;
-import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled;
import static org.opentripplanner.middleware.testutils.PersistenceTestUtils.deleteOtpUser;
+import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.DEPENDENT_USER_IDS;
public class OtpUserControllerTest extends OtpMiddlewareTestEnvironment {
private static final String INITIAL_PHONE_NUMBER = "+15555550222"; // Fake US 555 number.
@@ -72,7 +78,13 @@ public static void setUp() throws Exception {
dependentUserTwo = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("dependent-two"));
relatedUserThree = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("related-user-three"));
dependentUserThree = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("dependent-three"));
+
relatedUserFour = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("related-user-four"));
+
+ User auth0User = createAuth0UserForEmail(relatedUserFour.email, TEMP_AUTH0_USER_PASSWORD);
+ relatedUserFour.auth0UserId = auth0User.getId();
+ Persistence.otpUsers.replace(relatedUserFour.id, relatedUserFour);
+
dependentUserFour = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("dependent-four"));
}
@@ -241,14 +253,8 @@ void canRemoveRelatedUserOnDelete() {
@Test
void canRemoveUserFromRelatedUsersList() throws Exception {
setAuthDisabled(true);
- relatedUserFour.dependents.add(dependentUserFour.id);
- Persistence.otpUsers.replace(relatedUserFour.id, relatedUserFour);
- dependentUserFour.relatedUsers.add(new RelatedUser(
- relatedUserFour.email,
- RelatedUser.RelatedUserStatus.CONFIRMED,
- nickname
- ));
- Persistence.otpUsers.replace(dependentUserFour.id, dependentUserFour);
+
+ createTrustedCompanionship(relatedUserFour, dependentUserFour);
// Remove the first related user.
dependentUserFour.relatedUsers.clear();
@@ -275,4 +281,45 @@ void canRemoveUserFromRelatedUsersList() throws Exception {
setAuthDisabled(false);
}
+
+ @Test
+ void canGetDependentMobilityProfile() throws Exception {
+ String path = String.format(
+ "api/secure/user/getdependentmobilityprofile?%s=%s,%s",
+ DEPENDENT_USER_IDS,
+ dependentUserThree.id,
+ dependentUserFour.id
+ );
+
+ HttpResponseValues responseValues = makeGetRequest(path, getMockHeaders(relatedUserFour));
+ assertEquals(HttpStatus.FORBIDDEN_403, responseValues.status);
+
+ var mobilityProfile = new MobilityProfile();
+ mobilityProfile.mobilityDevices = Set.of("service animal", "electric wheelchair", "white cane");
+ mobilityProfile.updateMobilityMode();
+ dependentUserFour.mobilityProfile = mobilityProfile;
+ dependentUserFour.name = "dependent-user-four-name";
+
+ createTrustedCompanionship(relatedUserFour, dependentUserFour);
+
+ responseValues = makeGetRequest(path, getMockHeaders(relatedUserFour));
+ assertEquals(HttpStatus.OK_200, responseValues.status);
+ List mobilityProfileLites = JsonUtils.getPOJOFromJSONAsList(responseValues.responseBody, MobilityProfileLite.class);
+ assert mobilityProfileLites != null;
+ assertEquals(new MobilityProfileLite(dependentUserFour), mobilityProfileLites.get(0));
+ }
+
+ /**
+ * Create trusted companion relationship.
+ */
+ private static void createTrustedCompanionship(OtpUser relatedUser, OtpUser dependentUser) {
+ relatedUser.dependents.add(dependentUser.id);
+ Persistence.otpUsers.replace(relatedUser.id, relatedUser);
+ dependentUser.relatedUsers.add(new RelatedUser(
+ relatedUser.email,
+ RelatedUser.RelatedUserStatus.CONFIRMED,
+ nickname
+ ));
+ Persistence.otpUsers.replace(dependentUser.id, dependentUser);
+ }
}