Skip to content

Commit

Permalink
Merge pull request #517 from Katrix/feature/better-future
Browse files Browse the repository at this point in the history
Improve usage around Future

Signed-off-by: Jadon Fowler <[email protected]>
  • Loading branch information
phase authored Jul 2, 2018
2 parents 0c1d96f + c2e186d commit 6f78069
Show file tree
Hide file tree
Showing 84 changed files with 2,245 additions and 1,795 deletions.
259 changes: 110 additions & 149 deletions app/controllers/ApiController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,24 @@ import models.user.User
import ore.permission.EditApiKeys
import ore.permission.role.RoleTypes
import ore.permission.role.RoleTypes.RoleType
import ore.project.factory.ProjectFactory
import ore.project.factory.{PendingVersion, ProjectFactory}
import ore.project.io.{InvalidPluginFileException, PluginUpload, ProjectFiles}
import ore.rest.ProjectApiKeyTypes._
import ore.rest.{OreRestfulApi, OreWrites}
import ore.{OreConfig, OreEnv}
import play.api.cache.AsyncCacheApi
import play.api.i18n.MessagesApi
import util.StatusZ
import util.functional.{EitherT, OptionT, Id}
import util.instances.future._
import util.syntax._
import play.api.libs.json._
import play.api.mvc._
import security.CryptoUtils
import security.spauth.SingleSignOnConsumer
import slick.lifted.Compiled

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}

