diff --git a/app/Services/Auth/UserService.php b/app/Services/Auth/UserService.php index f4df159d8..09a902051 100644 --- a/app/Services/Auth/UserService.php +++ b/app/Services/Auth/UserService.php @@ -128,9 +128,22 @@ public function registerUser(array $payload, ?OAuth2OTP $otp = null):User { return $this->tx_service->transaction(function () use ($payload, $otp) { - Log::debug(sprintf("UserService::registerUser payload %s", json_encode($payload))); - $email = trim($payload['email']); + + // The payload is the raw sign-up form (password, confirmation, captcha and CSRF + // tokens): log field names only, never values. Field names are client-controlled, so + // they are JSON-encoded to escape control characters. They stay in the message: with + // inline line breaks allowed (Laravel's default), Monolog turns an escaped newline in + // a JSON-encoded context back into a real one. + Log::debug + ( + sprintf + ( + "UserService::registerUser email %s fields %s", + json_encode($email), + json_encode(array_keys($payload)) + ) + ); $former_user = $this->user_repository->getByEmailOrName($email); if (!is_null($former_user)) diff --git a/resources/js/components/turnstile_captcha.js b/resources/js/components/turnstile_captcha.js new file mode 100644 index 000000000..3658adc7d --- /dev/null +++ b/resources/js/components/turnstile_captcha.js @@ -0,0 +1,147 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Turnstile } from "@marsidev/react-turnstile"; +import Button from "@material-ui/core/Button"; +import Grid from "@material-ui/core/Grid"; + +// Turnstile does not always invoke onError when its iframe cannot be loaded (e.g. a network +// error reaching challenges.cloudflare.com): the widget just never renders and never issues a +// token. Without a token the backend rejects the form, so after this long without one we treat +// the security check as failed and let the user retry it. +export const CAPTCHA_LOAD_TIMEOUT_MS = 15000; + +export const CAPTCHA_STATUS = { + VERIFYING: "verifying", + // Turnstile is showing a challenge the user has to complete: not a load failure. + INTERACTIVE: "interactive", + SOLVED: "solved", + FAILED: "failed", +}; + +export const CAPTCHA_MESSAGES = { + VERIFYING: "Please wait while we verify you are human, then submit again.", + INTERACTIVE: "Complete the security check, then submit again.", + FAILED: + "We couldn't load the security check. Check your connection or disable any content blocker, then retry.", +}; + +// Id of the