Skip to content

Commit

Permalink
Add the ability to update an organizations avatar (#692)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ewoutvans authored and felixoi committed Dec 7, 2018
1 parent bdbe5a1 commit 48b8b6e
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 57 deletions.
16 changes: 12 additions & 4 deletions app/controllers/Organizations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")
}
}

/**
Expand Down
49 changes: 49 additions & 0 deletions app/security/spauth/SpongeAuthApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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}

import ore.OreConfig
import _root_.util.WSUtils.parseJson

import cats.ApplicativeError
import cats.data.{EitherT, OptionT}
import cats.effect.IO
import cats.syntax.all._
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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 _)
}
52 changes: 2 additions & 50 deletions app/views/users/view.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
<div class="edit-avatar" style="display: none;">
<i class="fa fa-edit"></i> @messages("user.editAvatar")
<a href="@routes.Organizations.updateAvatar(u.user.name)"><i class="fa fa-edit"></i> @messages("user.editAvatar")</a>
</div>

@if(!u.currentUser.get.readPrompts.contains(Prompt.ChangeAvatar)) {
Expand Down Expand Up @@ -299,48 +295,4 @@ <h4>@messages("user.tagline")</h4>
</div>
}
}

@modal("modal-avatar", "label-avatar", "user.editAvatar") {
@form(action = routes.Organizations.updateAvatar(u.user.name), 'id -> "form-avatar",
'enctype -> "multipart/form-data") {
@CSRF.formField
<div class="modal-body">
<div class="alert alert-danger">
<span class="error"></span>
</div>

<div class="setting">
<div class="setting-description">
<h4>
<input checked type="radio" name="avatar-method" value="by-url" />
@messages("user.avatar.byUrl")
</h4>
</div>
<input name="avatar-url" type="url" class="form-control" />
<div class="clearfix"></div>
</div>

<div class="setting setting-no-border">
<div class="setting-description">
<h4>
<input type="radio" name="avatar-method" value="by-file" />
@messages("user.avatar.byFile")
</h4>
</div>
<input name="avatar-file" type="file" disabled />
<div class="clearfix"></div>
</div>
</div>

<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
@messages("general.cancel")
</button>
<button type="submit" class="btn btn-primary">
<i class="fa fa-spinner fa-spin" style="display: none;"></i>
@messages("general.update")
</button>
</div>
}
}
}
2 changes: 2 additions & 0 deletions conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions public/javascripts/userPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit 48b8b6e

Please sign in to comment.