/**
* Ore API (v1)
Expand All @@ -46,7 +48,7 @@ final class ApiController @Inject()(api: OreRestfulApi,
implicit override val bakery: Bakery,
implicit override val cache: AsyncCacheApi,
implicit override val sso: SingleSignOnConsumer,
implicit override val messagesApi: MessagesApi)
implicit override val messagesApi: MessagesApi)(implicit val ec: ExecutionContext)
extends OreBaseController {

import writes._
Expand Down Expand Up @@ -84,46 +86,37 @@ final class ApiController @Inject()(api: OreRestfulApi,
}
}

def createKey(version: String, pluginId: String) = {
def createKey(version: String, pluginId: String) =
(Action andThen AuthedProjectActionById(pluginId) andThen ProjectPermissionAction(EditApiKeys)) async { implicit request =>
val data = request.data
this.forms.ProjectApiKeyCreate.bindFromRequest().fold( _ => Future.successful(BadRequest),
{
case keyType@Deployment =>
this.projectApiKeys.exists(k => k.projectId === data.project.id.get && k.keyType === keyType)
.flatMap { exists =>
if (exists) Future.successful(BadRequest)
else {
this.projectApiKeys.add(ProjectApiKey(
projectId = data.project.id.get,
keyType = keyType,
value = UUID.randomUUID().toString.replace("-", ""))).map { pak =>
Created(Json.toJson(pak))
}
}
}
val projectId = request.data.project.id.get
val res = for {
keyType <- bindFormOptionT[Future](this.forms.ProjectApiKeyCreate)
if keyType == Deployment
exists <- OptionT.liftF(this.projectApiKeys.exists(k => k.projectId === projectId && k.keyType === keyType))
if !exists
pak <- OptionT.liftF(
this.projectApiKeys.add(ProjectApiKey(
projectId = projectId,
keyType = keyType,
value = UUID.randomUUID().toString.replace("-", "")))
)
} yield Created(Json.toJson(pak))

case _ => Future.successful(BadRequest)
}
)
res.getOrElse(BadRequest)
}
}

def revokeKey(version: String, pluginId: String) = {
def revokeKey(version: String, pluginId: String) =
(AuthedProjectActionById(pluginId) andThen ProjectPermissionAction(EditApiKeys)) { implicit request =>
this.forms.ProjectApiKeyRevoke.bindFromRequest().fold(
_ => BadRequest,
key => {
if (key.projectId != request.data.project.id.get)
BadRequest
else {
key.remove()
Ok
}
}
)
val res = for {
key <- bindFormOptionT[Id](this.forms.ProjectApiKeyRevoke)
if key.projectId == request.data.project.id.get
} yield {
key.remove()
Ok
}

res.getOrElse(BadRequest)
}
}

/**
* Returns a JSON view of Versions meeting the specified criteria.
Expand All @@ -138,7 +131,7 @@ final class ApiController @Inject()(api: OreRestfulApi,
def listVersions(version: String, pluginId: String, channels: Option[String],
limit: Option[Int], offset: Option[Int]) = Action.async {
version match {
case "v1" => this.api.getVersionList(pluginId, channels, limit, offset).map(ApiResult)
case "v1" => this.api.getVersionList(pluginId, channels, limit, offset).map(Some.apply).map(ApiResult)
case _ => Future.successful(NotFound)
}
}
Expand All @@ -164,87 +157,67 @@ final class ApiController @Inject()(api: OreRestfulApi,
version match {
case "v1" =>
val projectData = request.data
this.forms.VersionDeploy.bindFromRequest().fold(
hasErrors => Future.successful(BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))),
formData => {

val apiKeyTable = TableQuery[ProjectApiKeyTable]
def queryApiKey(deployment: Rep[ProjectApiKeyType], key: Rep[String], pId: Rep[Int]) = {
val query = for {
k <- apiKeyTable if k.value === key && k.projectId === pId && k.keyType === deployment
} yield {
k.id
}
query.exists
}

val compiled = Compiled(queryApiKey _)

val apiKeyExists: Future[Boolean] = this.service.DB.db.run(compiled(Deployment, formData.apiKey, projectData.project.id.get).result)
val dep = for {
(apiKey, versionExists) <- apiKeyExists zip
projectData.project.versions.exists(_.versionString === name)
bindFormEitherT[Future](this.forms.VersionDeploy)(hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))).flatMap { formData =>
val apiKeyTable = TableQuery[ProjectApiKeyTable]
def queryApiKey(deployment: Rep[ProjectApiKeyType], key: Rep[String], pId: Rep[Int]) = {
val query = for {
k <- apiKeyTable if k.value === key && k.projectId === pId && k.keyType === deployment
} yield {
if (!apiKey) Future.successful(Unauthorized(error("apiKey", "api.deploy.invalidKey")))
else if (versionExists) Future.successful(BadRequest(error("versionName", "api.deploy.versionExists")))
else {

def upload(user: User): Future[Result] = {

val pending = Right(user).flatMap { user =>
this.factory.getUploadError(user)
.map(err => BadRequest(error("user", err)))
.toLeft(PluginUpload.bindFromRequest())
} flatMap {
case None => Left(BadRequest(error("files", "error.noFile")))
case Some(uploadData) => Right(uploadData)
} match {
case Left(err) => Future.successful(Left(err))
case Right(data) =>
try {
this.factory.processSubsequentPluginUpload(data, user, projectData.project).map {
case Left(err) => Left(BadRequest(error("upload", err)))
case Right(pv) => Right(pv)
}
} catch {
case e: InvalidPluginFileException =>
Future.successful(Left(BadRequest(error("upload", e.getMessage))))
}
}
pending flatMap {
case Left(err) => Future.successful(err)
case Right(pendingVersion) =>
pendingVersion.createForumPost = formData.createForumPost
pendingVersion.channelName = formData.channel.name
formData.changelog.foreach(pendingVersion.underlying.setDescription)
pendingVersion.complete().map { newVersion =>
if (formData.recommended)
projectData.project.setRecommendedVersion(newVersion._1)
Created(api.writeVersion(newVersion._1, projectData.project, newVersion._2, None, newVersion._3))
}
}
}
k.id
}
query.exists
}

for {
user <- projectData.project.owner.user
orga <- user.toMaybeOrganization
owner <- orga.map(_.owner.user).getOrElse(Future.successful(user))
result <- upload(owner)
} yield {
result
val compiled = Compiled(queryApiKey _)

val apiKeyExists: Future[Boolean] = this.service.DB.db.run(compiled(Deployment, formData.apiKey, projectData.project.id.get).result)

EitherT.liftF(apiKeyExists)
.filterOrElse(apiKey => !apiKey, Unauthorized(error("apiKey", "api.deploy.invalidKey")))
.semiFlatMap(_ => projectData.project.versions.exists(_.versionString === name))
.filterOrElse(identity, BadRequest(error("versionName", "api.deploy.versionExists")))
.semiFlatMap(_ => projectData.project.owner.user)
.semiFlatMap(user => user.toMaybeOrganization.semiFlatMap(_.owner.user).getOrElse(user))
.flatMap { owner =>

val pluginUpload = this.factory.getUploadError(owner)
.map(err => BadRequest(error("user", err)))
.toLeft(PluginUpload.bindFromRequest())
.flatMap(_.toRight(BadRequest(error("files", "error.noFile"))))

EitherT.fromEither[Future](pluginUpload).flatMap { data =>
//TODO: We should get rid of this try
try {
this.factory
.processSubsequentPluginUpload(data, owner, projectData.project)
.leftMap(err => BadRequest(error("upload", err)))
}
catch {
case e: InvalidPluginFileException =>
EitherT.leftT[Future, PendingVersion](BadRequest(error("upload", e.getMessage)))
}
}
}
dep.flatten
}
)
.semiFlatMap { pendingVersion =>
pendingVersion.createForumPost = formData.createForumPost
pendingVersion.channelName = formData.channel.name
formData.changelog.foreach(pendingVersion.underlying.setDescription)
pendingVersion.complete()
}
.map { case (newVersion, channel, tags) =>
if (formData.recommended)
projectData.project.setRecommendedVersion(newVersion)
Created(api.writeVersion(newVersion, projectData.project, channel, None, tags))
}
}.merge
case _ => Future.successful(NotFound)
}
}

def listPages(version: String, pluginId: String, parentId: Option[Int]) = Action.async {
version match {
case "v1" => this.api.getPages(pluginId, parentId).map(ApiResult)
case "v1" => this.api.getPages(pluginId, parentId).value.map(ApiResult)
case _ => Future.successful(NotFound)
}
}
Expand Down Expand Up @@ -288,7 +261,7 @@ final class ApiController @Inject()(api: OreRestfulApi,
*/
def listTags(version: String, plugin: String, versionName: String) = Action.async {
version match {
case "v1" => this.api.getTags(plugin, versionName).map(ApiResult)
case "v1" => this.api.getTags(plugin, versionName).value.map(ApiResult)
case _ => Future.successful(NotFound)
}
}
Expand All @@ -308,53 +281,41 @@ final class ApiController @Inject()(api: OreRestfulApi,
def showStatusZ = Action(Ok(this.status.json))

def syncSso() = Action.async { implicit request =>
this.forms.SyncSso.bindFromRequest.fold(
hasErrors => Future.successful(BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson))),
success = formData => {
val sso = formData._1
val sig = formData._2
val apiKey = formData._3


val confApiKey = this.config.security.get[String]("sso.apikey")
val confSecret = this.config.security.get[String]("sso.secret")

if (apiKey != confApiKey) {
Future.successful(BadRequest("API Key not valid"))
} else if (CryptoUtils.hmac_sha256(confSecret, sso.getBytes("UTF-8")) != sig) {
Future.successful(BadRequest("Signature not matched"))
} else {
val query = Uri.Query(Base64.getMimeDecoder.decode(sso))

val external_id = query.get("external_id")
val confApiKey = this.config.security.get[String]("sso.apikey")
val confSecret = this.config.security.get[String]("sso.secret")

bindFormEitherT[Future](this.forms.SyncSso)(hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson)))
.filterOrElse(_._3 == confApiKey, BadRequest("API Key not valid")) //_3 is apiKey
.filterOrElse(
{ case (sso, sig, _) => CryptoUtils.hmac_sha256(confSecret, sso.getBytes("UTF-8")) == sig},
BadRequest("Signature not matched")
)
.map(t => Uri.Query(Base64.getMimeDecoder.decode(t._1))) //_1 is sso
.semiFlatMap(q => this.users.get(q.get("external_id").get.toInt).value.tupleLeft(q))
.map { case (query, optUser) =>
optUser.foreach { user =>
val email = query.get("email")
val username = query.get("username")
val name = query.get("name")
val avatar_url = query.get("avatar_url")
val add_groups = query.get("add_groups")

this.users.get(external_id.get.toInt).map { optUser =>
if (optUser.isDefined) {
val user = optUser.get

email.foreach(user.setEmail)
username.foreach(user.setUsername)
name.foreach(user.setFullName)
avatar_url.foreach(user.setAvatarUrl)
add_groups.foreach(groups =>
user.setGlobalRoles(
if(groups.trim == "")
Set.empty
else
groups.split(",").map(group => RoleTypes.withInternalName(group)).toSet[RoleType]
)
)
}

Ok(Json.obj("status" -> "success"))
email.foreach(user.setEmail)
username.foreach(user.setUsername)
name.foreach(user.setFullName)
avatar_url.foreach(user.setAvatarUrl)
add_groups.foreach { groups =>
user.setGlobalRoles(
if (groups.trim == "")
Set.empty
else
groups.split(",").map(group => RoleTypes.withInternalName(group)).toSet[RoleType]
)
}

}
}
)

Ok(Json.obj("status" -> "success"))
}.merge
}
}
Loading

0 comments on commit 6f78069

Please sign in to comment.