All Downloads are FREE. Search and download functionalities are using the official Maven repository.

controllers.BackOfficeController.scala Maven / Gradle / Ivy

The newest version!
package otoroshi.controllers

import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.util.FastFuture
import akka.http.scaladsl.util.FastFuture._
import akka.stream.scaladsl.{Sink, Source}
import akka.util.ByteString
import ch.qos.logback.classic.{Level, LoggerContext}
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.google.common.base.Charsets
import com.nimbusds.jose.jwk.KeyType
import io.otoroshi.wasm4s.scaladsl._
import org.joda.time.DateTime
import org.mindrot.jbcrypt.BCrypt
import org.slf4j.LoggerFactory
import otoroshi.actions.{ApiActionContext, BackOfficeAction, BackOfficeActionAuth, BackOfficeActionContextAuth}
import otoroshi.auth._
import otoroshi.env.Env
import otoroshi.events._
import otoroshi.events.impl.{ElasticReadsAnalytics, ElasticTemplates, ElasticUtils, ElasticVersion}
import otoroshi.jobs.AnonymousReportingJobConfig
import otoroshi.jobs.newengine.NewEngine
import otoroshi.jobs.updates.SoftwareUpdatesJobs
import otoroshi.models.RightsChecker.SuperAdminOnly
import otoroshi.models._
import otoroshi.next.models.{GraphQLFormats, NgRoute, NgRouteComposition, NgTarget}
import otoroshi.next.plugins.EurekaServerSink
import otoroshi.next.plugins.api.NgPluginHelper
import otoroshi.next.proxy.{BackOfficeRequest, ProxyEngine}
import otoroshi.script.RequestHandler
import otoroshi.security._
import otoroshi.ssl._
import otoroshi.ssl.pki.models.{GenCertResponse, GenCsrQuery}
import otoroshi.utils.http.MtlsConfig
import otoroshi.utils.http.RequestImplicits._
import otoroshi.utils.infotoken.InfoTokenHelper
import otoroshi.utils.syntax.implicits._
import otoroshi.utils.yaml.Yaml
import play.api.Logger
import play.api.http.{HttpEntity, HttpRequestHandler}
import play.api.libs.json._
import play.api.libs.streams.Accumulator
import play.api.libs.ws.SourceBody
import play.api.mvc._

import java.util.Base64
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

case class ServiceLike(entity: EntityLocationSupport, groups: Seq[String]) extends EntityLocationSupport {
  def id: String                                = internalId
  override def internalId: String               = entity.internalId
  override def json: JsValue                    = entity.json
  override def theName: String                  = entity.theName
  override def theDescription: String           = entity.theDescription
  override def theTags: Seq[String]             = entity.theTags
  override def theMetadata: Map[String, String] = entity.theMetadata
  override def location: EntityLocation         = entity.location
}

object ServiceLike {
  def fromService(service: ServiceDescriptor): ServiceLike           = ServiceLike(service, service.groups)
  def fromRoute(service: NgRoute): ServiceLike                       = ServiceLike(service, service.groups)
  def fromRouteComposition(service: NgRouteComposition): ServiceLike = ServiceLike(service, service.groups)
}

case class BackofficeFlags(
    env: Env,
    _useAkkaHttpClient: Option[Boolean] = None,
    _logUrl: Option[Boolean] = None,
    _logStats: Option[Boolean] = None,
    _requestTimeout: Option[FiniteDuration] = None
) {
  lazy val useAkkaHttpClient: Boolean     = _useAkkaHttpClient
    .orElse(env.configuration.betterGetOptional[Boolean]("otoroshi.backoffice.flags.useAkkaHttpClient"))
    .getOrElse(false)
  lazy val logUrl: Boolean                =
    _logUrl.orElse(env.configuration.betterGetOptional[Boolean]("otoroshi.backoffice.flags.logUrl")).getOrElse(false)
  lazy val logStats: Boolean              =
    _logStats
      .orElse(env.configuration.betterGetOptional[Boolean]("otoroshi.backoffice.flags.logStats"))
      .getOrElse(false)
  lazy val requestTimeout: FiniteDuration = _requestTimeout
    .orElse(
      env.configuration
        .betterGetOptional[Long]("otoroshi.backoffice.flags.requestTimeout")
        .map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
    )
    .getOrElse(1.minute)
  def rawJson: JsValue                    = Json
    .obj()
    .applyOnWithOpt(_useAkkaHttpClient) { case (obj, _useAkkaHttpClient) =>
      obj ++ Json.obj("useAkkaHttpClient" -> _useAkkaHttpClient)
    }
    .applyOnWithOpt(_logUrl) { case (obj, _logUrl) => obj ++ Json.obj("logUrl" -> _logUrl) }
    .applyOnWithOpt(_logStats) { case (obj, _logStats) => obj ++ Json.obj("logStats" -> _logStats) }
    .applyOnWithOpt(_requestTimeout) { case (obj, _requestTimeout) =>
      obj ++ Json.obj("requestTimeout" -> _requestTimeout.toMillis)
    }
}

object BackofficeFlags {
  private val ref                                                                         = new AtomicReference[(Long, BackofficeFlags)]()
  def fromJson(json: JsValue)(implicit env: Env): BackofficeFlags = {
    val useAkkaHttpClient = json.select("useAkkaHttpClient").asOpt[Boolean]
    val logUrl            = json.select("logUrl").asOpt[Boolean]
    val logStats          = json.select("logStats").asOpt[Boolean]
    val requestTimeout    = json.select("requestTimeout").asOpt[Long].map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
    BackofficeFlags(env, useAkkaHttpClient, logUrl, logStats, requestTimeout)
  }
  def fill()(implicit ec: ExecutionContext, env: Env): Unit = {
    env.datastores.rawDataStore.get(s"${env.storageRoot}:backoffice:flags").map {
      case None          =>
        ref.set((System.currentTimeMillis(), BackofficeFlags(env)))
      case Some(bodyRaw) => {
        val flags = fromJson(bodyRaw.utf8String.parseJson)
        ref.set((System.currentTimeMillis(), flags))
      }
    }
  }
  def latest(implicit ec: ExecutionContext, env: Env): BackofficeFlags = {
    Option(ref.get()) match {
      case None                                                               =>
        fill()
        BackofficeFlags(env)
      case Some((time, flags)) if (time + 5000L) < System.currentTimeMillis() =>
        fill()
        flags
      case Some((_, flags))                                                   =>
        flags
    }
  }
  def writeJson(flags: JsValue)(implicit ec: ExecutionContext, env: Env): BackofficeFlags = write(fromJson(flags))
  def write(flags: BackofficeFlags)(implicit ec: ExecutionContext, env: Env): BackofficeFlags = {
    env.datastores.rawDataStore.set(s"${env.storageRoot}:backoffice:flags", flags.rawJson.stringify.byteString, None)
    flags
  }
}

