mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 02:18:09 +03:00
feat: recaptcha->hcaptcha (#1577)
This commit is contained in:
@@ -45,7 +45,7 @@ jobs:
|
||||
env:
|
||||
VITE_GIT_REPO: "GitHub:zkldi/Tachi"
|
||||
VITE_TCHIC_MODE: "boku"
|
||||
VITE_RECAPTCHA_KEY: "6LcsYbIpAAAAAEJffjIXmbQcxj_SBZG7BnSPjF4L"
|
||||
VITE_HCAPTCHA_SITEKEY: "4d968a2e-80ec-4b02-80be-c2205a4e7bb8"
|
||||
VITE_SERVER_URL: "https://boku.tachi.ac"
|
||||
VITE_DISCORD: "https://discord.gg/E4n8ZZF5ES"
|
||||
VITE_CDN_URL: "https://cdn-boku.tachi.ac"
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
VITE_TCHIC_MODE: "kamai"
|
||||
VITE_CDN_URL: "https://cdn-kamai.tachi.ac"
|
||||
VITE_GIT_REPO: "GitHub:zkldi/Tachi"
|
||||
VITE_RECAPTCHA_KEY: "6LcsYbIpAAAAAEJffjIXmbQcxj_SBZG7BnSPjF4L"
|
||||
VITE_HCAPTCHA_SITEKEY: "10000000-ffff-ffff-ffff-000000000001"
|
||||
TACHI_NAME: "Kamaitachi"
|
||||
BUILD_OUT_DIR: /tmp/kamai
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
VITE_FLO_CLIENT_ID: "9krYLjq1rz9icCefO6OWxoMk"
|
||||
VITE_MIN_CLIENT_ID: "A0DBDBB063CD800530EF01C6488B282137E0191E"
|
||||
VITE_GIT_REPO: "GitHub:zkldi/Tachi"
|
||||
VITE_RECAPTCHA_KEY: "6LcsYbIpAAAAAEJffjIXmbQcxj_SBZG7BnSPjF4L"
|
||||
VITE_HCAPTCHA_SITEKEY: "10000000-ffff-ffff-ffff-000000000001"
|
||||
TACHI_NAME: "Tachi Dev"
|
||||
BUILD_OUT_DIR: /tmp/dev
|
||||
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
"@fullcalendar/core": "catalog:",
|
||||
"@fullcalendar/daygrid": "catalog:",
|
||||
"@fullcalendar/react": "catalog:",
|
||||
"@hcaptcha/react-hcaptcha": "catalog:",
|
||||
"@nivo/bar": "catalog:",
|
||||
"@nivo/core": "catalog:",
|
||||
"@nivo/line": "catalog:",
|
||||
"@types/lodash": "catalog:",
|
||||
"@types/natural-compare": "catalog:",
|
||||
"@types/react-google-recaptcha": "catalog:",
|
||||
"deepmerge": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"fast-json-stable-hash": "catalog:",
|
||||
@@ -99,7 +99,6 @@
|
||||
"react-async": "catalog:",
|
||||
"react-bootstrap": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-google-recaptcha": "catalog:",
|
||||
"react-hot-toast": "catalog:",
|
||||
"react-markdown": "catalog:",
|
||||
"react-query": "catalog:",
|
||||
@@ -440,6 +439,7 @@
|
||||
"@fullcalendar/core": "^6.0.0",
|
||||
"@fullcalendar/daygrid": "^6.0.0",
|
||||
"@fullcalendar/react": "^6.0.0",
|
||||
"@hcaptcha/react-hcaptcha": "2.0.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@nivo/bar": "0.79.1",
|
||||
"@nivo/core": "0.79.0",
|
||||
@@ -467,7 +467,6 @@
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/react": "17.0.13",
|
||||
"@types/react-dom": "17.0.8",
|
||||
"@types/react-google-recaptcha": "2.1.2",
|
||||
"@types/react-router-bootstrap": "0.26.1",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/sql.js": "^1.4.11",
|
||||
@@ -542,7 +541,6 @@
|
||||
"react-async": "10.0.1",
|
||||
"react-bootstrap": "2.9.0-beta.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-hot-toast": "2.0.0",
|
||||
"react-markdown": "7.0.1",
|
||||
"react-query": "3.18.1",
|
||||
@@ -932,6 +930,8 @@
|
||||
|
||||
"@fullcalendar/react": ["@fullcalendar/react@6.1.20", "", { "peerDependencies": { "@fullcalendar/core": "~6.1.20", "react": "^16.7.0 || ^17 || ^18 || ^19", "react-dom": "^16.7.0 || ^17 || ^18 || ^19" } }, "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA=="],
|
||||
|
||||
"@hcaptcha/react-hcaptcha": ["@hcaptcha/react-hcaptcha@2.0.2", "", {}, "sha512-VbuH6VJ6m3BHmVBHs0fL9t+suZd7PQEqCzqL2BiUbBvbHI3XfvSgdiug2QiEPN8zskbPTIV/FfGPF53JCckrow=="],
|
||||
|
||||
"@httptoolkit/websocket-stream": ["@httptoolkit/websocket-stream@6.0.1", "", { "dependencies": { "@types/ws": "*", "duplexify": "^3.5.1", "inherits": "^2.0.1", "isomorphic-ws": "^4.0.1", "readable-stream": "^2.3.3", "safe-buffer": "^5.1.2", "ws": "*", "xtend": "^4.0.0" } }, "sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
@@ -1340,8 +1340,6 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@17.0.8", "", { "dependencies": { "@types/react": "*" } }, "sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A=="],
|
||||
|
||||
"@types/react-google-recaptcha": ["@types/react-google-recaptcha@2.1.2", "", { "dependencies": { "@types/react": "*" } }, "sha512-Zeb0pwDeXHYIQy5mNgewMr+jf4xtrjSHTdJW8wrubNB6sNEaxqo8yk02HVpwy7p7Hzh5y6iSz6gKoPonVBlswQ=="],
|
||||
|
||||
"@types/react-router": ["@types/react-router@5.1.20", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*" } }, "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q=="],
|
||||
|
||||
"@types/react-router-bootstrap": ["@types/react-router-bootstrap@0.26.1", "", { "dependencies": { "@types/react": "*" } }, "sha512-EdgZn/PmSA8+I0RW6WyDUgBvkVXc8AmbbgOJ8WJfec1R/V06X++OrIOzwzdgYl4g3uXvEB6oEIxzcjx1/8Opqw=="],
|
||||
@@ -2564,16 +2562,12 @@
|
||||
|
||||
"react-async": ["react-async@10.0.1", "", { "peerDependencies": { "react": ">=16.3.1" } }, "sha512-ORUz5ca0B57QgBIzEZM5SuhJ6xFjkvEEs0gylLNlWf06vuVcLZsjIw3wx58jJkZG38p+0nUAxRgFW2b7mnVZzA=="],
|
||||
|
||||
"react-async-script": ["react-async-script@1.2.0", "", { "dependencies": { "hoist-non-react-statics": "^3.3.0", "prop-types": "^15.5.0" }, "peerDependencies": { "react": ">=16.4.1" } }, "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q=="],
|
||||
|
||||
"react-bootstrap": ["react-bootstrap@2.9.0-beta.0", "", { "dependencies": { "@babel/runtime": "^7.22.5", "@restart/hooks": "^0.4.9", "@restart/ui": "^1.6.6", "@types/react-transition-group": "^4.4.6", "classnames": "^2.3.2", "dom-helpers": "^5.2.1", "invariant": "^2.2.4", "prop-types": "^15.8.1", "prop-types-extra": "^1.1.0", "react-transition-group": "^4.4.5", "uncontrollable": "^7.2.1", "warning": "^4.0.3" }, "peerDependencies": { "@types/react": ">=16.14.8", "react": ">=16.14.0", "react-dom": ">=16.14.0" }, "optionalPeers": ["@types/react"] }, "sha512-atmQhuRce7g/G+eclJn1eMZ1wbG6j8yYWrtr+Hlv/fVX0bw3QOUs62Q1G3qSqbkaZ4E625w/56GFR0rXf36Fog=="],
|
||||
|
||||
"react-dom": ["react-dom@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "scheduler": "^0.20.2" }, "peerDependencies": { "react": "17.0.2" } }, "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="],
|
||||
|
||||
"react-fast-compare": ["react-fast-compare@2.0.4", "", {}, "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="],
|
||||
|
||||
"react-google-recaptcha": ["react-google-recaptcha@2.1.0", "", { "dependencies": { "prop-types": "^15.5.0", "react-async-script": "^1.1.1" }, "peerDependencies": { "react": ">=16.4.1" } }, "sha512-K9jr7e0CWFigi8KxC3WPvNqZZ47df2RrMAta6KmRoE4RUi7Ys6NmNjytpXpg4HI/svmQJLKR+PncEPaNJ98DqQ=="],
|
||||
|
||||
"react-hot-toast": ["react-hot-toast@2.0.0", "", { "dependencies": { "goober": "^2.0.35" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-J0J2rcSvKetlziVquwESgk85pV9JB0Dz3RJIZcAEv7CWU2y2z8BQqxmZHZUxjPKIBGPqXAocZch4Kj4oUdX4+w=="],
|
||||
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
@@ -29,7 +29,7 @@ Logs a user in and returns a session cookie.
|
||||
| :---------: | :----: | :---------------------------------------------------------------------------------------: |
|
||||
| `username` | String | The user's username. This is compared case-insensitively. |
|
||||
| `!password` | String | The user's password. |
|
||||
| `captcha` | String | Information about the captcha filled out by the user. We use a Google ReCaptcha instance. |
|
||||
| `captcha` | String | The hCaptcha response token from the client widget; the server verifies it with hCaptcha. |
|
||||
|
||||
!!! info
|
||||
The `!` prefix is special in that anything with it is assumed to be private and is **always**
|
||||
@@ -85,7 +85,7 @@ POST /api/v1/auth/login
|
||||
| `!password` | String | An 8 character or longer string. |
|
||||
| `!email` | String | Valid email. The `!` prefix keeps this field out of server request logs. |
|
||||
| `inviteCode` (Kamaitachi Only) | String (Undefined/Unused on Bokutachi) | If on Kamaitachi, this is the user's invitation code. |
|
||||
| `captcha` | String | |
|
||||
| `captcha` | String | The hCaptcha response token from the client widget; the server verifies it with hCaptcha. |
|
||||
|
||||
### Response
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Logs a user in and returns a session cookie.
|
||||
| :---------: | :----: | :---------------------------------------------------------------------------------------: |
|
||||
| `username` | String | The user's username. This is compared case-insensitively. |
|
||||
| `!password` | String | The user's password. |
|
||||
| `captcha` | String | Information about the captcha filled out by the user. We use a Google ReCaptcha instance. |
|
||||
| `captcha` | String | The hCaptcha response token from the client widget; the server verifies it with hCaptcha. |
|
||||
|
||||
!!! info
|
||||
The `!` prefix is special in that anything with it is assumed to be private and is **always**
|
||||
@@ -85,7 +85,7 @@ POST /api/v1/auth/login
|
||||
| `!password` | String | An 8 character or longer string. |
|
||||
| `!email` | String | Valid email. The `!` prefix keeps this field out of server request logs. |
|
||||
| `inviteCode` (Kamaitachi Only) | String (Undefined/Unused on Bokutachi) | If on Kamaitachi, this is the user's invitation code. |
|
||||
| `captcha` | String | |
|
||||
| `captcha` | String | The hCaptcha response token from the client widget; the server verifies it with hCaptcha. |
|
||||
|
||||
### Response
|
||||
|
||||
|
||||
+1
-2
@@ -56,6 +56,7 @@
|
||||
"@fullcalendar/core": "^6.0.0",
|
||||
"@fullcalendar/daygrid": "^6.0.0",
|
||||
"@fullcalendar/react": "^6.0.0",
|
||||
"@hcaptcha/react-hcaptcha": "2.0.2",
|
||||
"@nivo/bar": "0.79.1",
|
||||
"@nivo/core": "0.79.0",
|
||||
"@nivo/line": "0.79.1",
|
||||
@@ -80,7 +81,6 @@
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/react": "17.0.13",
|
||||
"@types/react-dom": "17.0.8",
|
||||
"@types/react-google-recaptcha": "2.1.2",
|
||||
"@types/react-router-bootstrap": "0.26.1",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/supertest": "2.0.11",
|
||||
@@ -148,7 +148,6 @@
|
||||
"react-async": "10.0.1",
|
||||
"react-bootstrap": "2.9.0-beta.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-hot-toast": "2.0.0",
|
||||
"react-markdown": "7.0.1",
|
||||
"react-query": "3.18.1",
|
||||
|
||||
@@ -43,4 +43,6 @@ VITE_GIT_REPO=""
|
||||
# What client mode to use. Should be boku, kamai or omni.
|
||||
# This affects some things, like what buttons should appear in certain places and what colours the site should be.
|
||||
VITE_TCHIC_MODE="omni"
|
||||
```
|
||||
|
||||
# hCaptcha site key (optional in local dev — omit to hide the widget; required for prod/staging server captcha enforcement).
|
||||
VITE_HCAPTCHA_SITEKEY=""
|
||||
@@ -33,12 +33,12 @@
|
||||
"@fullcalendar/core": "catalog:",
|
||||
"@fullcalendar/daygrid": "catalog:",
|
||||
"@fullcalendar/react": "catalog:",
|
||||
"@hcaptcha/react-hcaptcha": "catalog:",
|
||||
"@nivo/bar": "catalog:",
|
||||
"@nivo/core": "catalog:",
|
||||
"@nivo/line": "catalog:",
|
||||
"@types/lodash": "catalog:",
|
||||
"@types/natural-compare": "catalog:",
|
||||
"@types/react-google-recaptcha": "catalog:",
|
||||
"deepmerge": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"fast-json-stable-hash": "catalog:",
|
||||
@@ -54,7 +54,6 @@
|
||||
"react-async": "catalog:",
|
||||
"react-bootstrap": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-google-recaptcha": "catalog:",
|
||||
"react-hot-toast": "catalog:",
|
||||
"react-markdown": "catalog:",
|
||||
"react-query": "catalog:",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { UserContext } from "#context/UserContext";
|
||||
import { APIFetchV1 } from "#util/api";
|
||||
import { HumaniseError } from "#util/humanise-error";
|
||||
import { HistorySafeGoBack } from "#util/misc";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import { useFormik } from "formik";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { Alert, Button, Form } from "react-bootstrap";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import toast from "react-hot-toast";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { type UserDocument } from "tachi-common";
|
||||
@@ -19,7 +19,7 @@ export default function LoginPage() {
|
||||
const { setUser } = useContext(UserContext);
|
||||
const history = useHistory();
|
||||
|
||||
const recaptchaRef = useRef<any>(null);
|
||||
const hcaptchaRef = useRef<InstanceType<typeof HCaptcha> | null>(null);
|
||||
|
||||
const [localDevFirstAdminLogin, setLocalDevFirstAdminLogin] = useState<
|
||||
| { password: string; status: "ready"; username: string }
|
||||
@@ -74,8 +74,8 @@ export default function LoginPage() {
|
||||
false,
|
||||
);
|
||||
|
||||
if (recaptchaRef.current) {
|
||||
recaptchaRef.current.reset();
|
||||
if (hcaptchaRef.current) {
|
||||
hcaptchaRef.current.resetCaptcha();
|
||||
}
|
||||
|
||||
if (!rj.success) {
|
||||
@@ -174,13 +174,16 @@ export default function LoginPage() {
|
||||
{err}
|
||||
</Form.Group>
|
||||
|
||||
{import.meta.env.VITE_RECAPTCHA_KEY && (
|
||||
<ReCAPTCHA
|
||||
onChange={(v) => {
|
||||
formik.setFieldValue("captcha", v);
|
||||
{import.meta.env.VITE_HCAPTCHA_SITEKEY && (
|
||||
<HCaptcha
|
||||
onExpire={() => {
|
||||
formik.setFieldValue("captcha", "");
|
||||
}}
|
||||
ref={recaptchaRef}
|
||||
sitekey={import.meta.env.VITE_RECAPTCHA_KEY}
|
||||
onVerify={(token) => {
|
||||
formik.setFieldValue("captcha", token);
|
||||
}}
|
||||
ref={hcaptchaRef}
|
||||
sitekey={import.meta.env.VITE_HCAPTCHA_SITEKEY}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import { type UseFormik } from "#types/react";
|
||||
import { APIFetchV1 } from "#util/api";
|
||||
import { HumaniseError } from "#util/humanise-error";
|
||||
import { HistorySafeGoBack } from "#util/misc";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import { useFormik } from "formik";
|
||||
import React, { type MutableRefObject, useContext, useRef, useState } from "react";
|
||||
import { Alert, Button, Form } from "react-bootstrap";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import toast from "react-hot-toast";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { type UserDocument } from "tachi-common";
|
||||
@@ -33,7 +33,7 @@ export default function RegisterPage() {
|
||||
|
||||
const { setUser } = useContext(UserContext);
|
||||
const history = useHistory();
|
||||
const recaptchaRef = useRef<any>();
|
||||
const hcaptchaRef = useRef<InstanceType<typeof HCaptcha> | null>(null);
|
||||
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
@@ -81,8 +81,8 @@ export default function RegisterPage() {
|
||||
true,
|
||||
);
|
||||
|
||||
if (recaptchaRef.current) {
|
||||
recaptchaRef.current.reset();
|
||||
if (hcaptchaRef.current) {
|
||||
hcaptchaRef.current.resetCaptcha();
|
||||
}
|
||||
|
||||
if (!rj.success) {
|
||||
@@ -126,7 +126,7 @@ export default function RegisterPage() {
|
||||
return (
|
||||
<LoginPageLayout description={<Description />} heading="Register">
|
||||
{readRules === "acknowledged" ? (
|
||||
<RegisterForm err={err} formik={formik} recaptchaRef={recaptchaRef} />
|
||||
<RegisterForm err={err} formik={formik} hcaptchaRef={hcaptchaRef} />
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<div className="mb-8">
|
||||
@@ -200,7 +200,7 @@ function Description() {
|
||||
function RegisterForm({
|
||||
formik,
|
||||
err,
|
||||
recaptchaRef,
|
||||
hcaptchaRef,
|
||||
}: {
|
||||
err: string;
|
||||
formik: UseFormik<{
|
||||
@@ -211,7 +211,7 @@ function RegisterForm({
|
||||
inviteCode: string;
|
||||
username: string;
|
||||
}>;
|
||||
recaptchaRef: MutableRefObject<any>;
|
||||
hcaptchaRef: MutableRefObject<InstanceType<typeof HCaptcha> | null>;
|
||||
}) {
|
||||
return (
|
||||
<Form className="d-flex flex-column gap-4 w-100" onSubmit={formik.handleSubmit}>
|
||||
@@ -275,13 +275,16 @@ function RegisterForm({
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
{import.meta.env.VITE_RECAPTCHA_KEY && (
|
||||
<ReCAPTCHA
|
||||
onChange={(v) => {
|
||||
formik.setFieldValue("captcha", v);
|
||||
{import.meta.env.VITE_HCAPTCHA_SITEKEY && (
|
||||
<HCaptcha
|
||||
onExpire={() => {
|
||||
formik.setFieldValue("captcha", "");
|
||||
}}
|
||||
ref={recaptchaRef}
|
||||
sitekey={import.meta.env.VITE_RECAPTCHA_KEY}
|
||||
onVerify={(token) => {
|
||||
formik.setFieldValue("captcha", token);
|
||||
}}
|
||||
ref={hcaptchaRef}
|
||||
sitekey={import.meta.env.VITE_HCAPTCHA_SITEKEY}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ COMMIT_HASH=
|
||||
LOG_LEVEL=
|
||||
|
||||
# Tachi server config (required)
|
||||
# hCaptcha secret key (server-side siteverify).
|
||||
TACHI_CAPTCHA_SECRET_KEY=
|
||||
TACHI_SESSION_SECRET=
|
||||
TACHI_OUR_URL=
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Env, ServerConfig } from "#lib/setup/config";
|
||||
import DB from "#services/pg/db";
|
||||
import nodeFetch from "#utils/fetch";
|
||||
import { Random20Hex } from "#utils/misc";
|
||||
import { CreateURLWithParams } from "#utils/url";
|
||||
import { FormatUserDoc } from "#utils/user";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type Transaction } from "kysely";
|
||||
@@ -173,20 +172,22 @@ export async function InsertDefaultUserSettings(
|
||||
}
|
||||
|
||||
export async function ValidateCaptcha(
|
||||
recaptcha: string,
|
||||
captchaResponse: string,
|
||||
remoteAddr: string | undefined,
|
||||
fetch = nodeFetch,
|
||||
) {
|
||||
const url = CreateURLWithParams(`https://www.google.com/recaptcha/api/siteverify`, {
|
||||
secret: ServerConfig.CAPTCHA_SECRET_KEY,
|
||||
response: recaptcha,
|
||||
remoteip: remoteAddr ?? "",
|
||||
});
|
||||
|
||||
const googleCaptchaRes: unknown = await fetch(url.href).then((r) => r.json());
|
||||
const verifyRes: unknown = await fetch("https://api.hcaptcha.com/siteverify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
secret: ServerConfig.CAPTCHA_SECRET_KEY,
|
||||
response: captchaResponse,
|
||||
remoteip: remoteAddr ?? "",
|
||||
}),
|
||||
}).then((r) => r.json());
|
||||
|
||||
const err = p(
|
||||
googleCaptchaRes,
|
||||
verifyRes,
|
||||
{
|
||||
success: "boolean",
|
||||
},
|
||||
@@ -196,20 +197,19 @@ export async function ValidateCaptcha(
|
||||
|
||||
if (err) {
|
||||
log.warn(
|
||||
{ googleCaptchaRes, err },
|
||||
`Google ReCaptcha returned something without a success property? Assuming this captcha check failed.`,
|
||||
{ err, verifyRes },
|
||||
`hCaptcha returned something without a success property? Assuming this captcha check failed.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// asserted above
|
||||
const gcr = googleCaptchaRes as { success: boolean };
|
||||
const hcr = verifyRes as { "error-codes"?: string[]; success: boolean };
|
||||
|
||||
if (!gcr.success) {
|
||||
log.debug({ gcr }, `Failed GCaptcha response`);
|
||||
if (!hcr.success) {
|
||||
log.debug({ errorCodes: hcr["error-codes"], hcr }, `Failed hCaptcha response`);
|
||||
}
|
||||
|
||||
return gcr.success;
|
||||
return hcr.success;
|
||||
}
|
||||
|
||||
export function MountAuthCookie(
|
||||
|
||||
Reference in New Issue
Block a user