identity/app/idapiclient/IdApiClient.scala (243 lines of code) (raw):

package idapiclient import com.gu.identity.model.{EmailList, Subscriber, User} import scala.concurrent.{ExecutionContext, Future} import idapiclient.responses.{AccountDeletionResult, CookiesResponse, Error, HttpResponse} import conf.IdConfig import idapiclient.parser.IdApiJsonBodyParser import net.liftweb.json.Serialization.write import utils.SafeLogging import idapiclient.requests.{AutoSignInToken, DeletionBody} import net.liftweb.json.Formats import org.slf4j.LoggerFactory import play.api.libs.ws.WSClient class IdApiClient(idJsonBodyParser: IdApiJsonBodyParser, conf: IdConfig, httpClient: HttpClient)(implicit val executionContext: ExecutionContext, ) extends SafeLogging { private val apiRootUrl: String = conf.apiRoot private val clientAuth: Auth = ClientAuth(conf.apiClientToken) private val exactTargetLogger = LoggerFactory.getLogger("exactTarget") import idJsonBodyParser.{extractUnit, extract, jsonField} private implicit val formats: Formats = idJsonBodyParser.formats private def extractUser: (Response[HttpResponse]) => Response[User] = extract(jsonField("user")) // AUTH def authBrowser( userAuth: Auth, trackingData: TrackingData, persistent: Option[Boolean] = None, ): Future[Response[CookiesResponse]] = { val params = buildParams(None, Some(trackingData), Seq("format" -> "cookies") ++ persistent.map("persistent" -> _.toString)) val headers = buildHeaders(Some(userAuth), extra = xForwardedForHeader(trackingData)) val body = write(userAuth) val response = httpClient.POST(apiUrl("auth"), Some(body), params, headers) response map extract(jsonField("cookies")) } def unauth(auth: Auth, trackingData: TrackingData): Future[Response[CookiesResponse]] = post("unauth", Some(auth), Some(trackingData)) map extract[CookiesResponse](jsonField("cookies")) // AUTO SIGN IN TOKENS def verifyAutoSignInToken(token: String): Future[Response[CookiesResponse]] = { val response = httpClient.PUT( apiUrl("auto-signin-token"), Some(write(AutoSignInToken(token))), clientAuth.parameters, clientAuth.headers, ) response map extract(jsonField("cookies")) } // USERS def user(userId: String, auth: Auth = Anonymous): Future[Response[User]] = { val apiPath = urlJoin("user", userId) val params = buildParams(Some(auth)) val headers = buildHeaders(Some(auth)) val response = httpClient.GET(apiUrl(apiPath), None, params, headers) response map extractUser } def userFromQueryParam(param: String, field: String, auth: Auth = Anonymous): Future[Response[User]] = { val apiPath = s"/user?${field}=${param}" val params = buildParams(Some(auth)) val headers = buildHeaders(Some(auth)) val response = httpClient.GET(apiUrl(apiPath), None, params, headers) response map extractUser } def saveUser(userId: String, user: UserUpdateDTO, auth: Auth): Future[Response[User]] = post(urlJoin("user", userId), Some(auth), body = Some(write(user))) map extractUser def me(auth: Auth): Future[Response[User]] = { val apiPath = urlJoin("user", "me") val params = buildParams(Some(auth)) val response = httpClient.GET(apiUrl(apiPath), None, params, buildHeaders(Some(auth))) response map extractUser } def userForToken(token: String): Future[Response[User]] = { val apiPath = urlJoin("pwd-reset", "user-for-token") val params = buildParams(extra = Iterable("token" -> token)) val response = httpClient.GET(apiUrl(apiPath), None, params, buildHeaders()) response map extractUser } // EMAILS def userEmails(userId: String, trackingParameters: TrackingData): Future[Response[Subscriber]] = { val apiPath = urlJoin("useremails", userId) val params = buildParams(tracking = Some(trackingParameters)) val response = httpClient.GET(apiUrl(apiPath), None, params, buildHeaders(extra = xForwardedForHeader(trackingParameters))) response map extract(jsonField("result")) } def addSubscription( userId: String, emailList: EmailList, auth: Auth, trackingParameters: TrackingData, ): Future[Response[Unit]] = { exactTargetLogger.debug(s"Subscribing $userId to listId: ${emailList.listId}") post( urlJoin("useremails", userId, "subscriptions"), Some(auth), Some(trackingParameters), Some(write(emailList)), ) map extractUnit } def deleteSubscription( userId: String, emailList: EmailList, auth: Auth, trackingParameters: TrackingData, ): Future[Response[Unit]] = { exactTargetLogger.debug(s"Unsubscribing $userId to listId: ${emailList.listId}") delete( urlJoin("useremails", userId, "subscriptions"), Some(auth), Some(trackingParameters), Some(write(emailList)), ) map extractUnit } // ACCOUNT DELETION def executeAccountDeletionStepFunction( userId: String, email: String, reason: Option[String], auth: Auth, ): Future[Response[AccountDeletionResult]] = { httpClient.POST( s"${conf.accountDeletionApiRoot}/delete", Some(write(DeletionBody(userId, email, reason))), urlParameters = Nil, headers = buildHeaders(Some(auth), extra = Seq(("x-api-key", conf.accountDeletionApiKey))), ) map extract[AccountDeletionResult](identity) } // EMAIL TOKENS def decryptEmailToken(token: String): Future[Response[String]] = { val apiPath = urlJoin("signin-token", "token", token) val response = httpClient.GET(uri = apiUrl(apiPath), None, None, buildHeaders()) response map extract(jsonField("email")) } def resendEmailValidationEmailByToken(token: String, returnUrl: Option[String]): Future[Response[Unit]] = { val apiPath = urlJoin("signin-token", "send-validation-email", token) val parameters = returnUrl.map(url => Iterable("returnUrl" -> url)).getOrElse(Iterable.empty) val response = httpClient.POST(uri = apiUrl(apiPath), None, None, buildHeaders(extra = parameters)) response map extractUnit } def put( apiPath: String, auth: Option[Auth] = None, trackingParameters: Option[TrackingData] = None, body: Option[String] = None, extraHeaders: Parameters, urlParameters: Parameters, ): Future[Response[HttpResponse]] = httpClient.PUT( apiUrl(apiPath), body, buildParams(auth, trackingParameters) ++ urlParameters, buildHeaders(auth) ++ extraHeaders, ) def post( apiPath: String, auth: Option[Auth] = None, trackingParameters: Option[TrackingData] = None, body: Option[String] = None, ): Future[Response[HttpResponse]] = { httpClient.POST( apiUrl(apiPath), body, buildParams(auth, trackingParameters), buildHeaders(auth, trackingParameters.map(xForwardedForHeader)), ) } def delete( apiPath: String, auth: Option[Auth] = None, trackingParameters: Option[TrackingData] = None, body: Option[String] = None, ): Future[Response[HttpResponse]] = httpClient.DELETE(apiUrl(apiPath), body, buildParams(auth, trackingParameters), buildHeaders(auth)) implicit object ParamsOpt2Params extends (Option[Parameters] => Parameters) { def apply(paramsOpt: Option[Parameters]): Parameters = paramsOpt.getOrElse(Iterable.empty) } private def buildParams( auth: Option[Auth] = None, tracking: Option[TrackingData] = None, extra: Parameters = Iterable.empty, ): Parameters = extra ++ clientAuth.parameters ++ auth.map(_.parameters).toSeq ++ tracking.map(_.parameters).toSeq private def buildHeaders(auth: Option[Auth] = None, extra: Parameters = Iterable.empty): Parameters = { extra ++ clientAuth.headers ++ auth.map(_.headers).toSeq } private def apiUrl(path: String) = urlJoin(apiRootUrl, path) private def urlJoin(pathParts: String*) = { pathParts .filter(_.nonEmpty) .map(slug => { slug.stripPrefix("/").stripSuffix("/") }) mkString "/" } private def xForwardedForHeader(trackingParameters: TrackingData): Parameters = trackingParameters.ipAddress .map(ip => Iterable("X-Forwarded-For" -> ip)) .getOrElse(Iterable.empty) } class HttpClient(wsClient: WSClient)(implicit val executionContext: ExecutionContext) extends SafeLogging { def GET( uri: String, body: Option[String] = None, urlParameters: Parameters, headers: Parameters, ): Future[Response[HttpResponse]] = { makeRequest("GET", uri, body, urlParameters, headers) } def POST( uri: String, body: Option[String], urlParameters: Parameters, headers: Parameters, ): Future[Response[HttpResponse]] = { makeRequest("POST", uri, body, urlParameters, headers) } def DELETE( uri: String, body: Option[String], urlParameters: Parameters = Nil, headers: Parameters, ): Future[Response[HttpResponse]] = { makeRequest("DELETE", uri, body, urlParameters, headers) } def PUT( uri: String, body: Option[String], urlParameters: Parameters = Nil, headers: Parameters, ): Future[Response[HttpResponse]] = makeRequest("PUT", uri, body, urlParameters, headers) def makeRequest( method: String, uri: String, body: Option[String], urlParameters: Parameters = Nil, headers: Parameters, ): Future[Response[HttpResponse]] = { wsClient .url(uri) .withBody(body.getOrElse("")) // FIXME: DELETE should not have a body .withQueryStringParameters(urlParameters.toList: _*) .withHttpHeaders(headers.toList: _*) .withMethod(method) .execute() .map { resp => Right(HttpResponse(resp.body, resp.status, resp.statusText)) } .recover { case e: Throwable => logger.error(s"Network error while communicating with $uri:", e) Left(List(Error(e.getClass.getName, e.toString))) } } }