class BackOfficeController(
    BackOfficeAction: BackOfficeAction,
    BackOfficeActionAuth: BackOfficeActionAuth,
    handlerRef: AtomicReference[HttpRequestHandler],
    cc: ControllerComponents
)(implicit
    env: Env
) extends AbstractController(cc) {

  implicit lazy val ec  = env.otoroshiExecutionContext
  implicit lazy val lat = env.otoroshiMaterializer

  lazy val handler       = handlerRef.get()
  lazy val logger        = Logger("otoroshi-backoffice-api")
  lazy val commitVersion = Option(System.getenv("COMMIT_ID")).getOrElse(env.otoroshiVersion)

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Proxy
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  val sourceBodyParser = BodyParser("BackOfficeApi BodyParser") { _ =>
    Accumulator.source[ByteString].map(Right.apply)
  }

  def getFlags() = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(RightsChecker.SuperAdminOnly) {
      Ok(BackofficeFlags.latest.rawJson).future
    }
  }

  def writeFlags() = BackOfficeActionAuth.async(parse.json) { ctx =>
    ctx.checkRights(RightsChecker.SuperAdminOnly) {
      val body  = ctx.request.body
      val flags = BackofficeFlags.writeJson(body)
      Ok(flags.rawJson).future
    }
  }

  def proxyAdminApi(path: String) = BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
    env.datastores.apiKeyDataStore.findById(env.backOfficeApiKey.clientId).flatMap {
      case None                                  =>
        FastFuture.successful(
          NotFound(
            Json.obj(
              "error" -> "admin apikey not found !"
            )
          )
        )
      case Some(apikey) if env.backofficeUsePlay => passWithPlay(ctx, apikey)
      case Some(apikey)                          => passWithOldEngine(ctx, apikey, path)
    }
  }

  private def passWithPlay(
      ctx: BackOfficeActionContextAuth[Source[ByteString, _]],
      apikey: ApiKey
  ): Future[Result] = {
    logger.debug(s"using play for ${ctx.request.method} ${ctx.request.theUrl}")
    val host               = env.adminApiHost
    val request            = new BackOfficeRequest(ctx.request, host, apikey, ctx.user, env)
    val (nreq, reqHandler) = handler.handlerForRequest(request)
    reqHandler match {
      case a: EssentialAction => a.apply(nreq).run(request.body)
      case _                  => Future.failed(new RuntimeException("websocket not supported here !"))
    }
  }

  // private def passWithNewEngine(
  //     ctx: BackOfficeActionContextAuth[Source[ByteString, _]],
  //     apikey: ApiKey
  // ): Future[Result] = {
  //   logger.debug(s"using new engine for ${ctx.request.method} ${ctx.request.theUrl}")
  //   env.scriptManager.getAnyScript[RequestHandler](NgPluginHelper.pluginId[ProxyEngine]) match {
  //     case Left(err)         =>
  //       Results.InternalServerError(Json.obj("error" -> "admin_api_error", "error_description" -> err)).vfuture
  //     case Right(raw_engine) => {
  //       val engine          = raw_engine.asInstanceOf[ProxyEngine]
  //       implicit val global = env.datastores.globalConfigDataStore.latest()
  //       val raw_request     = ctx.request
  //       val host            = env.adminApiExposedHost
  //       val request         = new BackOfficeRequest(raw_request, host, apikey, ctx.user, env)
  //       engine.handleRequest(request, engine.getConfig())
  //     }
  //   }
  // }

  private def passWithOldEngine(
      ctx: BackOfficeActionContextAuth[Source[ByteString, _]],
      apikey: ApiKey,
      path: String
  ): Future[Result] = {
    logger.debug(s"using old engine ! ${ctx.request.method} ${ctx.request.theUrl}")
    val host                   = env.adminApiExposedHost
    val localUrl               =
      if (env.adminApiProxyHttps) s"https://127.0.0.1:${env.httpsPort}" else s"http://127.0.0.1:${env.port}"
    val url                    =
      if (env.adminApiProxyUseLocal) localUrl else s"https://${env.adminApiExposedHost}${env.exposedHttpsPort}"
    lazy val currentReqHasBody = ctx.request.theHasBody
    val flags                  = BackofficeFlags.latest
    if (flags.logUrl) {
      logger.info(s"[${ctx.request.id}] calling ${ctx.request.method} $url/$path with Host = $host")
    }
    if (logger.isDebugEnabled) logger.debug(s"Calling ${ctx.request.method} $url/$path with Host = $host")
    val headers                = Seq(
      "Host"                           -> host,
      "X-Forwarded-For"                -> ctx.request.theIpAddress,
      env.Headers.OtoroshiVizFromLabel -> "Otoroshi Admin UI",
      env.Headers.OtoroshiVizFrom      -> "otoroshi-admin-ui",
      env.Headers.OtoroshiClientId     -> apikey.clientId,
      env.Headers.OtoroshiClientSecret -> apikey.clientSecret,
      env.Headers.OtoroshiAdminProfile -> Base64.getUrlEncoder.encodeToString(
        Json.stringify(ctx.user.profile).getBytes(Charsets.UTF_8)
      ),
      "Otoroshi-Tenant"                -> ctx.request.headers.get("Otoroshi-Tenant").getOrElse("default"),
      "Otoroshi-BackOffice-User"       -> JWT
        .create()
        .withClaim("user", Json.stringify(ctx.user.toJson))
        .sign(Algorithm.HMAC512(apikey.clientSecret))
    ) ++ ctx.request.headers.get("Content-Type").filter(_ => currentReqHasBody).map { ctype =>
      "Content-Type" -> ctype
    } ++ ctx.request.headers.get("Accept").map { accept =>
      "Accept" -> accept
    } ++ ctx.request.headers.get("X-Content-Type").map(v => "X-Content-Type" -> v)

    if (flags.useAkkaHttpClient) {

      val builder = env.Ws // MTLS needed here ???
        .akkaUrl(s"$url/$path")
        .withHttpHeaders(headers: _*)
        .withFollowRedirects(false)
        .withMethod(ctx.request.method)
        .withRequestTimeout(flags.requestTimeout)
        .withQueryStringParameters(ctx.request.queryString.toSeq.map(t => (t._1, t._2.head)): _*)

      val builderWithBody = if (currentReqHasBody) {
        builder.withBody(SourceBody(ctx.request.body))
      } else {
        builder
      }

      val start = System.currentTimeMillis()
      if (flags.logStats)
        logger.info(
          s"[${ctx.request.id}] akka - starting admin-api call: ${ctx.request.method} ${ctx.request.thePath}"
        )
      if (logger.isDebugEnabled)
        logger.debug(
          s"[${ctx.request.id}] akka - starting admin-api call: ${ctx.request.method} ${ctx.request.thePath}"
        )
      builderWithBody
        .stream()
        .fast
        .map { res =>
          if (flags.logStats)
            logger.info(
              s"[${ctx.request.id}] akka - got result for admin-api call: ${ctx.request.method} ${ctx.request.thePath} in ${System
                .currentTimeMillis() - start}ms : ${res.status} - ${res.headers}"
            )
          if (logger.isDebugEnabled)
            logger.debug(
              s"[${ctx.request.id}] akka - got result for admin-api call: ${ctx.request.method} ${ctx.request.thePath} in ${System
                .currentTimeMillis() - start}ms : ${res.status} - ${res.headers}"
            )
          val ctype = res.headers.get("Content-Type").flatMap(_.headOption).getOrElse("application/json")
          Status(res.status)
            .sendEntity(
              HttpEntity.Streamed(
                res.bodyAsSource
                  .recover {
                    case t: java.util.concurrent.TimeoutException if path.contains("/live") =>
                      ByteString.empty
                  }
                  .alsoTo(Sink.onComplete { case e =>
                    if (flags.logStats)
                      logger.info(
                        s"[${ctx.request.id}] akka - for admin-api call: ${ctx.request.method} ${ctx.request.thePath} body has been consumed in ${System
                          .currentTimeMillis() - start}ms"
                      )
                    if (logger.isDebugEnabled)
                      logger.debug(
                        s"[${ctx.request.id}] akka - for admin-api call: ${ctx.request.method} ${ctx.request.thePath} body has been consumed in ${System
                          .currentTimeMillis() - start}ms"
                      )
                  }),
                res.headers.get("Content-Length").flatMap(_.lastOption).map(_.toInt),
                res.headers.get("Content-Type").flatMap(_.headOption)
              )
            )
            .withHeaders(
              res.headers
                .mapValues(_.head)
                .toSeq
                .filter(_._1 != "Content-Type")
                .filter(_._1 != "Content-Length")
                .filter(_._1 != "Transfer-Encoding"): _*
            )
            .as(ctype)
        }
    } else {
      val builder = env.Ws // MTLS needed here ???
        .url(s"$url/$path")
        .withHttpHeaders(headers: _*)
        .withFollowRedirects(false)
        .withMethod(ctx.request.method)
        .withRequestTimeout(flags.requestTimeout)
        .withQueryStringParameters(ctx.request.queryString.toSeq.map(t => (t._1, t._2.head)): _*)

      val builderWithBody = if (currentReqHasBody) {
        builder.withBody(SourceBody(ctx.request.body))
      } else {
        builder
      }

      val start = System.currentTimeMillis()
      if (flags.logStats)
        logger.info(s"[${ctx.request.id}] starting admin-api call: ${ctx.request.method} ${ctx.request.thePath}")
      if (logger.isDebugEnabled)
        logger.debug(s"[${ctx.request.id}] starting admin-api call: ${ctx.request.method} ${ctx.request.thePath}")
      builderWithBody
        .stream()
        .fast
        .map { res =>
          if (flags.logStats)
            logger.info(
              s"[${ctx.request.id}] got result for admin-api call: ${ctx.request.method} ${ctx.request.thePath} in ${System
                .currentTimeMillis() - start}ms : ${res.status} - ${res.headers}"
            )
          if (logger.isDebugEnabled)
            logger.debug(
              s"[${ctx.request.id}] got result for admin-api call: ${ctx.request.method} ${ctx.request.thePath} in ${System
                .currentTimeMillis() - start}ms : ${res.status} - ${res.headers}"
            )
          val ctype = res.headers.get("Content-Type").flatMap(_.headOption).getOrElse("application/json")
          Status(res.status)
            .sendEntity(
              HttpEntity.Streamed(
                Source
                  .lazySource(() => res.bodyAsSource)
                  .recover {
                    case t: java.util.concurrent.TimeoutException if path.contains("/live") =>
                      ByteString.empty
                  }
                  .alsoTo(Sink.onComplete { case e =>
                    if (flags.logStats)
                      logger.info(
                        s"[${ctx.request.id}] for admin-api call: ${ctx.request.method} ${ctx.request.thePath} body has been consumed in ${System
                          .currentTimeMillis() - start}ms"
                      )
                    if (logger.isDebugEnabled)
                      logger.debug(
                        s"[${ctx.request.id}] for admin-api call: ${ctx.request.method} ${ctx.request.thePath} body has been consumed in ${System
                          .currentTimeMillis() - start}ms"
                      )
                  }),
                res.headers.get("Content-Length").flatMap(_.lastOption).map(_.toInt),
                res.headers.get("Content-Type").flatMap(_.headOption)
              )
            )
            .withHeaders(
              res.headers
                .mapValues(_.head)
                .toSeq
                .filter(_._1 != "Content-Type")
                .filter(_._1 != "Content-Length")
                .filter(_._1 != "Transfer-Encoding"): _*
            )
            .as(ctype)
        }
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Pure routing
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  def robotTxt =
    Action { req =>
      if (logger.isDebugEnabled) logger.debug(s"Rendering robot.txt on ${req.theProtocol}://${req.theHost}/robot.txt")
      Ok("""User-agent: *
         |Disallow: /""".stripMargin)
    }

  def version =
    BackOfficeActionAuth {
      Ok(
        Json.obj(
          "version"        -> commitVersion,
          "currentVersion" -> env.otoroshiVersion,
          "nextVersion"    -> SoftwareUpdatesJobs.latestVersionHolder.get()
        )
      )
    }

  def getEnv() =
    BackOfficeActionAuth.async { ctx =>
      val hash = BCrypt.hashpw("password", BCrypt.gensalt())
      for {
        config      <- env.datastores.globalConfigDataStore.singleton()
        users       <- env.datastores.simpleAdminDataStore.findAll()
        refusedOpt  <- env.datastores.rawDataStore.get(s"${env.storageRoot}:backoffice:anonymous-reporting-refused")
        preferences <- env.datastores.adminPreferencesDatastore.getPreferencesOrSetDefault(ctx.user.email)
      } yield {
        val reporting                 = AnonymousReportingJobConfig.fromEnv(env)
        val refusedDate               = refusedOpt.map(_.utf8String).map(DateTime.parse)
        val refused: JsValue          = refusedDate.map(_.toString).map(JsString.apply).getOrElse(JsNull)
        val (shouldAsk, shouldEnable) = if (reporting.enabled) {
          if (!config.anonymousReporting) {
            refusedDate match {
              case None                                                      => (true, false)
              case Some(date) if date.plusMonths(6).isBefore(DateTime.now()) => (true, false)
              case _                                                         => (false, false)
            }
          } else {
            (false, false)
          }
        } else {
          (true, true)
        }
        val changePassword            = users.filter { user =>
          //(user \ "password").as[String] == hash &&
          user.username == "[email protected]"
        }.nonEmpty
        Ok(
          Json.obj(
            "user_preferences"        -> preferences.json,
            "newEngineEnabled"        -> NewEngine.enabledFromConfig(config, env),
            "initWithNewEngine"       -> config.initWithNewEngine,
            "scriptingEnabled"        -> env.scriptingEnabled,
            "otoroshiLogo"            -> env.otoroshiLogo,
            "clusterRole"             -> env.clusterConfig.mode.name,
            "snowMonkeyRunning"       -> config.snowMonkeyConfig.enabled,
            "changePassword"          -> changePassword,
            "mailgun"                 -> config.mailerSettings.isDefined,
            "clevercloud"             -> config.cleverSettings.isDefined,
            "apiReadOnly"             -> config.apiReadOnly,
            "u2fLoginOnly"            -> config.u2fLoginOnly,
            "env"                     -> env.env,
            "redirectToDev"           -> false,
            "userAdmin"               -> ctx.user.rights.superAdmin,
            "superAdmin"              -> ctx.user.rights.superAdmin,
            "tenantAdmin"             -> ctx.user.rights.tenantAdmin(ctx.currentTenant),
            "currentTenant"           -> ctx.currentTenant.value,
            "bypassUserRightsCheck"   -> env.bypassUserRightsCheck,
            "clientIdHeader"          -> env.Headers.OtoroshiClientId,
            "clientSecretHeader"      -> env.Headers.OtoroshiClientSecret,
            "version"                 -> SoftwareUpdatesJobs.latestVersionHolder.get(),
            "currentVersion"          -> env.otoroshiVersion,
            "commitVersion"           -> commitVersion,
            "adminApiId"              -> env.backOfficeServiceId,
            "adminGroupId"            -> env.backOfficeGroupId,
            "adminApikeyId"           -> env.backOfficeApiKeyClientId,
            "user"                    -> ctx.user.email,
            "instanceId"              -> config.otoroshiId,
            "staticExposedDomain"     -> env.staticExposedDomain.map(JsString.apply).getOrElse(JsNull).as[JsValue],
            "providerDashboardUrl"    -> env.providerDashboardUrl.map(JsString.apply).getOrElse(JsNull).as[JsValue],
            "providerDashboardTitle"  -> env.providerDashboardTitle,
            "providerDashboardSecret" -> env.providerDashboardSecret,
            "instanceId"              -> config.otoroshiId,
            "instanceName"            -> env.name,
            "anonymousReporting"      -> Json.obj(
              "static"        -> reporting.enabled,
              "global"        -> config.anonymousReporting,
              "refused"       -> refused,
              "should_ask"    -> shouldAsk,
              "should_enable" -> shouldEnable
            )
          )
        )
      }
    }

  def index =
    BackOfficeAction.async { ctx =>
      env.datastores.globalConfigDataStore.singleton().map { config =>
        val thridPartyLoginEnabled = config.backOfficeAuthRef.nonEmpty
        ctx.user match {
          case Some(user)                      => Redirect("/bo/dashboard")
          case None if config.u2fLoginOnly     => Redirect(routes.U2FController.loginPage())
          case None if thridPartyLoginEnabled  =>
            Ok(otoroshi.views.html.backoffice.index(thridPartyLoginEnabled, ctx.user, ctx.request, env))
          case None if !thridPartyLoginEnabled => Redirect(routes.U2FController.loginPage())
        }
      }
    }

  def dashboard =
    BackOfficeActionAuth.async { ctx =>
      env.datastores.globalConfigDataStore.singleton().flatMap { config =>
        env.datastores.tenantDataStore.findAll().map { tenants =>
          val userTenants = tenants
            .filter(t => ctx.user.rights.rights.exists(r => r.tenant.canRead && r.tenant.matches(t.id)))
            .filterNot(_.id == TenantId.all)
            .map(_.id.value)
          Ok(
            otoroshi.views.html.backoffice.dashboard(
              ctx.user,
              config,
              env,
              env.otoroshiVersion,
              userTenants,
              ctx.request.session.get("ui-mode").getOrElse("dark")
            )
          )
        }
      }
    }

  def dashboardRoutes(ui: String) =
    BackOfficeActionAuth.async { ctx =>
      env.datastores.globalConfigDataStore.singleton().flatMap { config =>
        env.datastores.tenantDataStore.findAll().map { tenants =>
          val userTenants = tenants
            .filter(t => ctx.user.rights.rights.exists(r => r.tenant.canRead && r.tenant.matches(t.id)))
            .filterNot(_.id == TenantId.all)
            .map(_.id.value)
          Ok(
            otoroshi.views.html.backoffice.dashboard(
              ctx.user,
              config,
              env,
              env.otoroshiVersion,
              userTenants,
              ctx.request.session.get("ui-mode").getOrElse("dark")
            )
          )
        }
      }
    }

  def error(message: Option[String]) =
    BackOfficeAction { ctx =>
      Ok(otoroshi.views.html.oto.error(message.getOrElse("Error message"), env))
    }

  def documentationFrame(lineId: String, serviceId: String) =
    BackOfficeActionAuth.async { ctx =>
      env.datastores.serviceDescriptorDataStore.findById(serviceId).map {
        case Some(descriptor) if !ctx.canUserRead(descriptor) => ApiActionContext.forbidden
        case Some(descriptor)                                 => Ok(otoroshi.views.html.backoffice.documentationframe(descriptor, env))
        case None                                             => NotFound(Json.obj("error" -> s"Service with id $serviceId not found"))
      }
    }

  def documentationFrameDescriptor(lineId: String, serviceId: String) =
    BackOfficeActionAuth.async { ctx =>
      import scala.concurrent.duration._
      env.datastores.serviceDescriptorDataStore.findById(serviceId).flatMap {
        case Some(descriptor) if !ctx.canUserRead(descriptor)            => ApiActionContext.fforbidden
        case Some(service) if service.api.openApiDescriptorUrl.isDefined => {
          val state = IdGenerator.extendedToken(128)
          val claim = OtoroshiClaim(
            iss = env.Headers.OtoroshiIssuer,
            sub = "Documentation",
            aud = service.name,
            exp = DateTime.now().plusSeconds(30).toDate.getTime,
            iat = DateTime.now().toDate.getTime,
            jti = IdGenerator.uuid
          ).serialize(service.algoInfoFromOtoToBack)(env)
          val url   = service.api.openApiDescriptorUrl.get match {
            case uri if uri.startsWith("/") => s"${service.target.scheme}://${service.target.host}${uri}"
            case url                        => url
          }
          env.Ws // no need for mtls here
            .url(url)
            .withRequestTimeout(10.seconds)
            .withHttpHeaders(
              env.Headers.OtoroshiRequestId -> env.snowflakeGenerator.nextIdStr(),
              env.Headers.OtoroshiState     -> state,
              env.Headers.OtoroshiClaim     -> claim
            )
            .get()
            .map { resp =>
              try {
                val swagger = (resp.json.as[JsObject] \ "swagger").as[String]
                swagger match {
                  case "2.0" => Ok(Json.prettyPrint(resp.json)).as("application/json")
                  case "3.0" => Ok(Json.prettyPrint(resp.json)).as("application/json")
                  case _     =>
                    InternalServerError(otoroshi.views.html.oto.error(s"Swagger version $swagger not supported", env))
                }
              } catch {
                case e: Throwable => InternalServerError(Json.obj("error" -> e.getMessage))
              }
            }
        }
        case _                                                           => FastFuture.successful(NotFound(otoroshi.views.html.oto.error("Service not found", env)))
      }
    }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // APIs that are only relevant here
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  def cleverApps() =
    BackOfficeActionAuth.async { ctx =>
      val paginationPage: Int     = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
      val paginationPageSize: Int =
        ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(Int.MaxValue)
      val paginationPosition      = (paginationPage - 1) * paginationPageSize
      env.datastores.globalConfigDataStore.singleton().flatMap { globalConfig =>
        env.datastores.serviceDescriptorDataStore.findAll().flatMap { services =>
          globalConfig.cleverClient match {
            case Some(client) => {
              client.apps(client.orgaId).map { cleverapps =>
                val apps = cleverapps.value
                  .map { app =>
                    val id                 = (app \ "id").as[String]
                    val name               = (app \ "name").as[String]
                    val hosts: Seq[String] =
                      (app \ "vhosts").as[JsArray].value.map(vhost => (vhost \ "fqdn").as[String])
                    val preferedHost       =
                      hosts.filterNot(h => h.contains("cleverapps.io")).headOption.getOrElse(hosts.head)
                    val service            =
                      services.filter(ctx.canUserRead).find(s => s.targets.exists(t => hosts.contains(t.host)))
                    Json.obj(
                      "name"    -> (app \ "name").as[String],
                      "id"      -> id,
                      "url"     -> s"https://${preferedHost}/",
                      "console" -> s"https://console.clever-cloud.com/organisations/${client.orgaId}/applications/$id",
                      "exists"  -> service.isDefined,
                      "host"    -> preferedHost,
                      "otoUrl"  -> s"/lines/${service.map(_.env).getOrElse("--")}/services/${service.map(_.id).getOrElse("--")}"
                    )
                  }
                  .drop(paginationPosition)
                  .take(paginationPageSize)
                Ok(JsArray(apps))
              }
            }
            case None         => FastFuture.successful(Ok(Json.arr()))
          }
        }
      }
    }

  def eurekaServers() =
    BackOfficeActionAuth.async { _ =>
      env.datastores.routeDataStore.findAll().map { routes =>
        Ok(
          JsArray(
            routes
              .filter(r => r.plugins.hasPlugin[EurekaServerSink])
              .map(_.json)
          )
        )
      }
    }

  def externalEurekaServers() =
    BackOfficeActionAuth.async { ctx =>
      ctx.request.getQueryString("url") match {
        case Some(url) =>
          env.Ws
            .url(s"$url/apps")
            .withMethod("GET")
            .withHttpHeaders(Seq("Accept" -> "application/json"): _*)
            .execute()
            .map { res =>
              if (res.status == 200) {
                Ok(res.bodyAsBytes.utf8String.parseJson)
              } else {
                BadRequest(Json.obj("error" -> s"bad server response: ${res.status} - ${res.headers} - ${res.body}"))
              }
            }
        case None      =>
          BadRequest(Json.obj("error" -> "missing url")).vfuture
      }
    }

  def eurekaServerApps(id: String) =
    BackOfficeActionAuth.async { _ =>
      env.datastores.rawDataStore
        .allMatching(s"${env.storageRoot}:plugins:eureka-server-$id:apps*")
        .map { rawApps =>
          Ok(
            JsArray(
              rawApps.map(app => Json.parse(app.utf8String))
            )
          )
        }
    }

  def panicMode() =
    BackOfficeActionAuth.async { ctx =>
      ctx.checkRights(SuperAdminOnly) {
        env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { c =>
          c.copy(u2fLoginOnly = true, apiReadOnly = true).save()
        } flatMap { _ =>
          env.datastores.backOfficeUserDataStore.discardAllSessions()
        } map { _ =>
          val event = BackOfficeEvent(
            env.snowflakeGenerator.nextIdStr(),
            env.env,
            ctx.user,
            "ACTIVATE_PANIC_MODE",
            s"Admin activated panic mode",
            ctx.from,
            ctx.ua,
            Json.obj()
          )
          Audit.send(event)
          Alerts.send(PanicModeAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, event, ctx.from, ctx.ua))
          Ok(Json.obj("done" -> true))
        } recover { case _ =>
          Ok(Json.obj("done" -> false))
        }
      }
    }

  case class SearchedService(
      name: String,
      id: String,
      groupId: String,
      env: String,
      typ: String,
      location: EntityLocation
  ) extends EntityLocationSupport {
    override def internalId: String      = id
    override def json: JsValue           = Json.obj()
    def theDescription: String           = name
    def theMetadata: Map[String, String] = Map.empty
    def theName: String                  = name
    def theTags: Seq[String]             = Seq.empty
  }

  def searchServicesApi() =
    BackOfficeActionAuth.async(parse.json) { ctx =>
      val query                            = (ctx.request.body \ "query").asOpt[String].getOrElse("--").toLowerCase()
      Audit.send(
        BackOfficeEvent(
          env.snowflakeGenerator.nextIdStr(),
          env.env,
          ctx.user,
          "SERVICESEARCH",
          "user searched for a service",
          ctx.from,
          ctx.ua,
          Json.obj(
            "query" -> query
          )
        )
      )
      val fu: Future[Seq[SearchedService]] =
        for {
          services   <- env.datastores.serviceDescriptorDataStore.findAll()
          tcServices <- env.datastores.tcpServiceDataStore.findAll()
          routes     <- env.datastores.routeDataStore.findAll()
        } yield {
          val finalServices =
            services.map(s =>
              SearchedService(s.name, s.id, s.groups.headOption.getOrElse("default"), s.env, "http", s.location)
            ) ++
            routes.map(s =>
              SearchedService(s.name, s.id, s.groups.headOption.getOrElse("default"), "route", "route", s.location)
            ) ++
            tcServices.map(s => SearchedService(s.name, s.id, "tcp", "prod", "tcp", s.location))
          finalServices
        }
      fu.map { services =>
        val filtered = services.filter(ctx.canUserRead).filter { service =>
          service.id.toLowerCase() == query || service.name.toLowerCase().contains(query) || service.env
            .toLowerCase()
            .contains(query)
        }
        Ok(
          JsArray(
            filtered.map(s =>
              Json.obj("groupId" -> s.groupId, "serviceId" -> s.id, "name" -> s.name, "env" -> s.env, "type" -> s.typ)
            )
          )
        )
      }
    }

  def changeLogLevel(name: String, newLevel: Option[String]) =
    BackOfficeActionAuth.async { ctx =>
      ctx.checkRights(SuperAdminOnly) {
        val loggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
        val _logger       = loggerContext.getLogger(name)
        val oldLevel      = Option(_logger.getLevel).map(_.levelStr).getOrElse(Level.OFF.levelStr)
        _logger.setLevel(newLevel.map(v => Level.valueOf(v)).getOrElse(Level.ERROR))
        Ok(Json.obj("name" -> name, "oldLevel" -> oldLevel, "newLevel" -> _logger.getLevel.levelStr)).future
      }
    }

  def getLogLevel(name: String) =
    BackOfficeActionAuth.async { ctx =>
      ctx.checkRights(SuperAdminOnly) {
        val loggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
        val _logger       = loggerContext.getLogger(name)
        Ok(Json.obj("name" -> name, "level" -> _logger.getLevel.levelStr)).future
      }
    }

  def getAllLoggers() =
    BackOfficeActionAuth.async { ctx =>
      ctx.checkRights(SuperAdminOnly) {
        import collection.JavaConverters._

        val paginationPage: Int     = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
        val paginationPageSize: Int =
          ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(Int.MaxValue)
        val paginationPosition      = (paginationPage - 1) * paginationPageSize

        val loggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
        val rawLoggers    = loggerContext.getLoggerList.asScala.drop(paginationPosition).take(paginationPageSize)
        val loggers       = JsArray(rawLoggers.map(logger => {
          val level: String = Option(logger.getLevel).map(_.levelStr).getOrElse("OFF")
          Json.obj("name" -> logger.getName, "level" -> level)
        }))
        Ok(loggers).future
      }
    }

  case class ServiceRate(rate: Double, name: String, id: String)

  def mostCalledServices() =
    BackOfficeActionAuth.async { ctx =>
      val paginationPage: Int     = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
      val paginationPageSize: Int =
        ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(10)
      val paginationPosition      = (paginationPage - 1) * paginationPageSize

      env.datastores.serviceDescriptorDataStore.findAll().flatMap { services =>
        Future.sequence(
          services.map(s =>
            env.datastores.serviceDescriptorDataStore.callsPerSec(s.id).map(rate => ServiceRate(rate, s.name, s.id))
          )
        )
      } map { items =>
        items.sortWith(_.rate > _.rate).drop(paginationPosition).take(paginationPageSize)
      } map { items =>
        items.map { i =>
          val value: Double = Option(i.rate).filterNot(_.isInfinity).getOrElse(0.0)
          Json.obj(
            "rate" -> value,
            "name" -> i.name,
            "id"   -> i.id
          )
        }
      } map { items =>
        Ok(JsArray(items))
      }
    }

  def servicesMap() =
    BackOfficeActionAuth.async { ctx =>
      env.datastores.serviceGroupDataStore.findAll().flatMap { groups =>
        Future.sequence(
          groups.map { group =>
            env.datastores.serviceDescriptorDataStore.findByGroup(group.id).flatMap { services =>
              Future.sequence(services.map { service =>
                env.datastores.serviceDescriptorDataStore.callsPerSec(service.id).map(cps => (service, cps))
              })
            } map {
              case services if services.isEmpty  => Json.obj()
              case services if services.nonEmpty =>
                Json.obj(
                  "name"     -> group.name,
                  "children" -> JsArray(services.map { case (service, cps) =>
                    val size: Int = ((1.0 + cps) * 1000.0).toInt
                    Json.obj(
                      "name" -> service.name,
                      "env"  -> service.env,
                      "id"   -> service.id,
                      "size" -> size
                    )
                  })
                )
            }
          }
        )
      } map { children =>
        Json.obj("name" -> "Otoroshi Services", "children" -> children.filterNot(_ == Json.obj()))
      } map { json =>
        Ok(json)
      }
    }

  def fetchOpenIdConfiguration() =
    BackOfficeActionAuth.async(parse.json) { ctx =>
      import otoroshi.utils.http.Implicits._

      import scala.concurrent.duration._

      val id                  = (ctx.request.body \ "id").asOpt[String].getOrElse(IdGenerator.token(64))
      val name                = (ctx.request.body \ "name").asOpt[String].getOrElse("new oauth config")
      val desc                = (ctx.request.body \ "desc").asOpt[String].getOrElse("new oauth config")
      val clientId            = (ctx.request.body \ "clientId").asOpt[String].getOrElse("client")
      val clientSecret        = (ctx.request.body \ "clientSecret").asOpt[String].getOrElse("secret")
      val sessionCookieValues =
        (ctx.request.body \ "sessionCookieValues").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues())
      (ctx.request.body \ "url").asOpt[String] match {
        case None      =>
          FastFuture.successful(
            Ok(
              GenericOauth2ModuleConfig(
                id = id,
                name = name,
                desc = desc,
                clientId = clientId,
                clientSecret = clientSecret,
                oidConfig = None,
                tags = Seq.empty,
                metadata = Map.empty,
                sessionCookieValues = sessionCookieValues,
                clientSideSessionEnabled = true
              ).asJson
            )
          )
        case Some(url) => {
          env.Ws
            .url(url) // no need for mtls here
            .withRequestTimeout(10.seconds)
            .get()
            .map { resp =>
              if (resp.status == 200) {
                Try {
                  val config           = GenericOauth2ModuleConfig(
                    id = id,
                    name = name,
                    desc = desc,
                    oidConfig = Some(url),
                    tags = Seq.empty,
                    metadata = Map.empty,
                    sessionCookieValues = sessionCookieValues,
                    clientSideSessionEnabled = true
                  )
                  val body             = Json.parse(resp.body)
                  val issuer           = (body \ "issuer").asOpt[String].getOrElse("http://localhost:8082/")
                  val tokenUrl         = (body \ "token_endpoint").asOpt[String].getOrElse(config.tokenUrl)
                  val authorizeUrl     = (body \ "authorization_endpoint").asOpt[String].getOrElse(config.authorizeUrl)
                  val userInfoUrl      = (body \ "userinfo_endpoint").asOpt[String].getOrElse(config.userInfoUrl)
                  val introspectionUrl =
                    (body \ "introspection_endpoint").asOpt[String].getOrElse(config.introspectionUrl)
                  val loginUrl         = (body \ "authorization_endpoint").asOpt[String].getOrElse(authorizeUrl)
                  val logoutUrl        = (body \ "end_session_endpoint")
                    .asOpt[String]
                    .orElse((body \ "ping_end_session_endpoint").asOpt[String])
                    .getOrElse((issuer + "/logout").replace("//logout", "/logout"))
                  val jwksUri          = (body \ "jwks_uri").asOpt[String]
                  val scope            = (body \ "scopes_supported")
                    .asOpt[Seq[String]]
                    .map(_.mkString(" "))
                    .getOrElse("openid profile email name")
                  val claims           =
                    (body \ "claims_supported").asOpt[JsArray].map(Json.stringify).getOrElse("""["email","name"]""")
                  Ok(
                    config
                      .copy(
                        clientId = clientId,
                        clientSecret = clientSecret,
                        tokenUrl = tokenUrl,
                        authorizeUrl = authorizeUrl,
                        userInfoUrl = userInfoUrl,
                        introspectionUrl = introspectionUrl,
                        loginUrl = loginUrl,
                        logoutUrl = logoutUrl,
                        callbackUrl =
                          s"${env.rootScheme}${env.privateAppsHost}${env.privateAppsPort}/privateapps/generic/callback",
                        scope = scope,
                        claims = "",
                        accessTokenField = "access_token", // jwksUri.map(_ => "id_token").getOrElse("access_token"),
                        useJson = false,
                        useCookie = false,
                        readProfileFromToken = false,
                        nameField = (if (scope.contains(config.nameField)) config.nameField else config.emailField),
                        oidConfig = Some(url),
                        jwtVerifier = jwksUri.map(url =>
                          JWKSAlgoSettings(
                            url = url,
                            headers = Map.empty[String, String],
                            timeout = FiniteDuration(2000, TimeUnit.MILLISECONDS),
                            ttl = FiniteDuration(60 * 60 * 1000, TimeUnit.MILLISECONDS),
                            kty = KeyType.RSA,
                            None,
                            MtlsConfig.default
                          )
                        )
                      )
                      .asJson
                  )
                } getOrElse {
                  resp.ignore()
                  Ok(
                    GenericOauth2ModuleConfig(
                      id = id,
                      name = name,
                      desc = desc,
                      clientId = clientId,
                      clientSecret = clientSecret,
                      oidConfig = Some(url),
                      tags = Seq.empty,
                      metadata = Map.empty,
                      sessionCookieValues = sessionCookieValues,
                      clientSideSessionEnabled = true
                    ).asJson
                  )
                }
              } else {
                resp.ignore()
                Ok(
                  GenericOauth2ModuleConfig(
                    id = id,
                    name = name,
                    desc = desc,
                    clientId = clientId,
                    clientSecret = clientSecret,
                    oidConfig = Some(url),
                    tags = Seq.empty,
                    metadata = Map.empty,
                    sessionCookieValues = sessionCookieValues,
                    clientSideSessionEnabled = true
                  ).asJson
                )
              }
            }
        }
      }
    }

  def fetchSAMLConfiguration() = BackOfficeActionAuth.async(parse.json) { ctx =>
    import scala.xml.Elem
    import scala.xml.XML._
    Try {
      val xmlContent: Either[String, Elem] = (ctx.request.body \ "url").asOpt[String] match {
        case Some(url) => Right(load(url))
        case None      =>
          (ctx.request.body \ "xml").asOpt[String] match {
            case Some(content) => Right(loadString(content))
            case None          => Left("Missing body content")
          }
      }

      xmlContent match {
        case Left(err)         => FastFuture.successful(BadRequest(err))
        case Right(xmlContent) =>
          var metadata = (xmlContent \\ "EntitiesDescriptor").toString

          if (metadata.isEmpty)
            metadata = xmlContent.toString

          SamlAuthModuleConfig.fromDescriptor(metadata) match {
            case Left(err)     => FastFuture.successful(BadRequest(err))
            case Right(config) => FastFuture.successful(Ok(SamlAuthModuleConfig._fmt.writes(config)))
          }
      }
    } recover { case e: Throwable =>
      FastFuture.successful(
        BadRequest(
          Json.obj(
            "error" -> e.getMessage
          )
        )
      )
    } get
  }

  def fetchBodiesFor(serviceId: String, requestId: String) =
    BackOfficeActionAuth.async { ctx =>
      for {
        req  <- env.datastores.rawDataStore.get(s"${env.storageRoot}:bodies:$serviceId:$requestId:request")
        resp <- env.datastores.rawDataStore.get(s"${env.storageRoot}:bodies:$serviceId:$requestId:response")
      } yield {
        if (req.isEmpty && resp.isEmpty) {
          NotFound(Json.obj("error" -> "Bodies not found"))
        } else {
          Ok(
            Json.obj(
              "response" -> resp.map(_.utf8String).map(Json.parse).getOrElse(JsNull).as[JsValue],
              "request"  -> req.map(_.utf8String).map(Json.parse).getOrElse(JsNull).as[JsValue]
            )
          )
        }
      }
    }

  def resetCircuitBreakers(id: String) =
    BackOfficeActionAuth { ctx =>
      env.circuitBeakersHolder.resetCircuitBreakersFor(id)
      Ok(Json.obj("done" -> true))
    }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // TODO: APIs already in admin API, remove it at some point ?
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  /*
  def sessions() = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(TenantAdminOnly) {
      val paginationPage: Int = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
      val paginationPageSize: Int =
        ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(Int.MaxValue)
      val paginationPosition = (paginationPage - 1) * paginationPageSize
      env.datastores.backOfficeUserDataStore.tsessions() map { sessions =>
        Ok(JsArray(sessions.filter(ctx.canUserRead).drop(paginationPosition).take(paginationPageSize).map(_.json)))
      }
    }
  }

  def discardSession(id: String) = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(SuperAdminOnly) {
      env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
        env.datastores.backOfficeUserDataStore.findById(id).flatMap {
          case None => Results.NotFound(Json.obj("error" -> "Session not found")).future
          case Some(session) if !ctx.canUserWrite(session) => ApiActionContext.fforbidden
          case Some(_) => {
            env.datastores.backOfficeUserDataStore.discardSession(id) map { _ =>
              val event = BackOfficeEvent(
                env.snowflakeGenerator.nextIdStr(),
                env.env,
                ctx.user,
                "DISCARD_SESSION",
                s"Admin discarded an Admin session",
                ctx.from,
                ctx.ua,
                Json.obj("sessionId" -> id)
              )
              Audit.send(event)
              Alerts
                .send(SessionDiscardedAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, event, ctx.from, ctx.ua))
              Ok(Json.obj("done" -> true))
            }
          }
        }
      } recover {
        case _ => Ok(Json.obj("done" -> false))
      }
    }
  }

  def discardAllSessions() = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(SuperAdminOnly) {
      env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
        env.datastores.backOfficeUserDataStore.discardAllSessions() map { _ =>
          val event = BackOfficeEvent(
            env.snowflakeGenerator.nextIdStr(),
            env.env,
            ctx.user,
            "DISCARD_SESSIONS",
            s"Admin discarded Admin sessions",
            ctx.from,
            ctx.ua,
            Json.obj()
          )
          Audit.send(event)
          Alerts
            .send(SessionsDiscardedAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, event, ctx.from, ctx.ua))
          Ok(Json.obj("done" -> true))
        }
      } recover {
        case _ => Ok(Json.obj("done" -> false))
      }
    }
  }

  def privateAppsSessions() = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(TenantAdminOnly) {
      val paginationPage: Int = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
      val paginationPageSize: Int =
        ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(Int.MaxValue)
      val paginationPosition = (paginationPage - 1) * paginationPageSize
      env.datastores.privateAppsUserDataStore.findAll() map { sessions =>
        Ok(JsArray(sessions.filter(ctx.canUserRead).drop(paginationPosition).take(paginationPageSize).map(_.toJson)))
      }
    }
  }

  def discardPrivateAppsSession(id: String) = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(TenantAdminOnly) {
      env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
        env.datastores.privateAppsUserDataStore.findById(id).flatMap {
          case None => Results.NotFound(Json.obj("error" -> "Session not found")).future
          case Some(session) if !ctx.canUserWrite(session) => ApiActionContext.fforbidden
          case Some(_) => {
            env.datastores.privateAppsUserDataStore.delete(id) map { _ =>
              val event = BackOfficeEvent(
                env.snowflakeGenerator.nextIdStr(),
                env.env,
                ctx.user,
                "DISCARD_PRIVATE_APPS_SESSION",
                s"Admin discarded a private app session",
                ctx.from,
                ctx.ua,
                Json.obj("sessionId" -> id)
              )
              Audit.send(event)
              Alerts
                .send(SessionDiscardedAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, event, ctx.from, ctx.ua))
              Ok(Json.obj("done" -> true))
            }
          } recover {
            case _ => Ok(Json.obj("done" -> false))
          }
        }
      }
    }
  }

  def discardAllPrivateAppsSessions() = BackOfficeActionAuth.async { ctx =>
    ctx.checkRights(SuperAdminOnly) {
      env.datastores.globalConfigDataStore.singleton().filter(!_.apiReadOnly).flatMap { _ =>
        env.datastores.privateAppsUserDataStore.deleteAll() map { _ =>
          val event = BackOfficeEvent(
            env.snowflakeGenerator.nextIdStr(),
            env.env,
            ctx.user,
            "DISCARD_PRIVATE_APPS_SESSIONS",
            s"Admin discarded private apps sessions",
            ctx.from,
            ctx.ua,
            Json.obj()
          )
          Audit.send(event)
          Alerts
            .send(SessionsDiscardedAlert(env.snowflakeGenerator.nextIdStr(), env.env, ctx.user, event, ctx.from, ctx.ua))
          Ok(Json.obj("done" -> true))
        }
      } recover {
        case _ => Ok(Json.obj("done" -> false))
      }
    }
  }*/

  def auditEvents() =
    BackOfficeActionAuth.async { ctx =>
      ctx.checkRights(SuperAdminOnly) {
        val paginationPage: Int     = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
        val paginationPageSize: Int =
          ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(Int.MaxValue)
        val paginationPosition      = (paginationPage - 1) * paginationPageSize
        env.datastores.auditDataStore.findAllRaw().map { elems =>
          val filtered = elems.drop(paginationPosition).take(paginationPageSize)
          Ok.chunked(
            Source
              .single(ByteString("["))
              .concat(
                Source
                  .apply(scala.collection.immutable.Iterable.empty[ByteString] ++ filtered)
                  .intersperse(ByteString(","))
              )
              .concat(Source.single(ByteString("]")))
          ).as("application/json")
        }
      }
    }

  def alertEvents() =
    BackOfficeActionAuth.async { ctx =>
      ctx.checkRights(SuperAdminOnly) {
        val paginationPage: Int     = ctx.request.queryString.get("page").flatMap(_.headOption).map(_.toInt).getOrElse(1)
        val paginationPageSize: Int =
          ctx.request.queryString.get("pageSize").flatMap(_.headOption).map(_.toInt).getOrElse(Int.MaxValue)
        val paginationPosition      = (paginationPage - 1) * paginationPageSize
        env.datastores.alertDataStore.findAllRaw().map { elems =>
          val filtered = elems.drop(paginationPosition).take(paginationPageSize)
          Ok.chunked(
            Source
              .single(ByteString("["))
              .concat(
                Source
                  .apply(scala.collection.immutable.Iterable.empty[ByteString] ++ filtered)
                  .intersperse(ByteString(","))
              )
              .concat(Source.single(ByteString("]")))
          ).as("application/json")
        }
      }
    }

  def selfSignedCert(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { body =>
        Try {
          Json.parse(body.utf8String).\("host").asOpt[String] match {
            case Some(host) => {
              env.datastores.certificatesDataStore.findById(Cert.OtoroshiIntermediateCA).map {
                case None     => {
                  val cert =
                    FakeKeyStore.createSelfSignedCertificate(host, FiniteDuration(365, TimeUnit.DAYS), None, None)
                  val c    = Cert(cert.cert, cert.keyPair, None, false)
                  val cc   = c.enrich()
                  Ok(cc.toJson)
                }
                case Some(ca) => {
                  val cert = FakeKeyStore.createCertificateFromCA(
                    host,
                    FiniteDuration(365, TimeUnit.DAYS),
                    None,
                    None,
                    ca.certificate.get,
                    ca.certificates.tail,
                    ca.cryptoKeyPair
                  )
                  val c    = Cert(cert.cert, cert.keyPair, ca, false)
                  val cc   = c.enrich()
                  Ok(cc.toJson)
                }
              }
            }
            case None       => FastFuture.successful(BadRequest(Json.obj("error" -> s"No host provided")))
          }
        } recover { case e =>
          e.printStackTrace()
          FastFuture.successful(BadRequest(Json.obj("error" -> s"Bad certificate : $e")))
        } get
      }
    }

  def selfSignedClientCert(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { body =>
        Try {
          Json.parse(body.utf8String).\("dn").asOpt[String] match {
            case Some(dn) => {
              env.datastores.certificatesDataStore.findById(Cert.OtoroshiIntermediateCA).map {
                case None     => {
                  val cert =
                    FakeKeyStore.createSelfSignedClientCertificate(dn, FiniteDuration(365, TimeUnit.DAYS), None, None)
                  val c    = Cert(cert.cert, cert.keyPair, None, true)
                  val cc   = c.enrich()
                  Ok(cc.toJson)
                }
                case Some(ca) => {
                  val cert = FakeKeyStore.createClientCertificateFromCA(
                    dn,
                    FiniteDuration(365, TimeUnit.DAYS),
                    None,
                    None,
                    ca.certificate.get,
                    ca.certificates.tail,
                    ca.cryptoKeyPair
                  )
                  val c    = Cert(cert.cert, cert.keyPair, ca, true)
                  val cc   = c.enrich()
                  Ok(cc.toJson)
                }
              }
            }
            case None     => FastFuture.successful(BadRequest(Json.obj("error" -> s"No cn provided")))
          }
        } recover { case e =>
          e.printStackTrace()
          FastFuture.successful(BadRequest(Json.obj("error" -> s"Bad certificate : $e")))
        } get
      }
    }

  def importP12File(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      val password = ctx.request.getQueryString("password").getOrElse("")
      val client   = ctx.request.getQueryString("client").contains("true")
      val many     = ctx.request.getQueryString("many").contains("true")
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { body =>
        Try {
          val certs = P12Helper.extractCertificate(body, password, client)
          if (!many) {
            val cert = certs.head
            Ok(cert.enrich().copy(client = client).toJson).future
          } else {
            Source(certs.toList)
              .mapAsync(1) { cert =>
                val c = cert.enrich().copy(client = client)
                c.save().map(_ => c)
              }
              .runWith(Sink.seq)
              .map { seq =>
                Ok(JsArray(seq.map(_.json)))
              }
          }
        } recover { case e =>
          e.printStackTrace()
          FastFuture.successful(BadRequest(Json.obj("error" -> s"Bad p12 : $e")))
        } get
      }
    }

  import otoroshi.ssl.SSLImplicits._

  def caCert(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).map { body =>
        Try {
          Json.parse(body.utf8String).\("cn").asOpt[String] match {
            case Some(cn) => {
              // val keyPairGenerator = KeyPairGenerator.getInstance(KeystoreSettings.KeyPairAlgorithmName)
              // keyPairGenerator.initialize(KeystoreSettings.KeyPairKeyLength)
              // val keyPair = keyPairGenerator.generateKeyPair()
              val ca    = FakeKeyStore.createCA(s"CN=$cn", FiniteDuration(365, TimeUnit.DAYS), None, None)
              val _cert = Cert(
                id = IdGenerator.token(32),
                name = "none",
                description = "none",
                chain = ca.cert.asPem,
                privateKey = ca.key.asPem,
                caRef = None,
                autoRenew = false,
                client = false,
                exposed = false,
                revoked = false
              ).enrich()
              val cert  = _cert.copy(name = _cert.domain, description = s"Certificate for ${_cert.subject}")
              Ok(cert.toJson)
            }
            case None     => BadRequest(Json.obj("error" -> s"No host provided"))
          }
        } recover { case e =>
          e.printStackTrace()
          BadRequest(Json.obj("error" -> s"Bad certificate : $e"))
        } get
      }
    }

  def caSignedCert(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { body =>
        Try {
          (
            Json.parse(body.utf8String).\("id").asOpt[String],
            Json.parse(body.utf8String).\("host").asOpt[String]
          ) match {
            case (Some(id), Some(host)) => {
              env.datastores.certificatesDataStore.findById(id).map {
                case None     => NotFound(Json.obj("error" -> s"No CA found"))
                case Some(ca) => {
                  // val keyPairGenerator = KeyPairGenerator.getInstance(KeystoreSettings.KeyPairAlgorithmName)
                  // keyPairGenerator.initialize(KeystoreSettings.KeyPairKeyLength)
                  // val keyPair = keyPairGenerator.generateKeyPair()
                  val cert = FakeKeyStore.createCertificateFromCA(
                    host,
                    FiniteDuration(365, TimeUnit.DAYS),
                    None,
                    None,
                    ca.certificate.get,
                    ca.certificates.tail,
                    ca.cryptoKeyPair
                  )
                  Ok(Cert(cert.cert, cert.keyPair, ca, false).enrich().toJson)
                }
              }
            }
            case _                      => FastFuture.successful(BadRequest(Json.obj("error" -> s"No host provided")))
          }
        } recover { case e =>
          e.printStackTrace()
          FastFuture.successful(BadRequest(Json.obj("error" -> s"Bad certificate : $e")))
        } get
      }
    }

  def caSignedClientCert(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { body =>
        Try {
          (Json.parse(body.utf8String).\("id").asOpt[String], Json.parse(body.utf8String).\("dn").asOpt[String]) match {
            case (Some(id), Some(dn)) => {
              env.datastores.certificatesDataStore.findById(id).map {
                case None     => NotFound(Json.obj("error" -> s"No CA found"))
                case Some(ca) => {
                  val cert = FakeKeyStore.createClientCertificateFromCA(
                    dn,
                    FiniteDuration(365, TimeUnit.DAYS),
                    None,
                    None,
                    ca.certificate.get,
                    ca.certificates.tail,
                    ca.cryptoKeyPair
                  )
                  Ok(Cert(cert.cert, cert.keyPair, ca, true).enrich().toJson)
                }
              }
            }
            case _                    => FastFuture.successful(BadRequest(Json.obj("error" -> s"No host provided")))
          }
        } recover { case e =>
          e.printStackTrace()
          FastFuture.successful(BadRequest(Json.obj("error" -> s"Bad certificate : $e")))
        } get
      }
    }

  def renew(id: String) =
    BackOfficeActionAuth.async { ctx =>
      env.datastores.certificatesDataStore.findById(id).map(_.map(_.enrich())).flatMap {
        case None                                  => FastFuture.successful(NotFound(Json.obj("error" -> s"No Certificate found")))
        case Some(cert) if !ctx.canUserWrite(cert) => ApiActionContext.fforbidden
        case Some(cert)                            => cert.renew().map(c => Ok(c.toJson))
      }
    }

  def createLetsEncryptCertificate() =
    BackOfficeActionAuth.async(parse.json) { ctx =>
      (ctx.request.body \ "host").asOpt[String] match {
        case None         => FastFuture.successful(BadRequest(Json.obj("error" -> "no domain found in request")))
        case Some(domain) =>
          otoroshi.utils.letsencrypt.LetsEncryptHelper.createCertificate(domain).map {
            case Left(err)   => InternalServerError(Json.obj("error" -> err))
            case Right(cert) => Ok(cert.toJson)
          }
      }
    }

  def createCsr =
    BackOfficeActionAuth.async(parse.json) { ctx =>
      val issuerRef = (ctx.request.body \ "caRef").asOpt[String]
      GenCsrQuery.fromJson(ctx.request.body) match {
        case Left(err)    => BadRequest(Json.obj("error" -> err)).future
        case Right(query) => {
          env.datastores.certificatesDataStore.findAll().flatMap { certificates =>
            issuerRef.flatMap(ref => certificates.find(_.id == ref)) match {
              case None         => BadRequest(Json.obj("error" -> "no issuer defined")).future
              case Some(issuer) => {
                env.pki.genCsr(query, issuer.certificate).map {
                  case Left(err)  => BadRequest(Json.obj("error" -> err))
                  case Right(res) => Ok(res.json)
                }
              }
            }
          }
        }
      }
    }

  def createCertificate =
    BackOfficeActionAuth.async(parse.json) { ctx =>
      val issuerRef = (ctx.request.body \ "caRef").asOpt[String]
      val maybeHost = (ctx.request.body \ "host").asOpt[String]
      val client    = (ctx.request.body \ "client").asOpt[Boolean].getOrElse(false)

      def handle(r: Future[Either[String, GenCertResponse]]): Future[Result] = {
        r.map {
          case Left(err)  => BadRequest(Json.obj("error" -> err))
          case Right(res) => Ok(res.toCert.copy(client = client, autoRenew = true).toJson)
        }
      }

      env.datastores.certificatesDataStore.findAll().flatMap { certificates =>
        val issuer = issuerRef.flatMap(ref => certificates.find(_.id == ref))
        (ctx.request.body \ "letsEncrypt").asOpt[Boolean] match {
          case Some(true) =>
            maybeHost match {
              case None         => BadRequest(Json.obj("error" -> "No domain found !")).future
              case Some(domain) =>
                otoroshi.utils.letsencrypt.LetsEncryptHelper.createCertificate(domain).map {
                  case Left(err)   => InternalServerError(Json.obj("error" -> err))
                  case Right(cert) => Ok(cert.toJson)
                }
            }
          case _          => {
            GenCsrQuery
              .fromJson(ctx.request.body)
              .map(v => v.copy(duration = v.duration * (24 * 60 * 60 * 1000))) match {
              case Left(err)                                        => BadRequest(Json.obj("error" -> err)).future
              case Right(query) if query.ca && issuer.isEmpty       => handle(env.pki.genSelfSignedCA(query))
              case Right(query) if query.ca && issuer.isDefined     =>
                handle(
                  env.pki.genSubCA(
                    query,
                    issuer.get.certificate.get,
                    issuer.get.certificates.tail,
                    issuer.get.cryptoKeyPair.getPrivate()
                  )
                )
              case Right(query) if query.client && issuer.isEmpty   => handle(env.pki.genSelfSignedCert(query))
              case Right(query) if query.client && issuer.isDefined =>
                handle(
                  env.pki.genCert(
                    query,
                    issuer.get.certificate.get,
                    issuer.get.certificates.tail,
                    issuer.get.cryptoKeyPair.getPrivate()
                  )
                )
              case Right(query) if issuer.isEmpty                   => handle(env.pki.genSelfSignedCert(query))
              case Right(query) if issuer.isDefined                 =>
                handle(
                  env.pki.genCert(
                    query,
                    issuer.get.certificate.get,
                    issuer.get.certificates.tail,
                    issuer.get.cryptoKeyPair.getPrivate()
                  )
                )
              case _                                                => BadRequest(Json.obj("error" -> "bad state")).future
            }
          }
        }
      }
    }

  def certificateData(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).map { body =>
        Try {
          val parts: Seq[String] = body.utf8String.split("-----BEGIN CERTIFICATE-----").toSeq
          parts.tail.headOption.map { cert =>
            val content: String = cert.replace("-----END CERTIFICATE-----", "")
            Ok(CertificateData(content))
          } getOrElse {
            Try {
              val content: String =
                body.utf8String.replace("-----BEGIN CERTIFICATE-----", "").replace("-----END CERTIFICATE-----", "")
              Ok(CertificateData(content))
            } recover { case e =>
              // e.printStackTrace()
              Ok(Json.obj("error" -> s"Bad certificate : $e"))
            } get
          }
        } recover { case e =>
          // e.printStackTrace()
          Ok(Json.obj("error" -> s"Bad certificate : $e"))
        } get
      }
    }

  def certificateIsValid(): Action[Source[ByteString, _]] =
    BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
      ctx.request.body.runFold(ByteString.empty)(_ ++ _).map { body =>
        Try {
          Cert.fromJsonSafe(Json.parse(body.utf8String)) match {
            case JsSuccess(cert, _) => Ok(Json.obj("valid" -> cert.isValid))
            case JsError(e)         => BadRequest(Json.obj("error" -> s"Bad certificate : $e"))
          }
        } recover { case e =>
          e.printStackTrace()
          BadRequest(Json.obj("error" -> s"Bad certificate : $e"))
        } get
      }
    }

  def checkExistingLdapConnection(id: String) =
    BackOfficeActionAuth.async { ctx =>
      // env.datastores.authConfigsDataStore.findById(id).flatMap {
      env.proxyState.authModuleAsync(id).flatMap {
        case None                                                           => FastFuture.successful(NotFound(Json.obj("error" -> "auth. config. not found !")))
        case Some(module: LdapAuthModuleConfig) if !ctx.canUserRead(module) => ApiActionContext.fforbidden
        case Some(module: LdapAuthModuleConfig)                             => {
          module.checkConnection().map {
            case (works, error) if works => Ok(Json.obj("works" -> works))
            case (works, error)          => Ok(Json.obj("works" -> works, "error" -> error))
          }
        }
        case Some(_)                                                        => FastFuture.successful(BadRequest(Json.obj("error" -> "auth. config. not LDAP !")))
      }
    }

  def checkLdapConnection() =
    BackOfficeActionAuth.async(parse.json) { ctx =>
      if ((ctx.request.body \ "user").isDefined) {
        val username = (ctx.request.body \ "user" \ "username").as[String]
        val password = (ctx.request.body \ "user" \ "password").as[String]
        LdapAuthModuleConfig.fromJson((ctx.request.body \ "config").as[JsValue]) match {
          case Left(e)       => FastFuture.successful(BadRequest(Json.obj("error" -> "bad auth. module. config")))
          case Right(module) => {
            module.bindUser(username, password) match {
              case Left(err)   => FastFuture.successful(Ok(Json.obj("works" -> false, "error" -> err)))
              case Right(user) => FastFuture.successful(Ok(Json.obj("works" -> true, "user" -> user.asJson)))
            }
          }
        }
      } else {
        LdapAuthModuleConfig.fromJson(ctx.request.body) match {
          case Left(e)                                   => FastFuture.successful(BadRequest(Json.obj("error" -> "bad auth. module. config")))
          case Right(module) if !ctx.canUserRead(module) => ApiActionContext.fforbidden
          case Right(module)                             => {
            module.checkConnection().map {
              case (works, error) if works => Ok(Json.obj("works" -> works))
              case (works, error)          => Ok(Json.obj("works" -> works, "error" -> error))
            }
          }
        }
      }
    }

  def fetchGroupsAndServices() =
    BackOfficeActionAuth.async { ctx =>
      for {
        rgroups            <- env.proxyState.allServiceGroups().vfuture
        rservices          <- env.proxyState.allServices().vfuture
        rroutes            <- env.proxyState.allRoutes().vfuture
        rrouteCompositions <- env.proxyState.allNgServices().vfuture
      } yield {
        val groups            = rgroups
          .filter(ctx.canUserRead)
          .map(g => Json.obj("label" -> g.name, "value" -> s"group_${g.id}", "kind" -> "group"))
        val services          = rservices
          .filter(ctx.canUserRead)
          .map(g => Json.obj("label" -> g.name, "value" -> s"service_${g.id}", "kind" -> "service"))
        val routes            = rroutes
          .filter(ctx.canUserRead)
          .map(g => Json.obj("label" -> g.name, "value" -> s"route_${g.id}", "kind" -> "route"))
        val routeCompositions = rrouteCompositions
          .filter(ctx.canUserRead)
          .map(g => Json.obj("label" -> g.name, "value" -> s"route-composition_${g.id}", "kind" -> "route-composition"))
        Ok(JsArray(groups ++ services ++ routes ++ routeCompositions))
      }
    }

  def findServiceLike(serviceId: String): Future[Option[ServiceLike]] = {
    env.datastores.serviceDescriptorDataStore.findById(serviceId) flatMap {
      case Some(service) => ServiceLike.fromService(service).some.vfuture
      case None          =>
        env.datastores.routeDataStore.findById(serviceId) flatMap {
          case Some(service) => ServiceLike.fromRoute(service).some.vfuture
          case None          =>
            env.datastores.routeCompositionDataStore.findById(serviceId) map {
              case Some(service) => ServiceLike.fromRouteComposition(service).some
              case None          => None
            }
        }
    }
  }

  def fetchApikeysForGroupAndService(serviceId: String) =
    BackOfficeActionAuth.async { ctx =>
      findServiceLike(serviceId) flatMap {
        case None                                       => FastFuture.successful(NotFound(Json.obj("error" -> "service not found")))
        case Some(service) if !ctx.canUserRead(service) => ApiActionContext.fforbidden
        case Some(service)                              => {
          env.datastores.apiKeyDataStore.findAll().map { apikeys =>
            val filtered = apikeys
              .filter(ctx.canUserRead)
              .filter(_.authorizedOnServiceOrGroups(service.id, service.groups))
            Ok(JsArray(filtered.map(_.toJson)))
          }
        }
      }
    }

  def checkElasticsearchConnection() = BackOfficeActionAuth.async(parse.json) { ctx =>
    ElasticAnalyticsConfig.read(ctx.request.body) match {
      case None         => Ok(Json.obj("none" -> true)).future
      case Some(config) => {
        val read = new ElasticReadsAnalytics(config, env)
        for {
          version <- read.checkVersion()
          search  <- read.checkSearch()
        } yield {
          val versionJson = version match {
            case Left(err) => Json.obj("error" -> err)
            case Right(v)  => JsString(v)
          }
          val searchJson  = search match {
            case Left(err) => Json.obj("error" -> err)
            case Right(v)  => JsNumber(v)
          }
          Ok(
            Json.obj(
              "version" -> versionJson,
              "search"  -> searchJson
            )
          )
        }
      }
    }
  }

  def applyElasticsearchTemplate() = BackOfficeActionAuth.async(parse.json) { ctx =>
    ElasticAnalyticsConfig.read(ctx.request.body) match {
      case None         => Ok(Json.obj("error" -> "bad configuration")).future
      case Some(config) => {
        // val read = new ElasticReadsAnalytics(config, env)
        for {
          res <- ElasticUtils.applyTemplate(config, logger, env).map(_ => Right(())).recover { case t: Throwable =>
                   Left(t)
                 }
        } yield {
          res match {
            case Left(t)      => InternalServerError(Json.obj("error" -> t.getMessage))
            case Right(value) => Ok(Json.obj("done" -> true))
          }
        }
      }
    }
  }

  def elasticTemplate() = BackOfficeActionAuth.async(parse.json) { ctx =>
    ElasticAnalyticsConfig.read(ctx.request.body) match {
      case None         => Ok(Json.obj("error" -> "bad configuration")).future
      case Some(config) => {
        val index: String = config.index.getOrElse("otoroshi-events")
        for {
          version <- ElasticUtils.getElasticVersion(config, logger, env)
        } yield {
          val strTpl: String   = version match {
            case ElasticVersion.UnderSeven(_)      => ElasticTemplates.indexTemplate_v6
            case ElasticVersion.AboveSeven(_)      => ElasticTemplates.indexTemplate_v7
            case ElasticVersion.AboveSevenEight(_) => ElasticTemplates.indexTemplate_v7_8
            case ElasticVersion.AboveEight(_)      => ElasticTemplates.indexTemplate_v7_8
          }
          val template: String = if (config.indexSettings.clientSide) {
            strTpl
              .replace("$$$INDEX$$$", index)
              .replace("$$$SHARDS$$$", config.indexSettings.numberOfShards.toString)
              .replace("$$$REPLICAS$$$", config.indexSettings.numberOfReplicas.toString)
          } else {
            strTpl
              .replace("$$$INDEX$$$-*", index)
              .replace("$$$SHARDS$$$", config.indexSettings.numberOfShards.toString)
              .replace("$$$REPLICAS$$$", config.indexSettings.numberOfReplicas.toString)
          }
          Ok(Json.obj("template" -> template))
        }
      }
    }
  }

  def elasticVersion() = BackOfficeActionAuth.async(parse.json) { ctx =>
    ElasticAnalyticsConfig.read(ctx.request.body) match {
      case None         => Ok(Json.obj("error" -> "bad configuration")).future
      case Some(config) => {
        val index: String = config.index.getOrElse("otoroshi-events")
        for {
          version <- ElasticUtils.checkVersion(config, logger, env)
        } yield {
          version match {
            case Left(err) => InternalServerError(Json.obj("error" -> err))
            case Right(v)  => Ok(Json.obj("version" -> v))
          }
        }
      }
    }
  }

  def updateUiMode() = BackOfficeActionAuth.async(parse.json) { ctx =>
    implicit val reqh = ctx.request
    val mode          = ctx.request.body.select("mode").asOpt[String].getOrElse("dark")
    NoContent.addingToSession("ui-mode" -> mode).future
  }

  def graphqlProxy() = BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
    val url     = ctx.request.queryString.get("url").map(_.last).get
    val host    = Uri(url).authority.host.toString()
    val headers = (ctx.request.headers.toSimpleMap ++ Map("Host" -> host)).toSeq

    val builder = env.Ws
      .url(url)
      .withHttpHeaders(headers: _*)
      .withFollowRedirects(false)
      .withMethod(ctx.request.method)
      .withQueryStringParameters(ctx.request.queryString.toSeq.filterNot(_._1 == "url").map(t => (t._1, t._2.head)): _*)

    val builderWithBody = if (otoroshi.utils.body.BodyUtils.hasBody(ctx.request)) {
      builder.withBody(SourceBody(ctx.request.body))
    } else {
      builder
    }

    builderWithBody
      .execute()
      .fast
      .map { res =>
        Results
          .Status(res.status)(res.body)
          .withHeaders(res.headers.mapValues(_.last).toSeq.filterNot(_._1 == "Content-Type"): _*)
          .as(res.contentType)
      }
  }

  def routeEntries(routeId: String) = BackOfficeActionAuth.async { ctx =>
    env.datastores.routeDataStore.findById(routeId) flatMap {
      case None        => NotFound(Json.obj("error" -> "route not found")).future
      case Some(route) =>
        val isSecured = route.plugins.slots.exists(p => p.plugin.contains("ForceHttpsTraffic"))

        Ok(Json.obj("entries" -> route.frontend.domains.map(d => {
          Uri(s"http://${d.raw}")
            .withScheme(if (isSecured) "https" else "http")
            .withPort(if (isSecured) env.exposedHttpsPortInt else env.exposedHttpPortInt)
            .toString()
        }))).future
    }
  }

  def ports(routeId: String) = BackOfficeActionAuth.async { ctx =>
    env.datastores.routeDataStore.findById(routeId) flatMap {
      case None        => NotFound(Json.obj("error" -> "route not found")).future
      case Some(route) =>
        Ok(
          Json.obj(
            "https" -> env.exposedHttpsPortInt,
            "http"  -> env.exposedHttpPortInt
          )
        ).future
    }
  }

  def graphQLToJson() = BackOfficeActionAuth.async(parse.json) { ctx =>
    val schema = ctx.request.body.select("schema").asOpt[String].getOrElse("{}")

    sangria.parser.QueryParser.parse(schema) match {
      case scala.util.Failure(exception)   => BadRequest(Json.obj("error" -> exception.getMessage)).future
      case scala.util.Success(astDocument) =>
        val res = GraphQLFormats.astDocumentToJson(astDocument)

        Ok(Json.obj("types" -> res)).future
    }
  }

  def jsonToGraphqlSchema() = BackOfficeActionAuth.async(parse.json) { ctx =>
    import sangria.schema.Schema

    val types = ctx.request.body
      .select("types")
      .as[JsArray]
      .value
      .map(GraphQLFormats.objectTypeDefinitionFmt.reads)
      .flatMap {
        case JsSuccess(v, _) => Some(v)
        case JsError(_)      => None
      }

    val schema = ctx.request.body.select("schema").asOpt[String].getOrElse("{}")

    Try {
      sangria.parser.QueryParser.parse(schema) match {
        case scala.util.Failure(exception)   => BadRequest(Json.obj("error" -> exception.getMessage)).future
        case scala.util.Success(astDocument) =>
          val generatedSchema = Schema.buildFromAst(astDocument)
          val document        = generatedSchema.toAst

          val newDocument = document.copy(
            definitions = document.definitions.flatMap {
              case _: sangria.ast.TypeDefinition          => None
              case _: sangria.ast.InterfaceTypeDefinition => None
              case v                                      => Some(v)
            } ++ types
          )

          Ok(
            Json.obj(
              "schema" -> Schema.buildFromAst(newDocument).renderPretty
            )
          ).future
      }
    } recover { case e =>
      BadRequest(
        Json.obj(
          "error" -> e.getMessage
        )
      ).future
    } get
  }

  def toYaml = BackOfficeActionAuth(parse.json) { ctx =>
    Ok(Yaml.write(ctx.request.body)).as("application/yaml")
  }

  def wasmFiles() = BackOfficeActionAuth.async { ctx =>
    env.datastores.globalConfigDataStore
      .singleton()
      .flatMap { globalConfig =>
        globalConfig.wasmoSettings match {
          case Some(config) =>
            val (header, token) = ApikeyHelper.generate(config.settings)
            Try {
              env.MtlsWs
                .url(s"${config.settings.url}/plugins", config.tlsConfig)
                .withFollowRedirects(false)
                .withHttpHeaders(
                  header -> token,
                  "kind" -> config.settings.pluginsFilter.getOrElse("*")
                )
                .get()
                .map(res => {
                  if (res.status == 200) {
                    Ok(res.json)
                  } else {
                    BadRequest("Unable to join the wasmo server")
                  }
                })
                .recover { case e: Throwable =>
                  logger.error(e.getMessage)
                  Ok(Json.arr())
                }
            } match {
              case Failure(err) => Ok(Json.arr()).vfuture
              case Success(v)   => v
            }
          case _            =>
            BadRequest(
              Json.obj(
                "error" -> "Missing config in global configuration"
              )
            ).future
        }
      }
  }

  def getWasmFilesFromBodyConfiguration() = BackOfficeActionAuth.async(parse.json) { ctx =>
    val jsonBody = ctx.request.body

    val wasmoConfiguration = TlsWasmoSettings.format.reads(jsonBody).get
    val (header, token)    = ApikeyHelper.generate(wasmoConfiguration.settings)

    Try {
      env.MtlsWs
        .url(s"${wasmoConfiguration.settings.url}/plugins", wasmoConfiguration.tlsConfig)
        .withFollowRedirects(false)
        .withHttpHeaders(
          header -> token,
          "kind" -> wasmoConfiguration.settings.pluginsFilter.getOrElse("*")
        )
        .get()
        .map(res => {
          if (res.status == 200) {
            Ok(res.json)
          } else {
            BadRequest(Json.obj("error" -> "Unable to join the wasmo server"))
          }
        })
        .recover { case e: Throwable =>
          logger.error(e.getMessage)
          BadRequest(Json.obj("error" -> "Unable to join the wasmo server"))
        }
    } match {
      case Failure(err) => BadRequest(Json.obj("error" -> "Unable to join the wasmo server")).vfuture
      case Success(v)   => v
    }
  }

  def anonymousReporting() = BackOfficeActionAuth.async(parse.json) { ctx =>
    val enabled = ctx.request.body.select("enabled").asOpt[Boolean].getOrElse(false)
    if (enabled) {
      env.datastores.globalConfigDataStore
        .set(env.datastores.globalConfigDataStore.latest().copy(anonymousReporting = true))
        .map { _ =>
          NoContent
        }
    } else {
      env.datastores.rawDataStore
        .set(
          s"${env.storageRoot}:backoffice:anonymous-reporting-refused",
          DateTime.now().toString().byteString,
          Some(30.days.toMillis)
        )
        .map { _ =>
          NoContent
        }
    }
  }

  def testFilteringAndProjection() = BackOfficeActionAuth(parse.json) { ctx =>
    val body                                   = ctx.request.body
    val input                                  = body.select("input").asOpt[JsValue].getOrElse(Json.obj())
    val matchExpressions: JsObject             = body.select("match").asOpt[JsObject].getOrElse(Json.obj())
    val matchIncludeExpressions: Seq[JsObject] =
      matchExpressions.select("include").asOpt[Seq[JsObject]].getOrElse(Seq.empty[JsObject])
    val matchExcludeExpressions: Seq[JsObject] =
      matchExpressions.select("exclude").asOpt[Seq[JsObject]].getOrElse(Seq.empty[JsObject])
    val projectionExpression: JsObject         = body.select("projection").asOpt[JsObject].getOrElse(Json.obj())

    val shouldInclude =
      if (matchIncludeExpressions.isEmpty) true
      else matchIncludeExpressions.forall(expr => otoroshi.utils.Match.matches(input, expr))
    val shouldExclude =
      if (matchExcludeExpressions.isEmpty) false
      else matchExcludeExpressions.forall(expr => otoroshi.utils.Match.matches(input, expr))

    val matches = shouldInclude && !shouldExclude

    val projected = otoroshi.utils.Projection.project(input, projectionExpression, identity)

    Ok(Json.obj("matches" -> matches, "projection" -> projected))
  }

  def testFilteringAndProjectionInputDoc() = BackOfficeActionAuth { ctx =>
    val rawRequest = ctx.request
    val route      = NgRoute.empty
    val target     = NgTarget.default
    Ok(
      GatewayEvent(
        `@id` = env.snowflakeGenerator.nextIdStr(),
        reqId = env.snowflakeGenerator.nextIdStr(),
        parentReqId = None,
        `@timestamp` = DateTime.now(),
        `@calledAt` = DateTime.now(),
        protocol = ctx.request.theProtocol,
        to = Location(
          scheme = rawRequest.theProtocol,
          host = rawRequest.theHost,
          uri = rawRequest.relativeUri
        ),
        target = Location(
          scheme = target.toTarget.scheme,
          host = target.toTarget.host,
          uri = rawRequest.relativeUri
        ),
        backendDuration = 20L,
        duration = 30L,
        overhead = 10L,
        cbDuration = 0L,
        overheadWoCb = 10L,
        callAttempts = 1,
        url = rawRequest.theUrl,
        method = rawRequest.method,
        from = rawRequest.theIpAddress,
        env = "prod",
        data = DataInOut(
          dataIn = 0L,
          dataOut = 128L
        ),
        status = 200,
        headers = rawRequest.headers.toSimpleMap.toSeq.map(Header.apply),
        headersOut = Seq.empty,
        otoroshiHeadersIn = rawRequest.headers.toSimpleMap.toSeq.map(Header.apply),
        otoroshiHeadersOut = Seq.empty,
        extraInfos = None,
        identity = Identity(
          identityType = "APIKEY",
          identity = "client_id",
          label = "client",
          tags = Seq.empty,
          metadata = Map.empty
        ).some,
        responseChunked = false,
        `@serviceId` = s"route_${IdGenerator.uuid}",
        `@service` = route.name,
        descriptor = Some(route.legacy),
        route = Some(route),
        `@product` = route.metadata.getOrElse("product", "--"),
        remainingQuotas = RemainingQuotas(),
        viz = None,
        clientCertChain = rawRequest.clientCertChainPem,
        err = false,
        gwError = None,
        userAgentInfo = None,
        geolocationInfo = None,
        extraAnalyticsData = None
      ).toJson
    )
  }

  def getUserPreferences() = BackOfficeActionAuth.async { ctx =>
    env.datastores.adminPreferencesDatastore.getPreferencesOrSetDefault(ctx.user.email).map { prefs =>
      Ok(prefs.json)
    }
  }

  def getUserPreference(id: String) = BackOfficeActionAuth.async { ctx =>
    env.datastores.adminPreferencesDatastore.getPreference(ctx.user.email, id) map {
      case None       => NotFound(Json.obj("error" -> "preference not found"))
      case Some(pref) => Ok(pref)
    }
  }

  def setUserPreferences() = BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
    ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
      AdminPreferences.format.reads(bodyRaw.utf8String.parseJson) match {
        case JsError(err)        => BadRequest(Json.obj("error" -> "bad_request")).vfuture
        case JsSuccess(prefs, _) => {
          env.datastores.adminPreferencesDatastore.setPreferences(ctx.user.email, prefs).map { prefs =>
            Ok(prefs.json)
          }
        }
      }
    }
  }

  def setUserPreference(id: String) = BackOfficeActionAuth.async(sourceBodyParser) { ctx =>
    ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
      env.datastores.adminPreferencesDatastore.setPreference(ctx.user.email, id, bodyRaw.utf8String.parseJson).map {
        value =>
          Ok(value)
      }
    }
  }

  def clearUserPreferences() = BackOfficeActionAuth.async { ctx =>
    env.datastores.adminPreferencesDatastore.deletePreferences(ctx.user.email).map { _ =>
      Ok(Json.obj("done" -> true))
    }
  }

  def clearUserPreference(id: String) = BackOfficeActionAuth.async { ctx =>
    env.datastores.adminPreferencesDatastore.deletePreference(ctx.user.email, id).map { _ =>
      Ok(Json.obj("done" -> true))
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy