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") } }