From 9c34d433f9979513066e9af547d1e2be02738cd5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 8 Apr 2021 16:33:41 +0200 Subject: [PATCH] replace recaptcha with hcaptcha - closes #3530 --- COPYING.md | 2 +- app/controllers/Account.scala | 6 +-- app/controllers/Auth.scala | 13 ++--- app/views/account/reopen.scala | 11 ++-- app/views/auth/bits.scala | 36 ++++++------- app/views/auth/signup.scala | 19 +++---- app/views/base/hcaptcha.scala | 18 +++++++ app/views/base/recaptcha.scala | 29 ----------- conf/base.conf | 8 +-- .../src/main/ContentSecurityPolicy.scala | 10 ++-- modules/common/src/main/mon.scala | 4 +- modules/security/src/main/Env.scala | 12 ++--- .../main/{Recaptcha.scala => Hcaptcha.scala} | 52 +++++++++---------- .../security/src/main/SecurityConfig.scala | 2 +- modules/security/src/main/SecurityForm.scala | 22 ++++---- modules/security/src/main/Signup.scala | 12 ++--- modules/security/src/main/model.scala | 6 +-- ui/site/src/login.ts | 17 ++++-- 18 files changed, 136 insertions(+), 143 deletions(-) create mode 100644 app/views/base/hcaptcha.scala delete mode 100644 app/views/base/recaptcha.scala rename modules/security/src/main/{Recaptcha.scala => Hcaptcha.scala} (64%) diff --git a/COPYING.md b/COPYING.md index ae60b01df9..a4dc27d472 100644 --- a/COPYING.md +++ b/COPYING.md @@ -104,6 +104,6 @@ Lichess as deployed on https://lichess.org/ also uses these external services: - [detectlanguage.com](https://detectlanguage.com/) - Fallback to [Google Fonts](https://fonts.google.com/) - [Google Cloud Messaging](https://developers.google.com/cloud-messaging/) for mobile notifications -- [reCAPTCHA](https://www.google.com/recaptcha/) +- [hCaptcha](https://hcaptcha.com) - [PeerJS](https://peerjs.com/) for voice chat - [crowdin](https://crowdin.com/project/lichess) for localization diff --git a/app/controllers/Account.scala b/app/controllers/Account.scala index 70c31e9e72..c92562a629 100644 --- a/app/controllers/Account.scala +++ b/app/controllers/Account.scala @@ -391,8 +391,8 @@ final class Account( def reopenApply = OpenBody { implicit ctx => implicit val req = ctx.body - env.security.recaptcha.verify() flatMap { - _.ok ?? { + env.security.hcaptcha.verify() flatMap { captcha => + if (captcha.ok) env.security.forms.reopen.form .bindFromRequest() .fold( @@ -412,7 +412,7 @@ final class Account( }(rateLimitedFu) } ) - } + else BadRequest(renderReopen(none, none)).fuccess } } diff --git a/app/controllers/Auth.scala b/app/controllers/Auth.scala index 169d8676b5..a98ca6bd16 100644 --- a/app/controllers/Auth.scala +++ b/app/controllers/Auth.scala @@ -335,8 +335,8 @@ final class Auth( def passwordResetApply = OpenBody { implicit ctx => implicit val req = ctx.body - env.security.recaptcha.verify() flatMap { - _.ok ?? { + env.security.hcaptcha.verify() flatMap { captcha => + if (captcha.ok) forms.passwordReset.form .bindFromRequest() .fold( @@ -353,7 +353,7 @@ final class Auth( Redirect(routes.Auth.passwordResetSent(data.realEmail.conceal)).fuccess } ) - } + else BadRequest(renderPasswordReset(none, fail = true)).fuccess } } @@ -416,8 +416,8 @@ final class Auth( def magicLinkApply = OpenBody { implicit ctx => implicit val req = ctx.body - env.security.recaptcha.verify() flatMap { - _.ok ?? { + env.security.hcaptcha.verify() flatMap { captcha => + if (captcha.ok) forms.magicLink.form .bindFromRequest() .fold( @@ -437,7 +437,8 @@ final class Auth( Redirect(routes.Auth.magicLinkSent(data.realEmail.value)).fuccess } ) - } + else + BadRequest(renderMagicLink(none, fail = true)).fuccess } } diff --git a/app/views/account/reopen.scala b/app/views/account/reopen.scala index 934096c19f..ed64fa06ee 100644 --- a/app/views/account/reopen.scala +++ b/app/views/account/reopen.scala @@ -9,14 +9,14 @@ import controllers.routes object reopen { - def form(form: lila.security.RecaptchaForm[_], error: Option[String] = None)(implicit + def form(form: lila.security.HcaptchaForm[_], error: Option[String] = None)(implicit ctx: Context ) = views.html.base.layout( title = "Reopen your account", moreCss = cssTag("auth"), - moreJs = views.html.base.recaptcha.script(form), - csp = defaultCsp.withRecaptcha.some + moreJs = views.html.base.hcaptcha.script(form), + csp = defaultCsp.withHcaptcha.some ) { main(cls := "page-small box box-pad")( h1("Reopen your account"), @@ -26,7 +26,7 @@ object reopen { p(strong("This will only work once.")), p("If you close your account a second time, there will be no way of recovering it."), hr, - postForm(id := form.formId, cls := "form3", action := routes.Account.reopenApply)( + postForm(cls := "form3", action := routes.Account.reopenApply)( error.map { err => p(cls := "error")(strong(err)) }, @@ -35,7 +35,8 @@ object reopen { .group(form("email"), trans.email(), help = frag("Email address associated to the account").some)( form3.input(_, typ = "email") ), - form3.action(views.html.base.recaptcha.button(form)(form3.submit(trans.emailMeALink()))) + views.html.base.hcaptcha.tag(form), + form3.action(form3.submit(trans.emailMeALink())) ) ) } diff --git a/app/views/auth/bits.scala b/app/views/auth/bits.scala index faa28a78a7..46a28f3236 100644 --- a/app/views/auth/bits.scala +++ b/app/views/auth/bits.scala @@ -7,7 +7,7 @@ import play.api.data.{ Field, Form } import lila.api.Context import lila.app.templating.Environment._ import lila.app.ui.ScalatagsTemplate._ -import lila.security.RecaptchaForm +import lila.security.HcaptchaForm import lila.user.User object bits { @@ -33,25 +33,24 @@ object bits { } ) - def passwordReset(form: RecaptchaForm[_], fail: Boolean)(implicit ctx: Context) = + def passwordReset(form: HcaptchaForm[_], fail: Boolean)(implicit ctx: Context) = views.html.base.layout( title = trans.passwordReset.txt(), moreCss = cssTag("auth"), - moreJs = views.html.base.recaptcha.script(form), - csp = defaultCsp.withRecaptcha.some + moreJs = views.html.base.hcaptcha.script(form), + csp = defaultCsp.withHcaptcha.some ) { main(cls := "auth auth-signup box box-pad")( h1( fail option span(cls := "is-red", dataIcon := "L"), trans.passwordReset() ), - postForm(id := form.formId, cls := "form3", action := routes.Auth.passwordResetApply)( - form3.group(form("email"), trans.email())(form3.input(_, typ = "email")(autofocus)), - form3.action( - views.html.base.recaptcha.button(form) { - form3.submit(trans.emailMeALink()) - } - ) + postForm(cls := "form3", action := routes.Auth.passwordResetApply)( + form3.group(form("email"), trans.email())( + form3.input(_, typ = "email")(autofocus, required, autocomplete := "email") + ), + views.html.base.hcaptcha.tag(form), + form3.action(form3.submit(trans.emailMeALink())) ) ) } @@ -106,12 +105,12 @@ object bits { ) } - def magicLink(form: RecaptchaForm[_], fail: Boolean)(implicit ctx: Context) = + def magicLink(form: HcaptchaForm[_], fail: Boolean)(implicit ctx: Context) = views.html.base.layout( title = "Log in by email", moreCss = cssTag("auth"), - moreJs = views.html.base.recaptcha.script(form), - csp = defaultCsp.withRecaptcha.some + moreJs = views.html.base.hcaptcha.script(form), + csp = defaultCsp.withHcaptcha.some ) { main(cls := "auth auth-signup box box-pad")( h1( @@ -119,13 +118,12 @@ object bits { "Log in by email" ), p("We will send you an email containing a link to log you in."), - postForm(id := form.formId, cls := "form3", action := routes.Auth.magicLinkApply)( + postForm(cls := "form3", action := routes.Auth.magicLinkApply)( form3.group(form("email"), trans.email())( - form3.input(_, typ = "email")(autofocus, autocomplete := "email") + form3.input(_, typ = "email")(autofocus, required, autocomplete := "email") ), - form3.action(views.html.base.recaptcha.button(form) { - form3.submit(trans.emailMeALink()) - }) + views.html.base.hcaptcha.tag(form), + form3.action(form3.submit(trans.emailMeALink())) ) ) } diff --git a/app/views/auth/signup.scala b/app/views/auth/signup.scala index 79cda0ac03..55d7d3bb3f 100644 --- a/app/views/auth/signup.scala +++ b/app/views/auth/signup.scala @@ -9,25 +9,21 @@ import controllers.routes object signup { - def apply(form: lila.security.RecaptchaForm[_])(implicit ctx: Context) = + def apply(form: lila.security.HcaptchaForm[_])(implicit ctx: Context) = views.html.base.layout( title = trans.signUp.txt(), moreJs = frag( jsModule("login"), embedJsUnsafeLoadThen("""loginSignup.signupStart()"""), - views.html.base.recaptcha.script(form), - fingerprintTag, - embedJsUnsafeLoadThen(""" - lichess.loadModule('passwordComplexity').then(() => - passwordComplexity.addPasswordChangeListener('form3-password') - )""") + views.html.base.hcaptcha.script(form), + fingerprintTag ), moreCss = cssTag("auth"), - csp = defaultCsp.withRecaptcha.some + csp = defaultCsp.withHcaptcha.some ) { main(cls := "auth auth-signup box box-pad")( h1(trans.signUp()), - postForm(id := form.formId, cls := "form3", action := routes.Auth.signupPost)( + postForm(id := "signup-form", cls := "form3", action := routes.Auth.signupPost)( auth.bits.formFields(form("username"), form("password"), form("email").some, register = true), input(id := "signup-fp-input", name := "fp", tpe := "hidden"), div(cls := "form-group text", dataIcon := "")( @@ -41,9 +37,8 @@ object signup { ) ), agreement(form("agreement"), form.form.errors.exists(_.key startsWith "agreement.")), - views.html.base.recaptcha.button(form) { - button(cls := "submit button text big")(trans.signUp()) - } + views.html.base.hcaptcha.tag(form), + button(cls := "submit button text big")(trans.signUp()) ) ) } diff --git a/app/views/base/hcaptcha.scala b/app/views/base/hcaptcha.scala new file mode 100644 index 0000000000..96e62f0dbd --- /dev/null +++ b/app/views/base/hcaptcha.scala @@ -0,0 +1,18 @@ +package views.html +package base + +import lila.api.Context +import lila.app.templating.Environment._ +import lila.app.ui.ScalatagsTemplate._ +import lila.security.HcaptchaForm + +object hcaptcha { + + private val dataSitekey = attr("data-sitekey") + + def script(re: HcaptchaForm[_])(implicit ctx: Context) = + re.enabled option raw("""""") + + def tag(form: HcaptchaForm[_]) = + div(cls := "h-captcha form-group", dataSitekey := form.config.key) +} diff --git a/app/views/base/recaptcha.scala b/app/views/base/recaptcha.scala deleted file mode 100644 index a557799980..0000000000 --- a/app/views/base/recaptcha.scala +++ /dev/null @@ -1,29 +0,0 @@ -package views.html -package base - -import lila.api.Context -import lila.app.templating.Environment._ -import lila.app.ui.ScalatagsTemplate._ -import lila.security.RecaptchaForm - -object recaptcha { - - private val callbackFunction = "recaptchaSubmit" - - def script(re: RecaptchaForm[_])(implicit ctx: Context) = - re.enabled option frag( - raw( - """""" - ), - embedJsUnsafe( - s"""$callbackFunction=t=>document.getElementById('${re.formId}').submit()""" - ) - ) - - def button(re: RecaptchaForm[_])(tag: Tag) = - tag( - cls := "g-recaptcha", - attr("data-sitekey") := re.config.key, - attr("data-callback") := callbackFunction - ) -} diff --git a/conf/base.conf b/conf/base.conf index bb87815a90..6e69e0a27b 100644 --- a/conf/base.conf +++ b/conf/base.conf @@ -236,7 +236,7 @@ security { enabled = false url = "http://ip2proxy.lichess.ovh:1929" } - recaptcha = ${recaptcha} + hcaptcha = ${hcaptcha} lame_name_check = true } oauth { @@ -248,9 +248,9 @@ oauth { app = oauth_client } } -recaptcha { - endpoint = "https://www.google.com/recaptcha/api/siteverify" - public_key = "6LcxLTUUAAAAAFwWrEgEehMJ6VXrtVOKIifTLGoW" +hcaptcha { + endpoint = "https://hcaptcha.com/siteverify" + public_key = "f91a151d-73e5-4a95-9d4e-74bfa19bec9d" private_key = "" enabled = false } diff --git a/modules/common/src/main/ContentSecurityPolicy.scala b/modules/common/src/main/ContentSecurityPolicy.scala index 7324f5b291..7f69f34d33 100644 --- a/modules/common/src/main/ContentSecurityPolicy.scala +++ b/modules/common/src/main/ContentSecurityPolicy.scala @@ -46,10 +46,14 @@ case class ContentSecurityPolicy( def withGoogleForm = copy(frameSrc = "https://docs.google.com" :: frameSrc) - def withRecaptcha = + private val hCaptchaDomains = List("https://hcaptcha.com", "https://*.hcaptcha.com") + + def withHcaptcha = copy( - scriptSrc = "https://www.google.com" :: scriptSrc, - frameSrc = "https://www.google.com" :: frameSrc + scriptSrc = hCaptchaDomains ::: scriptSrc, + frameSrc = hCaptchaDomains ::: frameSrc, + styleSrc = hCaptchaDomains ::: styleSrc, + connectSrc = hCaptchaDomains ::: connectSrc ) def withPeer = copy(connectSrc = "wss://0.peerjs.com" :: connectSrc) diff --git a/modules/common/src/main/mon.scala b/modules/common/src/main/mon.scala index 32e1dfba46..765c3922a0 100644 --- a/modules/common/src/main/mon.scala +++ b/modules/common/src/main/mon.scala @@ -315,9 +315,9 @@ object mon { } def usersAlikeTime(field: String) = timer("security.usersAlike.time").withTag("field", field) def usersAlikeFound(field: String) = histogram("security.usersAlike.found").withTag("field", field) - object recaptcha { + object hCaptcha { def hit(client: String, result: String) = - counter("recaptcha.hit").withTags(Map("client" -> client, "result" -> result)) + counter("hcaptcha.hit").withTags(Map("client" -> client, "result" -> result)) } } object tv { diff --git a/modules/security/src/main/Env.scala b/modules/security/src/main/Env.scala index b911fc1b1d..0a49c06761 100644 --- a/modules/security/src/main/Env.scala +++ b/modules/security/src/main/Env.scala @@ -36,10 +36,10 @@ final class Env( private val config = appConfig.get[SecurityConfig]("security")(SecurityConfig.loader) - private def recaptchaPublicConfig = config.recaptcha.public + private def hcaptchaPublicConfig = config.hcaptcha.public - def recaptcha[A](formId: String, form: play.api.data.Form[A]) = - RecaptchaForm(form, formId, config.recaptcha.public) + def hcaptcha[A](form: play.api.data.Form[A]) = + HcaptchaForm(form, config.hcaptcha.public) lazy val firewall = new Firewall( coll = db(config.collection.firewall), @@ -48,9 +48,9 @@ final class Env( lazy val flood = wire[Flood] - lazy val recaptcha: Recaptcha = - if (config.recaptcha.enabled) wire[RecaptchaGoogle] - else RecaptchaSkip + lazy val hcaptcha: Hcaptcha = + if (config.hcaptcha.enabled) wire[HcaptchaReal] + else HcaptchaSkip lazy val forms = wire[SecurityForm] diff --git a/modules/security/src/main/Recaptcha.scala b/modules/security/src/main/Hcaptcha.scala similarity index 64% rename from modules/security/src/main/Recaptcha.scala rename to modules/security/src/main/Hcaptcha.scala index 318a48c02c..b42f4f0702 100644 --- a/modules/security/src/main/Recaptcha.scala +++ b/modules/security/src/main/Hcaptcha.scala @@ -13,15 +13,15 @@ import play.api.mvc.RequestHeader import lila.common.config._ import lila.common.HTTPRequest -trait Recaptcha { +trait Hcaptcha { - def verify(response: String, req: RequestHeader): Fu[Recaptcha.Result] + def verify(response: String, req: RequestHeader): Fu[Hcaptcha.Result] - def verify()(implicit req: play.api.mvc.Request[_], formBinding: FormBinding): Fu[Recaptcha.Result] = - verify(~Recaptcha.form.bindFromRequest().value.flatten, req) + def verify()(implicit req: play.api.mvc.Request[_], formBinding: FormBinding): Fu[Hcaptcha.Result] = + verify(~Hcaptcha.form.bindFromRequest().value.flatten, req) } -object Recaptcha { +object Hcaptcha { sealed abstract class Result(val ok: Boolean) object Result { @@ -30,7 +30,7 @@ object Recaptcha { case object Fail extends Result(false) } - val field = "g-recaptcha-response" -> optional(nonEmptyText) + val field = "h-captcha-response" -> optional(nonEmptyText) val form = Form(single(field)) private[security] case class Config( @@ -39,30 +39,27 @@ object Recaptcha { @ConfigName("private_key") privateKey: Secret, enabled: Boolean ) { - def public = RecaptchaPublicConfig(publicKey, enabled) + def public = HcaptchaPublicConfig(publicKey, enabled) } implicit private[security] val configLoader = AutoConfig.loader[Config] } -object RecaptchaSkip extends Recaptcha { +object HcaptchaSkip extends Hcaptcha { - def verify(response: String, req: RequestHeader) = fuccess(Recaptcha.Result.Valid) + def verify(response: String, req: RequestHeader) = fuccess(Hcaptcha.Result.Valid) } -final class RecaptchaGoogle( +final class HcaptchaReal( ws: StandaloneWSClient, netDomain: NetDomain, - config: Recaptcha.Config + config: Hcaptcha.Config )(implicit ec: scala.concurrent.ExecutionContext) - extends Recaptcha { + extends Hcaptcha { - import Recaptcha.Result + import Hcaptcha.Result - private case class GoodResponse( - success: Boolean, - hostname: String - ) + private case class GoodResponse(success: Boolean, hostname: String) implicit private val goodReader = Json.reads[GoodResponse] private case class BadResponse( @@ -80,34 +77,35 @@ final class RecaptchaGoogle( Map( "secret" -> config.privateKey.value, "response" -> response, - "remoteip" -> HTTPRequest.ipAddress(req).value + "remoteip" -> HTTPRequest.ipAddress(req).value, + "sitekey" -> config.publicKey ) ) map { case res if res.status == 200 => res.body[JsValue].validate[GoodResponse] match { case JsSuccess(res, _) => - lila.mon.security.recaptcha.hit(client, "success").increment() + lila.mon.security.hCaptcha.hit(client, "success").increment() if (res.success && res.hostname == netDomain.value) Result.Valid else Result.Fail case JsError(err) => res.body[JsValue].validate[BadResponse].asOpt match { case Some(err) if err.missingInput => - logger.info(s"recaptcha missing ${HTTPRequest printClient req}") - lila.mon.security.recaptcha.hit(client, "missing").increment() + logger.info(s"hcaptcha missing ${HTTPRequest printClient req}") + lila.mon.security.hCaptcha.hit(client, "missing").increment() if (HTTPRequest.apiVersion(req).isDefined) Result.Pass else Result.Fail case Some(err) => - lila.mon.security.recaptcha.hit(client, err.toString).increment() + lila.mon.security.hCaptcha.hit(client, err.toString).increment() Result.Fail case _ => - lila.mon.security.recaptcha.hit(client, "error").increment() - logger.info(s"recaptcha $err ${res.body}") + lila.mon.security.hCaptcha.hit(client, "error").increment() + logger.info(s"hcaptcha $err ${res.body}") Result.Fail } } case res => - lila.mon.security.recaptcha.hit(client, res.status.toString).increment() - logger.info(s"recaptcha ${res.status} ${res.body}") + lila.mon.security.hCaptcha.hit(client, res.status.toString).increment() + logger.info(s"hcaptcha ${res.status} ${res.body}") Result.Fail } - } + }.thenPp } diff --git a/modules/security/src/main/SecurityConfig.scala b/modules/security/src/main/SecurityConfig.scala index 21f5a40500..e27531bcdb 100644 --- a/modules/security/src/main/SecurityConfig.scala +++ b/modules/security/src/main/SecurityConfig.scala @@ -21,7 +21,7 @@ final private class SecurityConfig( @ConfigName("disposable_email") val disposableEmail: DisposableEmail, @ConfigName("dns_api") val dnsApi: DnsApi, @ConfigName("check_mail_api") val checkMail: CheckMail, - val recaptcha: Recaptcha.Config, + val hcaptcha: Hcaptcha.Config, val mailer: Mailer.Config, @ConfigName("ip2proxy") val ip2Proxy: Ip2Proxy, @ConfigName("lame_name_check") val lameNameCheck: LameNameCheck diff --git a/modules/security/src/main/SecurityForm.scala b/modules/security/src/main/SecurityForm.scala index 846a8ba17e..0a3dc21f55 100644 --- a/modules/security/src/main/SecurityForm.scala +++ b/modules/security/src/main/SecurityForm.scala @@ -14,7 +14,7 @@ final class SecurityForm( authenticator: lila.user.Authenticator, emailValidator: EmailAddressValidator, lameNameCheck: LameNameCheck, - recaptchaPublicConfig: RecaptchaPublicConfig + hcaptchaPublicConfig: HcaptchaPublicConfig )(implicit ec: scala.concurrent.ExecutionContext) { import SecurityForm._ @@ -77,7 +77,7 @@ final class SecurityForm( val emailField = withAcceptableDns(acceptableUniqueEmail(none)) - val website = RecaptchaForm( + val website = HcaptchaForm( Form( mapping( "username" -> username, @@ -87,8 +87,7 @@ final class SecurityForm( "fp" -> optional(nonEmptyText) )(SignupData.apply)(_ => None) ), - "signup-form", - recaptchaPublicConfig + hcaptchaPublicConfig ) val mobile = Form( @@ -100,14 +99,13 @@ final class SecurityForm( ) } - val passwordReset = RecaptchaForm( + val passwordReset = HcaptchaForm( Form( mapping( "email" -> sendableEmail // allow unacceptable emails for BC )(PasswordReset.apply)(_ => None) ), - "password-reset-form", - recaptchaPublicConfig + hcaptchaPublicConfig ) val newPassword = Form( @@ -130,14 +128,13 @@ final class SecurityForm( ) ) - val magicLink = RecaptchaForm( + val magicLink = HcaptchaForm( Form( mapping( "email" -> sendableEmail // allow unacceptable emails for BC )(MagicLink.apply)(_ => None) ), - "magic-link-form", - recaptchaPublicConfig + hcaptchaPublicConfig ) def changeEmail(u: User, old: Option[EmailAddress]) = @@ -207,15 +204,14 @@ final class SecurityForm( def toggleKid = passwordProtected _ - val reopen = RecaptchaForm( + val reopen = HcaptchaForm( Form( mapping( "username" -> LilaForm.cleanNonEmptyText, "email" -> sendableEmail // allow unacceptable emails for BC )(Reopen.apply)(_ => None) ), - "reopen-form", - recaptchaPublicConfig + hcaptchaPublicConfig ) private def passwordMapping(candidate: User.LoginCandidate) = diff --git a/modules/security/src/main/Signup.scala b/modules/security/src/main/Signup.scala index 0485c0c634..cd894d17dc 100644 --- a/modules/security/src/main/Signup.scala +++ b/modules/security/src/main/Signup.scala @@ -18,7 +18,7 @@ final class Signup( forms: SecurityForm, emailAddressValidator: EmailAddressValidator, emailConfirm: EmailConfirm, - recaptcha: Recaptcha, + hcaptcha: Hcaptcha, authenticator: lila.user.Authenticator, userRepo: lila.user.UserRepo, slack: lila.irc.SlackApi, @@ -59,16 +59,16 @@ final class Signup( def website( blind: Boolean )(implicit req: Request[_], lang: Lang, formBinding: FormBinding): Fu[Signup.Result] = - recaptcha.verify().flatMap { - case Recaptcha.Result.Fail => fuccess(Signup.MissingCaptcha) - case Recaptcha.Result.Pass if !blind => fuccess(Signup.MissingCaptcha) - case recaptchaResult => + hcaptcha.verify().flatMap { + case Hcaptcha.Result.Fail => fuccess(Signup.MissingCaptcha) + case Hcaptcha.Result.Pass if !blind => fuccess(Signup.MissingCaptcha) + case hcaptchaResult => forms.signup.website.form .bindFromRequest() .fold[Fu[Signup.Result]]( err => fuccess(Signup.Bad(err tap signupErrLog)), data => - signupRateLimit(data.username, if (recaptchaResult == Recaptcha.Result.Valid) 1 else 3) { + signupRateLimit(data.username, if (hcaptchaResult == Hcaptcha.Result.Valid) 1 else 3) { MustConfirmEmail(data.fingerPrint) flatMap { mustConfirm => lila.mon.user.register.count(none) lila.mon.user.register.mustConfirmEmail(mustConfirm.toString).increment() diff --git a/modules/security/src/main/model.scala b/modules/security/src/main/model.scala index e427f0e47f..17b866afd1 100644 --- a/modules/security/src/main/model.scala +++ b/modules/security/src/main/model.scala @@ -36,12 +36,12 @@ case class LocatedSession(session: UserSession, location: Option[Location]) case class IpAndFp(ip: IpAddress, fp: Option[String], user: User.ID) -case class RecaptchaPublicConfig(key: String, enabled: Boolean) +case class HcaptchaPublicConfig(key: String, enabled: Boolean) -case class RecaptchaForm[A](form: Form[A], formId: String, config: RecaptchaPublicConfig) { +case class HcaptchaForm[A](form: Form[A], config: HcaptchaPublicConfig) { def enabled = config.enabled def apply(key: String) = form(key) - def withForm[B](form: Form[B]) = RecaptchaForm(form, formId, config) + def withForm[B](form: Form[B]) = HcaptchaForm(form, config) } case class LameNameCheck(value: Boolean) extends AnyVal diff --git a/ui/site/src/login.ts b/ui/site/src/login.ts index fdb5c14a27..d412bd69e8 100644 --- a/ui/site/src/login.ts +++ b/ui/site/src/login.ts @@ -60,12 +60,23 @@ export function signupStart() { xhr.json(xhr.url('/player/autocomplete', { term: name, exists: 1 })).then(res => $exists.toggle(res)); }, 300); - $form.on('submit', () => - $form.find('button.submit').prop('disabled', true).removeAttr('data-icon').addClass('frameless').html(spinnerHtml) - ); + $form.on('submit', () => { + if ($form.find('[name="h-captcha-response"]').val()) + $form + .find('button.submit') + .prop('disabled', true) + .removeAttr('data-icon') + .addClass('frameless') + .html(spinnerHtml); + else return false; + }); window.signupSubmit = () => { const form = document.getElementById('signup-form') as HTMLFormElement; if (form.reportValidity()) form.submit(); }; + + lichess + .loadModule('passwordComplexity') + .then(() => window['passwordComplexity'].addPasswordChangeListener('form3-password')); }