Merge branch 'master' into benediktwerner-cg-resize

* master:
  /player/top/:nb/:perfKey only uses nb=200 on the website
  mention access tokens on account security page
  regroup change username
  log use of client authentication for #9199
  keep zen mode on when game ends
  tweak API endpoint
  add API endpoint to get multiple TV games of a channel
  add garbage char to the list
  create pager duty maintenance window on scheduled restart
This commit is contained in:
Thibault Duplessis 2021-06-17 08:25:24 +02:00
commit 0200e1bad5
21 changed files with 168 additions and 43 deletions

View file

@ -366,7 +366,10 @@ final class Account(
Auth { implicit ctx => me =>
env.security.api.dedup(me.id, ctx.req) >>
env.security.api.locatedOpenSessions(me.id, 50) map { sessions =>
Ok(html.account.security(me, sessions, currentSessionId))
Ok(
html.account
.security(me, sessions, currentSessionId, thirdPartyApps = true, personalAccessTokens = true)
)
}
}

View file

@ -123,11 +123,7 @@ final class Game(
.GlobalConcurrencyLimitPerIP(HTTPRequest ipAddress req)(
env.api.gameApiV2.exportByIds(config)
) { source =>
Ok.chunked(source)
.withHeaders(
noProxyBufferHeader
)
.as(gameContentType(config))
noProxyBuffer(Ok.chunked(source)).as(gameContentType(config))
}
.fuccess
}

View file

@ -10,7 +10,8 @@ import lila.game.Pov
final class Tv(
env: Env,
apiC: => Api
apiC: => Api,
gameC: => Game
) extends LilaController(env) {
def index = onChannel(lila.tv.Tv.Channel.Best.key)
@ -62,7 +63,7 @@ final class Tv(
def gamesChannel(chanKey: String) =
Open { implicit ctx =>
(lila.tv.Tv.Channel.byKey get chanKey) ?? { channel =>
lila.tv.Tv.Channel.byKey.get(chanKey) ?? { channel =>
env.tv.tv.getChampions zip env.tv.tv.getGames(channel, 15) map { case (champs, games) =>
NoCache {
Ok(html.tv.games(channel, games map Pov.naturalOrientation, champs))
@ -71,6 +72,22 @@ final class Tv(
}
}
def apiGamesChannel(chanKey: String) =
Action.async { req =>
lila.tv.Tv.Channel.byKey.get(chanKey) ?? { channel =>
env.tv.tv.getGameIds(channel, getInt("nb", req).fold(10)(_ atMost 30 atLeast 1)) map { gameIds =>
val config =
lila.api.GameApiV2.ByIdsConfig(
ids = gameIds,
format = lila.api.GameApiV2.Format byRequest req,
flags = gameC.requestPgnFlags(req, extended = false).copy(delayMoves = false),
perSecond = lila.common.config.MaxPerSecond(30)
)
noProxyBuffer(Ok.chunked(env.api.gameApiV2.exportByIds(config))).as(gameC.gameContentType(config))
}
}
}
def feed =
Action.async { req =>
import makeTimeout.short

View file

@ -315,7 +315,7 @@ final class User(
env.user.cached.top200Perf get perfType.id dmap { _ take (nb atLeast 1 atMost 200) } flatMap {
users =>
negotiate(
html = Ok(html.user.top(perfType, users)).fuccess,
html = (nb == 200) ?? Ok(html.user.top(perfType, users)).fuccess,
api = _ =>
fuccess {
implicit val lpWrites = OWrites[UserModel.LightPerf](env.user.jsonView.lightPerfIsOnline)

View file

@ -34,6 +34,9 @@ object layout {
a(activeCls("editProfile"), href := routes.Account.profile)(
trans.editProfile()
),
a(activeCls("username"), href := routes.Account.username)(
trans.changeUsername()
),
isGranted(_.Coach) option a(activeCls("coach"), href := routes.Coach.edit)(
trans.coach.lichessCoach()
),
@ -44,9 +47,6 @@ object layout {
a(activeCls("email"), href := routes.Account.email)(
trans.changeEmail()
),
a(activeCls("username"), href := routes.Account.username)(
trans.changeUsername()
),
a(activeCls("twofactor"), href := routes.Account.twoFactor)(
trans.tfa.twoFactorAuth()
),

View file

@ -10,26 +10,50 @@ import play.api.i18n.Lang
object security {
def apply(u: lila.user.User, sessions: List[lila.security.LocatedSession], curSessionId: String)(implicit
def apply(
u: lila.user.User,
sessions: List[lila.security.LocatedSession],
curSessionId: String,
thirdPartyApps: Boolean,
personalAccessTokens: Boolean
)(implicit
ctx: Context
) =
account.layout(title = s"${u.username} - ${trans.security.txt()}", active = "security") {
div(cls := "account security box")(
h1(trans.security()),
standardFlash(cls := "box__pad"),
div(cls := "box__pad")(
p(trans.thisIsAListOfDevicesThatHaveLoggedIntoYourAccount()),
sessions.sizeIs > 1 option div(
trans.alternativelyYouCanX {
postForm(cls := "revoke-all", action := routes.Account.signout("all"))(
submitButton(cls := "button button-empty button-red confirm")(
trans.revokeAllSessions()
div(cls := "account security")(
div(cls := "box")(
h1(trans.sessions()),
standardFlash(cls := "box__pad"),
div(cls := "box__pad")(
p(trans.thisIsAListOfDevicesThatHaveLoggedIntoYourAccount()),
sessions.sizeIs > 1 option div(
trans.alternativelyYouCanX {
postForm(cls := "revoke-all", action := routes.Account.signout("all"))(
submitButton(cls := "button button-empty button-red confirm")(
trans.revokeAllSessions()
)
)
)
}
}
)
),
table(sessions, curSessionId.some)
),
thirdPartyApps option div(cls := "account security box")(
h1("Third party apps"),
p(cls := "box__pad")(
"Revoke access of any ",
a(href := routes.OAuthApp.index)("third party apps"),
" that you do not trust."
)
),
table(sessions, curSessionId.some)
personalAccessTokens option div(cls := "account security box")(
h1("Personal access tokens"),
p(cls := "box__pad")(
"Revoke any ",
a(href := routes.OAuthToken.index)("personal access tokens"),
" that you do not recognize."
)
)
)
}

View file

@ -96,6 +96,10 @@ accessibility {
}
}
}
pagerDuty {
serviceId = ""
apiKey = ""
}
prismic {
api_url = "https://lichess.cdn.prismic.io/api"
}

View file

@ -31,6 +31,7 @@ GET /games controllers.Tv.games
GET /games/:chanKey controllers.Tv.gamesChannel(chanKey: String)
GET /api/tv/channels controllers.Tv.channels
GET /api/tv/feed controllers.Tv.feed
GET /api/tv/:chanKey controllers.Tv.apiGamesChannel(chanKey: String)
# Relation
POST /rel/follow/:userId controllers.Relation.follow(userId: String)

View file

@ -12,7 +12,8 @@ final class ApiConfig(
val prismicApiUrl: String,
val explorerEndpoint: String,
val tablebaseEndpoint: String,
val accessibility: ApiConfig.Accessibility
val accessibility: ApiConfig.Accessibility,
val pagerDuty: ApiConfig.PagerDuty
)
object ApiConfig {
@ -28,6 +29,8 @@ object ApiConfig {
}
}
final class PagerDuty(val serviceId: String, val apiKey: Secret)
def loadFrom(c: play.api.Configuration) =
new ApiConfig(
c.get[Secret]("api.token"),
@ -40,6 +43,10 @@ object ApiConfig {
new Accessibility(
c.get[String]("accessibility.blind.cookie.name"),
c.get[Secret]("accessibility.blind.cookie.salt")
),
new PagerDuty(
c.get[String]("pagerDuty.serviceId"),
c.get[Secret]("pagerDuty.apiKey")
)
)
}

View file

@ -6,10 +6,11 @@ import play.api.libs.ws.StandaloneWSClient
import play.api.{ Configuration, Mode }
import scala.concurrent.duration._
import lila.common.config._
import lila.common.Bus
import lila.user.User
import lila.chat.GetLinkCheck
import lila.common.Bus
import lila.common.config._
import lila.hub.actorApi.Announce
import lila.user.User
@Module
final class Env(
@ -58,7 +59,7 @@ final class Env(
) {
val config = ApiConfig loadFrom appConfig
import config.apiToken
import config.{ apiToken, pagerDuty => pagerDutyConfig }
import net.{ baseUrl, domain }
lazy val pgnDump: PgnDump = wire[PgnDump]
@ -94,8 +95,11 @@ final class Env(
private lazy val linkCheck = wire[LinkCheck]
Bus.subscribeFun("chatLinkCheck") { case GetLinkCheck(line, source, promise) =>
promise completeWith linkCheck(line, source)
private lazy val pagerDuty = wire[PagerDuty]
Bus.subscribeFun("chatLinkCheck", "announce") {
case GetLinkCheck(line, source, promise) => promise completeWith linkCheck(line, source)
case Announce(msg, date, _) if msg contains "will restart" => pagerDuty.lilaRestart(date).unit
}
system.scheduler.scheduleWithFixedDelay(1 minute, 1 minute) { () =>

View file

@ -371,7 +371,7 @@ object GameApiV2 {
format: Format,
flags: WithFlags,
perSecond: MaxPerSecond,
playerFile: Option[String]
playerFile: Option[String] = None
) extends Config
case class ByTournamentConfig(

View file

@ -0,0 +1,53 @@
package lila.api
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
import play.api.libs.json.Json
import play.api.libs.ws.JsonBodyWritables._
import play.api.libs.ws.StandaloneWSClient
import lila.hub.actorApi.Announce
final private class PagerDuty(ws: StandaloneWSClient, config: ApiConfig.PagerDuty)(implicit
ec: scala.concurrent.ExecutionContext
) {
def lilaRestart(date: DateTime): Funit =
(config.serviceId.nonEmpty && config.apiKey.value.nonEmpty) ??
ws.url("https://api.pagerduty.com/maintenance_windows")
.withHttpHeaders(
"Authorization" -> s"Token token=${config.apiKey.value}",
"Content-type" -> "application/json",
"Accept" -> "application/vnd.pagerduty+json;version=2"
)
.post(
Json
.obj(
"maintenance_window" -> Json.obj(
"type" -> "maintenance_window",
"start_time" -> formatDate(date),
"end_time" -> formatDate(date.plusMinutes(3)),
"description" -> "restart announce",
"services" -> Json.arr(
Json.obj(
"id" -> config.serviceId,
"type" -> "service_reference"
)
)
)
)
)
.addEffects(
err => logger.error("lilaRestart failed", err),
res =>
if (res.status != 201) {
println(res.body)
logger.warn(s"lilaRestart status=${res.status}")
}
)
.void
private lazy val logger = lila.log("pagerDuty")
private def formatDate(date: DateTime) = ISODateTimeFormat.dateTime print date
}

View file

@ -90,6 +90,7 @@ public class StringUtils {
case '\u200f':
case '\u202e':
case '\u1160':
case '\u3164':
break;
default:
sb.append(c);

View file

@ -554,6 +554,7 @@ val `disableKidMode` = new I18nKey("disableKidMode")
val `security` = new I18nKey("security")
val `thisIsAListOfDevicesThatHaveLoggedIntoYourAccount` = new I18nKey("thisIsAListOfDevicesThatHaveLoggedIntoYourAccount")
val `alternativelyYouCanX` = new I18nKey("alternativelyYouCanX")
val `sessions` = new I18nKey("sessions")
val `revokeAllSessions` = new I18nKey("revokeAllSessions")
val `playChessEverywhere` = new I18nKey("playChessEverywhere")
val `asFreeAsLichess` = new I18nKey("asFreeAsLichess")
@ -711,6 +712,8 @@ val `clickToRevealEmailAddress` = new I18nKey("clickToRevealEmailAddress")
val `download` = new I18nKey("download")
val `welcome` = new I18nKey("welcome")
val `lichessPatronInfo` = new I18nKey("lichessPatronInfo")
val `coachManager` = new I18nKey("coachManager")
val `streamerManager` = new I18nKey("streamerManager")
val `opponentLeftCounter` = new I18nKey("opponentLeftCounter")
val `mateInXHalfMoves` = new I18nKey("mateInXHalfMoves")
val `nextCaptureOrPawnMoveInXHalfMoves` = new I18nKey("nextCaptureOrPawnMoveInXHalfMoves")
@ -751,8 +754,6 @@ val `availableInNbLanguages` = new I18nKey("availableInNbLanguages")
val `nbSecondsToPlayTheFirstMove` = new I18nKey("nbSecondsToPlayTheFirstMove")
val `nbSeconds` = new I18nKey("nbSeconds")
val `andSaveNbPremoveLines` = new I18nKey("andSaveNbPremoveLines")
val `coachManager` = new I18nKey("coachManager")
val `streamerManager` = new I18nKey("streamerManager")
object arena {
val `arenaTournaments` = new I18nKey("arena:arenaTournaments")

View file

@ -135,8 +135,11 @@ final class TeamApi(
} else motivateOrJoin(team, me, request)
def joinApi(team: Team, me: User, oAuthAppOwner: Option[User.ID], msg: Option[String]): Fu[Requesting] =
if (team.open || oAuthAppOwner.contains(team.createdBy)) doJoin(team, me) inject Requesting.Joined
else motivateOrJoin(team, me, msg)
if (team.open) doJoin(team, me) inject Requesting.Joined
else if (oAuthAppOwner.contains(team.createdBy)) {
lila.log("auth").info(s"${me.id} joined restricted team of oauth app owner ${team.createdBy}")
doJoin(team, me) inject Requesting.Joined
} else motivateOrJoin(team, me, msg)
private def motivateOrJoin(team: Team, me: User, msg: Option[String]) =
msg.fold(fuccess[Requesting](Requesting.NeedRequest)) { txt =>

View file

@ -35,10 +35,13 @@ final class Tv(
}
def getGames(channel: Tv.Channel, max: Int): Fu[List[Game]] =
trouper.ask[List[Game.ID]](TvTrouper.GetGameIds(channel, max, _)) flatMap {
getGameIds(channel, max) flatMap {
_.map(roundProxyGame).sequenceFu.map(_.flatten)
}
def getGameIds(channel: Tv.Channel, max: Int): Fu[List[Game.ID]] =
trouper.ask[List[Game.ID]](TvTrouper.GetGameIds(channel, max, _))
def getBestGame = getGame(Tv.Channel.Best) orElse gameRepo.random
def getBestAndHistory = getGameAndHistory(Tv.Channel.Best)

View file

@ -695,6 +695,7 @@ computer analysis, game chat and shareable URL.</string>
<string name="security">Security</string>
<string name="thisIsAListOfDevicesThatHaveLoggedIntoYourAccount">This is a list of devices that have logged into your account. Revoke any sessions that you do not recognise.</string>
<string name="alternativelyYouCanX">Alternatively you can %s.</string>
<string name="sessions">Sessions</string>
<string name="revokeAllSessions">revoke all sessions</string>
<string name="playChessEverywhere">Play chess everywhere</string>
<string name="asFreeAsLichess">As free as Lichess</string>

View file

@ -6,7 +6,7 @@
display: none;
border-bottom-right-radius: $box-radius-size;
body.playing.zen & {
body.zen & {
display: block;
}

View file

@ -1,10 +1,10 @@
%zen {
body.playing.zen & {
body.zen & {
display: none;
}
}
body.playing.zen {
body.zen {
.ricons {
margin: 0.5em 0 1em 0;
}

View file

@ -142,7 +142,7 @@ export default class RoundController {
});
lichess.pubsub.on('zen', () => {
if (this.isPlaying()) {
if (!this.data.player.spectator) {
const zen = $('body').toggleClass('zen').hasClass('zen');
window.dispatchEvent(new Event('resize'));
xhr.setZen(zen);

View file

@ -26,6 +26,13 @@
.slist {
@extend %break-word;
}
.box {
margin-bottom: 4em;
> p {
padding-bottom: 2em;
}
}
}
.twofactor ol li {