242 lines
8.5 KiB
Scala
242 lines
8.5 KiB
Scala
package controllers
|
|
|
|
import javax.inject._
|
|
import com.typesafe.config.ConfigFactory
|
|
import play.api.i18n.Messages
|
|
import play.api.libs.json._
|
|
import play.api.mvc._
|
|
import repository.DeciderRepository
|
|
import play.api.libs.ws.WSClient
|
|
|
|
import scala.collection.JavaConverters._
|
|
import scala.concurrent.{ExecutionContext, Future}
|
|
import scala.util.Try
|
|
|
|
/**
|
|
* Admin panel for managing deciders.
|
|
* Authentication via Steam OpenID.
|
|
*/
|
|
@Singleton
|
|
class AdminController @Inject()(
|
|
cc: ControllerComponents,
|
|
ws: WSClient
|
|
) extends AbstractController(cc) with SecuredActions {
|
|
|
|
implicit val ec: ExecutionContext = cc.executionContext
|
|
private val logger = org.slf4j.LoggerFactory.getLogger("controllers.AdminController")
|
|
|
|
// ---------- Steam OpenID ----------
|
|
|
|
private val SteamLoginUrl = "https://steamcommunity.com/openid/login"
|
|
|
|
// ---------- Public endpoints ----------
|
|
|
|
def login() = Action { implicit request =>
|
|
val realm = request.headers.get("Host").map(h => s"http://$h/").getOrElse("http://localhost:1337/")
|
|
val returnTo = controllers.routes.AdminController.steamCallback().absoluteURL()
|
|
|
|
val params = Map(
|
|
"openid.ns" -> "http://specs.openid.net/auth/2.0",
|
|
"openid.mode" -> "checkid_setup",
|
|
"openid.return_to" -> returnTo,
|
|
"openid.realm" -> realm,
|
|
"openid.identity" -> "http://specs.openid.net/auth/2.0/identifier_select",
|
|
"openid.claimed_id" -> "http://specs.openid.net/auth/2.0/identifier_select"
|
|
)
|
|
|
|
val query = params.map { case (k, v) => s"${java.net.URLEncoder.encode(k, "UTF-8")}=${java.net.URLEncoder.encode(v, "UTF-8")}" }.mkString("&")
|
|
Redirect(s"$SteamLoginUrl?$query")
|
|
}
|
|
|
|
def steamCallback() = Action.async { implicit request =>
|
|
// Steam присылает параметры в формате OpenID: openid.mode, openid.identity,
|
|
// openid.claimed_id. Play отдаёт ключи в queryString как есть, с точкой,
|
|
// поэтому нормализуем имена к виду openid_mode и т.д.
|
|
val openidParams = request.queryString.map { case (k, v) => k.replace('.', '_') -> v }
|
|
|
|
// Validate Steam OpenID response
|
|
val mode = openidParams.get("openid_mode").flatMap(_.headOption)
|
|
val identifier = openidParams.get("openid_identity").flatMap(_.headOption)
|
|
val assertion = openidParams.get("openid_claimed_id").flatMap(_.headOption)
|
|
|
|
(mode, identifier orElse assertion) match {
|
|
case (Some("id_res"), Some(steamId)) if isSteamId(steamId) =>
|
|
val steamId64 = parseSteamId64(steamId)
|
|
val config = ConfigFactory.load()
|
|
val allowed = Try(config.getStringList("admin.allowedSteamIds").asScala.toSet).getOrElse(Set.empty)
|
|
if (allowed.isEmpty || allowed.contains(steamId64)) {
|
|
Future.successful(Redirect("/decider/admin").withSession("steamId" -> steamId64))
|
|
} else {
|
|
Future.successful(Redirect("/decider/admin").flashing("error" -> "You are not authorized"))
|
|
}
|
|
|
|
case (Some("id_res"), _) =>
|
|
Future.successful(Redirect("/decider/admin").flashing("error" -> "Steam authentication failed"))
|
|
|
|
case _ =>
|
|
Future.successful(Redirect("/decider/admin").flashing("error" -> "Invalid OpenID response"))
|
|
}
|
|
}
|
|
|
|
private def isSteamId(id: String): Boolean =
|
|
id.startsWith("https://steamcommunity.com/openid/id/") ||
|
|
id.startsWith("http://steamcommunity.com/openid/id/")
|
|
|
|
private def parseSteamId64(id: String): String =
|
|
id.replaceAll("""(https?://steamcommunity\.com/openid/id/)""", "")
|
|
|
|
// ---------- Admin page ----------
|
|
|
|
def index() = Secured { implicit request =>
|
|
implicit val messages: Messages = messagesApi.preferred(request)
|
|
Ok(views.html.admin(DeciderRepository.all))
|
|
}
|
|
|
|
// ---------- API: list map images ----------
|
|
|
|
def mapImages() = Secured { implicit request =>
|
|
val images = repository.MapImageRepository.all.map { image =>
|
|
Json.obj(
|
|
"name" -> image.name,
|
|
"file" -> image.file,
|
|
"players" -> image.players
|
|
)
|
|
}
|
|
Ok(Json.toJson(images))
|
|
}
|
|
|
|
// ---------- API: upload map image ----------
|
|
|
|
def mapImageUpload() = Secured(parse.multipartFormData) { implicit request =>
|
|
request.body.file("file") match {
|
|
case Some(part) =>
|
|
repository.MapImageRepository.saveUploaded(part.filename, part.ref.path.toFile) match {
|
|
case Right(image) =>
|
|
logger.info(s"Admin ${SecuredActions.steamIdOrUnknown(request)} uploaded map image '${image.file}'")
|
|
Ok(Json.obj(
|
|
"ok" -> true,
|
|
"name" -> image.name,
|
|
"file" -> image.file,
|
|
"players" -> image.players
|
|
))
|
|
case Left(error) =>
|
|
BadRequest(Json.obj("error" -> error))
|
|
}
|
|
case None =>
|
|
BadRequest(Json.obj("error" -> "No file uploaded"))
|
|
}
|
|
}
|
|
|
|
// ---------- API: list all deciders ----------
|
|
|
|
def decidersList() = Secured { implicit request =>
|
|
val list = DeciderRepository.all.map(d => Json.obj(
|
|
"key" -> d.key,
|
|
"name" -> d.name,
|
|
"rules" -> d.rules,
|
|
"raceCount" -> d.raceCount,
|
|
"existAdditionalRaces" -> d.existAdditionalRaces,
|
|
"existTextFieldRaces" -> d.existTextFieldRaces,
|
|
"description" -> d.description,
|
|
"maps" -> d.maps.map(m => Json.obj("map" -> m.map, "description" -> m.description))
|
|
))
|
|
Ok(Json.toJson(list))
|
|
}
|
|
|
|
// ---------- API: get one decider ----------
|
|
|
|
def deciderGet(key: String) = Secured { implicit request =>
|
|
DeciderRepository.get(key) match {
|
|
case Some(d) =>
|
|
Ok(Json.obj(
|
|
"key" -> d.key,
|
|
"name" -> d.name,
|
|
"rules" -> d.rules,
|
|
"raceCount" -> d.raceCount,
|
|
"existAdditionalRaces" -> d.existAdditionalRaces,
|
|
"existTextFieldRaces" -> d.existTextFieldRaces,
|
|
"description" -> d.description,
|
|
"maps" -> d.maps.map(m => Json.obj("map" -> m.map, "description" -> m.description))
|
|
))
|
|
case None =>
|
|
NotFound(Json.obj("error" -> s"Decider $key not found"))
|
|
}
|
|
}
|
|
|
|
// ---------- API: save / update decider ----------
|
|
|
|
def deciderSave() = SecuredJson { implicit request =>
|
|
val json = request.body
|
|
|
|
(json \ "key").asOpt[String] match {
|
|
case None =>
|
|
BadRequest(Json.obj("error" -> "Missing key"))
|
|
|
|
case Some(key) =>
|
|
Try {
|
|
val maps = (json \ "maps").as[List[JsValue]].map { m =>
|
|
repository.DeciderMapConfig(
|
|
map = (m \ "map").as[String],
|
|
description = (m \ "description").asOpt[String]
|
|
)
|
|
}
|
|
repository.Decider(
|
|
key = key,
|
|
name = (json \ "name").as[String],
|
|
rules = (json \ "rules").as[String],
|
|
raceCount = (json \ "raceCount").as[Int],
|
|
existAdditionalRaces = (json \ "existAdditionalRaces").as[Boolean],
|
|
existTextFieldRaces = (json \ "existTextFieldRaces").as[Boolean],
|
|
description = (json \ "description").as[Boolean],
|
|
maps = maps
|
|
)
|
|
}.toEither match {
|
|
case Right(decider) =>
|
|
val existed = DeciderRepository.get(decider.key).isDefined
|
|
DeciderRepository.save(decider)
|
|
DeciderRepository.reload()
|
|
if (existed) {
|
|
logger.info(
|
|
s"Admin ${SecuredActions.steamIdOrUnknown(request)} updated decider '${decider.key}' " +
|
|
s"(name='${decider.name}', maps=${decider.maps.size})"
|
|
)
|
|
} else {
|
|
logger.info(
|
|
s"Admin ${SecuredActions.steamIdOrUnknown(request)} created decider '${decider.key}' " +
|
|
s"(name='${decider.name}', maps=${decider.maps.size})"
|
|
)
|
|
}
|
|
Ok(Json.obj("ok" -> true))
|
|
|
|
case Left(error) =>
|
|
BadRequest(Json.obj("error" -> s"$error"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- API: delete decider ----------
|
|
|
|
def deciderDelete(key: String) = SecuredApi { implicit request =>
|
|
if (DeciderRepository.delete(key)) {
|
|
DeciderRepository.reload()
|
|
logger.info(s"Admin ${SecuredActions.steamIdOrUnknown(request)} deleted decider '$key'")
|
|
Ok(Json.obj("ok" -> true))
|
|
} else {
|
|
NotFound(Json.obj("error" -> s"Decider $key not found"))
|
|
}
|
|
}
|
|
|
|
// ---------- Legacy endpoints (deprecated) ----------
|
|
|
|
@deprecated("unused", "1.0")
|
|
def viewAllUsers() = Action { implicit request =>
|
|
logger.info("viewAllUsers")
|
|
Ok("viewAllUsers")
|
|
}
|
|
|
|
@deprecated("unused", "1.0")
|
|
def reloadConfig() = Action { implicit request =>
|
|
Ok("Config reloaded")
|
|
}
|
|
}
|