app/controllers/UserController.scala (172 lines of code) (raw):
package controllers
import auth.{BearerTokenAuth, Security}
import javax.inject.{Inject, Singleton}
import play.api.{Configuration, Logger}
import play.api.libs.circe.Circe
import play.api.mvc.{AbstractController, ControllerComponents}
import responses._
import io.circe.syntax._
import io.circe.generic.auto._
import models.{UserProfile, UserProfileDAO, UserProfileField}
import play.api.cache.SyncCacheApi
import requests.{FieldUpdateOperation, UserProfileFieldUpdate, UserProfileFieldUpdateEncoder}
import scala.concurrent.Future
import auth.ClaimsSetExtensions._
import helpers.UserAvatarHelper
import org.slf4j.LoggerFactory
@Singleton
class UserController @Inject()(override val controllerComponents:ControllerComponents,
override val config: Configuration,
override val bearerTokenAuth:BearerTokenAuth,
override val cache:SyncCacheApi,
userProfileDAO:UserProfileDAO,
userAvatarHelper: UserAvatarHelper)
extends AbstractController(controllerComponents) with Security with UserProfileFieldUpdateEncoder with Circe {
implicit val ec = controllerComponents.executionContext
override protected val logger=LoggerFactory.getLogger(getClass)
def loginStatus = IsAuthenticated { username=> request =>
userProfileFromSession(request.session) match {
case None=>
BadRequest(GenericErrorResponse("profile_error","no profile in session").asJson)
case Some(Left(err))=>
logger.error(err.toString)
InternalServerError(GenericErrorResponse("profile_error", err.toString).asJson)
case Some(Right(profile))=>
val userData = UserResponse.fromProfile(profile)
val maybeWithPicture = userData.copy(avatarUrl=userAvatarHelper.getAvatarUrl(profile.userEmail).map(_.toString))
Ok(maybeWithPicture.asJson)
}
}
def allUsers = IsAdminAsync { _=> request=>
userProfileDAO.allUsers().map(resultList=>{
val errors = resultList.collect({case Left(err)=>err})
if(errors.nonEmpty){
InternalServerError(ErrorListResponse("db_error", "Could not look up users", errors.map(_.toString)).asJson)
} else {
Ok(ObjectListResponse("ok","user",resultList.collect({case Right(up)=>up}), resultList.length).asJson)
}
})
}
/**
* converts the singleString field in the request to a boolean value, or returns an error
* @param rq [[UserProfileFieldUpdate]] request
* @return either the boolean value or an error string
*/
protected def getSingleBoolValue(rq:UserProfileFieldUpdate):Either[String,Boolean] =
rq.stringValue.map(_.toLowerCase) match {
case None=>Left("stringValue must be specified")
case Some("true")=>Right(true)
case Some("yes")=>Right(true)
case Some("allow")=>Right(true)
case Some("false")=>Right(false)
case Some("no")=>Right(false)
case Some("deny")=>Right(false)
case Some(otherStr)=>Left(s"$otherStr is not a recognised value. Try 'true' or 'false'.")
}
protected def getSingleLongValue(rq:UserProfileFieldUpdate):Either[String,Long] = {
try {
rq.stringValue.map(_.toInt) match {
case Some(value) => Right(value)
case None => Left("No value was provided")
}
} catch {
case ex:Throwable=>
Left(s"Could not convert ${rq.stringValue} to integer: ${ex.toString}")
}
}
protected def getSingleStringValue(rq:UserProfileFieldUpdate):Either[String,String] =
rq.stringValue match {
case Some(value)=>Right(value)
case None=>Left("No value was provided")
}
/**
* updates an existing list with the listValue field in the request, based on the `operation` field of the request
* (overwrite, add, remove)
* @param rq [[UserProfileFieldUpdate]] request
* @param existingList existing list to update
* @return either a new list value or an error string
*/
protected def updateStringList(rq:UserProfileFieldUpdate, existingList:Seq[String]):Either[String, Seq[String]] =
rq.listValue match {
case None=>Left("listValue must be specified")
case Some(updates)=>
rq.operation match {
case FieldUpdateOperation.OP_OVERWRITE=>
Right(updates)
case FieldUpdateOperation.OP_ADD=>
//don't duplicate items. If an item to add is already in the existingList, drop it.
val filteredUpdates = updates.filter(update=> !existingList.contains(update))
Right(existingList ++ filteredUpdates)
case FieldUpdateOperation.OP_REMOVE=>
Right(existingList.filter(entry=> !updates.contains(entry)))
}
}
/**
* tries to perform the actions requested in the [[UserProfileFieldUpdate]] object
* @param originalProfile [[UserProfile]] object to update
* @param rq [[UserProfileFieldUpdate]] object containing instructions for what to update
* @return either an error string or the updated UserProfile (unsaved)
*/
def performUpdate(originalProfile:UserProfile, rq:UserProfileFieldUpdate):Either[String, UserProfile] =
rq.fieldName match {
case UserProfileField.IS_ADMIN=>
getSingleBoolValue(rq).map(newValue=>originalProfile.copy(isAdmin = newValue))
case UserProfileField.ALL_COLLECTIONS=>
getSingleBoolValue(rq).map(newValue=>originalProfile.copy(allCollectionsVisible = newValue))
case UserProfileField.VISIBLE_COLLECTIONS=>
updateStringList(rq, originalProfile.visibleCollections).map(newValue=>originalProfile.copy(visibleCollections = newValue))
case UserProfileField.PER_RESTORE_QUOTA=>
getSingleLongValue(rq).map(newValue=>originalProfile.copy(perRestoreQuota = Some(newValue)))
case UserProfileField.ROLLING_QUOTA=>
getSingleLongValue(rq).map(newValue=>originalProfile.copy(rollingRestoreQuota = Some(newValue)))
case UserProfileField.ADMIN_APPROVAL_QUOTA=>
getSingleLongValue(rq).map(newValue=>originalProfile.copy(adminAuthQuota = Some(newValue)))
case UserProfileField.ADMIN_ROLLING_APPROVAL_QUOTA=>
getSingleLongValue(rq).map(newValue=>originalProfile.copy(adminRollingAuthQuota = Some(newValue)))
case UserProfileField.DEPARTMENT=>
getSingleStringValue(rq).map(newValue=>originalProfile.copy(department = Some(newValue)))
case UserProfileField.PRODUCTION_OFFICE=>
getSingleStringValue(rq).map(newValue=>originalProfile.copy(productionOffice = Some(newValue)))
case _=>
Left(s"Did not recognise field ${rq.fieldName}")
}
/**
* handle a frontend request to update a user profile
* @return
*/
def updateUserProfileField = IsAdminAsync(circe.json(2048)) { _=> request=>
request.body.as[UserProfileFieldUpdate] match {
case Left(err)=>
logger.error(err.toString)
Future(BadRequest(GenericErrorResponse("bad_request", err.toString).asJson))
case Right(updateRq)=>
userProfileDAO.userProfileForEmail(updateRq.user).flatMap({
case None=>
Future(BadRequest(GenericErrorResponse("not_found", s"user ${updateRq.user} not found").asJson))
case Some(Left(err))=>
logger.error(err.toString)
Future(InternalServerError(GenericErrorResponse("db_error", err.toString).asJson))
case Some(Right(originalProfile))=>
performUpdate(originalProfile, updateRq) match {
case Right(updatedProfile) =>
logger.info(s"updatedProfile is $updatedProfile")
userProfileDAO
.put(updatedProfile)
.map(_ => Ok(ObjectGetResponse("updated", "profile", updatedProfile).asJson))
.recover({
case err: Throwable =>
logger.error(s"Could not update user profile for ${updateRq.user}: ${err.getMessage}", err)
InternalServerError(GenericErrorResponse("db_error", err.getMessage).asJson)
})
case Left(err) =>
Future(BadRequest(GenericErrorResponse("bad_request", err).asJson))
}
})
}
}
def myProfile = IsAuthenticated { _=> request=>
userProfileFromSession(request.session) match {
case Some(Left(err))=>
InternalServerError(GenericErrorResponse("error", err.toString).asJson)
case Some(Right(profile))=>
Ok(ObjectGetResponse("ok","userProfile",profile).asJson)
case None=>
NotFound(GenericErrorResponse("error","not found").asJson)
}
}
case class UserDeleteRequest (user:String)
def deleteUser = IsAdminAsync(circe.json(2048)) { _=> request=>
request.body.as[UserDeleteRequest] match {
case Left(err)=>
logger.error(err.toString)
Future(BadRequest(GenericErrorResponse("bad_request", err.toString).asJson))
case Right(deleteRq)=> {
userProfileDAO
.delete(deleteRq.user)
.map(_ => Ok(ObjectGetResponse("deleted", "user", deleteRq).asJson))
.recover({
case err: Throwable =>
logger.error(s"Could not delete user profile for ${deleteRq.user}: ${err.getMessage}", err)
InternalServerError(GenericErrorResponse("db_error", err.getMessage).asJson)
})
}
}
}
}