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); + } }