From 48b8b6e553e08770b7692002a51ade89a22c4882 Mon Sep 17 00:00:00 2001 From: Ewout Date: Fri, 7 Dec 2018 18:27:46 +0100 Subject: [PATCH] Add the ability to update an organizations avatar (#692) --- app/controllers/Organizations.scala | 16 ++++++-- app/security/spauth/SpongeAuthApi.scala | 49 +++++++++++++++++++++++ app/views/users/view.scala.html | 52 +------------------------ conf/messages | 2 + conf/routes | 2 +- public/javascripts/userPage.js | 6 ++- 6 files changed, 70 insertions(+), 57 deletions(-) diff --git a/app/controllers/Organizations.scala b/app/controllers/Organizations.scala index a4ffc3eae..cf7f40ea3 100755 --- a/app/controllers/Organizations.scala +++ b/app/controllers/Organizations.scala @@ -5,7 +5,7 @@ import javax.inject.Inject import scala.concurrent.ExecutionContext import play.api.cache.AsyncCacheApi -import play.api.i18n.MessagesApi +import play.api.i18n.{Lang, MessagesApi} import play.api.mvc.{Action, AnyContent} import controllers.sugar.Bakery @@ -118,10 +118,18 @@ class Organizations @Inject()(forms: OreForms)( * Updates an [[models.user.Organization]]'s avatar. * * @param organization Organization to update avatar of - * @return Json response with errors if any + * @return Redirect to auth or bad request */ - def updateAvatar(organization: String): Action[AnyContent] = EditOrganizationAction(organization) { - Ok + def updateAvatar(organization: String): Action[AnyContent] = EditOrganizationAction(organization).asyncF { + implicit request => + implicit val lang: Lang = request.lang + + auth.getChangeAvatarToken(request.user.name, organization).value.map { + case Left(_) => + Redirect(routes.Users.showProjects(organization, None)).withError(messagesApi("organization.avatarFailed")) + case Right(token) => + Redirect(auth.url + s"/accounts/user/$organization/change-avatar/?key=${token.signedData}") + } } /** diff --git a/app/security/spauth/SpongeAuthApi.scala b/app/security/spauth/SpongeAuthApi.scala index 9c8f30157..2ed7cacbd 100644 --- a/app/security/spauth/SpongeAuthApi.scala +++ b/app/security/spauth/SpongeAuthApi.scala @@ -5,6 +5,7 @@ import javax.inject.Inject import scala.concurrent.duration._ +import play.api.libs.functional.syntax._ import play.api.libs.json.Reads._ import play.api.libs.json._ import play.api.libs.ws.{WSClient, WSResponse} @@ -12,6 +13,7 @@ import play.api.libs.ws.{WSClient, WSResponse} import ore.OreConfig import _root_.util.WSUtils.parseJson +import cats.ApplicativeError import cats.data.{EitherT, OptionT} import cats.effect.IO import cats.syntax.all._ @@ -73,6 +75,43 @@ trait SpongeAuthApi { readUser(IO.fromFuture(IO(this.ws.url(url).withRequestTimeout(timeout).get()))).toOption } + /** + * Returns the signed_data that can be used to construct the change-avatar + * @param username + * @param ec + * @return + */ + def getChangeAvatarToken( + requester: String, + organization: String + )(): EitherT[IO, String, ChangeAvatarToken] = { + val params = Map( + "api-key" -> Seq(this.apiKey), + "request_username" -> Seq(requester), + ) + readChangeAvatarToken( + IO.fromFuture( + IO( + this.ws.url(route(s"/api/users/$organization/change-avatar-token/")).withRequestTimeout(timeout).post(params) + ) + ) + ) + } + + private def readChangeAvatarToken(response: IO[WSResponse]): EitherT[IO, String, ChangeAvatarToken] = { + val parsed = OptionT(response.map(parseJson(_, Logger))) + .map(json => json.as[ChangeAvatarToken]) + .toRight("Failed to parse json response") + + ApplicativeError[EitherT[IO, String, ?], Throwable].recoverWith(parsed) { + case _: TimeoutException => + EitherT.leftT("error.spongeauth.auth") + case e => + Logger.error("An unexpected error occured while handling a response", e) + EitherT.leftT("error.spongeauth.unexpected") + } + } + private def readUser(response: IO[WSResponse]): EitherT[IO, String, SpongeUser] = { EitherT( OptionT(response.map(parseJson(_, Logger))) @@ -107,3 +146,13 @@ final class SpongeAuth @Inject()(config: OreConfig, override val ws: WSClient) e override val timeout: FiniteDuration = conf.timeout } + +case class ChangeAvatarToken(signedData: String, targetUsername: String, requestUserId: Int) + +object ChangeAvatarToken { + implicit val changeAvatarTokenReads: Reads[ChangeAvatarToken] = + (JsPath \ "signed_data") + .read[String] + .and((JsPath \ "raw_data" \ "target_username").read[String]) + .and((JsPath \ "raw_data" \ "request_user_id").read[Int])(ChangeAvatarToken.apply _) +} diff --git a/app/views/users/view.scala.html b/app/views/users/view.scala.html index 66974971b..e68af66e4 100644 --- a/app/views/users/view.scala.html +++ b/app/views/users/view.scala.html @@ -49,15 +49,11 @@ @userAvatar( userName = Some(u.user.name), avatarUrl = u.user.avatarUrl, - clazz = "user-avatar-md" + (if (canEditOrgSettings) " organization-avatar" else ""), - href = "#", - attr = if (u.orgaPerm.getOrElse(EditSettings, false)) Map( - "data-toggle" -> "modal", - "data-target" -> "#modal-avatar") else Map()) + clazz = "user-avatar-md" + (if (canEditOrgSettings) " organization-avatar" else "")) @if(canEditOrgSettings) { @if(!u.currentUser.get.readPrompts.contains(Prompt.ChangeAvatar)) { @@ -299,48 +295,4 @@

@messages("user.tagline")

} } - - @modal("modal-avatar", "label-avatar", "user.editAvatar") { - @form(action = routes.Organizations.updateAvatar(u.user.name), 'id -> "form-avatar", - 'enctype -> "multipart/form-data") { - @CSRF.formField - - - - } - } } diff --git a/conf/messages b/conf/messages index 896a892ec..0722c21ee 100755 --- a/conf/messages +++ b/conf/messages @@ -324,6 +324,8 @@ user.unlock.confirm = Are you sure you want to unlock your account? user.enterPassword = Enter your password to continue: user.stats = Stats +organization.avatarFailed = Failed to request token for changing the organization avatar. + notification.invite = You have been invited to join the {0} notification.invites = Invites notification.invite.all = All diff --git a/conf/routes b/conf/routes index 1fff5103c..cb9b7f1f5 100755 --- a/conf/routes +++ b/conf/routes @@ -90,7 +90,7 @@ POST /organizations/new @controllers POST /organizations/invite/:id/:status @controllers.Organizations.setInviteStatus(id: DbRef[OrganizationUserRole], status) -POST /organizations/:organization/settings/avatar @controllers.Organizations.updateAvatar(organization) +GET /organizations/:organization/settings/avatar @controllers.Organizations.updateAvatar(organization) POST /organizations/:organization/settings/members @controllers.Organizations.updateMembers(organization) POST /organizations/:organization/settings/members/remove @controllers.Organizations.removeMember(organization) diff --git a/public/javascripts/userPage.js b/public/javascripts/userPage.js index 19c3c1892..d06986662 100644 --- a/public/javascripts/userPage.js +++ b/public/javascripts/userPage.js @@ -149,8 +149,10 @@ function setupAvatarForm() { $('.organization-avatar').hover(function() { $('.edit-avatar').fadeIn('fast'); - }, function() { - $('.edit-avatar').fadeOut('fast'); + }, function(e) { + if(!$(e.relatedTarget).closest("div").hasClass("edit-avatar")) { + $('.edit-avatar').fadeOut('fast'); + } }); var avatarModal = $('#modal-avatar');