forked from didirus/AstralRinth
Merge commit '1aa2299b558fc9eaba74615e8453cfe7da8510d9' into feature-clean
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -7539,9 +7539,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.132"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||
dependencies = [
|
||||
"itoa 1.0.11",
|
||||
"memchr",
|
||||
|
||||
@@ -343,8 +343,6 @@ export default defineNuxtConfig({
|
||||
globalThis.CF_PAGES_COMMIT_SHA ||
|
||||
"unknown",
|
||||
|
||||
turnstile: { siteKey: "0x4AAAAAAAW3guHM6Eunbgwu" },
|
||||
|
||||
stripePublishableKey:
|
||||
process.env.STRIPE_PUBLISHABLE_KEY ||
|
||||
globalThis.STRIPE_PUBLISHABLE_KEY ||
|
||||
@@ -362,7 +360,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
modules: ["@vintl/nuxt", "@nuxtjs/turnstile", "@pinia/nuxt"],
|
||||
modules: ["@vintl/nuxt", "@pinia/nuxt"],
|
||||
vintl: {
|
||||
defaultLocale: "en-US",
|
||||
locales: [
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt/devtools": "^1.3.3",
|
||||
"@nuxtjs/turnstile": "^0.8.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/node": "^20.1.0",
|
||||
"@vintl/compact-number": "^2.0.5",
|
||||
|
||||
59
apps/frontend/src/components/ui/HCaptcha.vue
Normal file
59
apps/frontend/src/components/ui/HCaptcha.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
const token = defineModel();
|
||||
const id = ref(null);
|
||||
|
||||
function hCaptchaUpdateToken(newToken) {
|
||||
token.value = newToken;
|
||||
}
|
||||
|
||||
function hCaptchaReady() {
|
||||
window.hCaptchaUpdateToken = hCaptchaUpdateToken;
|
||||
id.value = window.hcaptcha.render("h-captcha");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.hCaptchaReady = hCaptchaReady;
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
src: "https://js.hcaptcha.com/1/api.js?render=explicit&onload=hCaptchaReady",
|
||||
async: true,
|
||||
defer: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
reset: () => {
|
||||
token.value = null;
|
||||
window.hcaptcha.reset(id.value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="h-captcha"
|
||||
class="h-captcha"
|
||||
data-sitekey="4a7a2c80-68f2-4190-9d52-131c76e0c14e"
|
||||
:data-theme="$theme.active === 'light' ? 'light' : 'dark'"
|
||||
data-callback="hCaptchaUpdateToken"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.h-captcha {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid var(--color-button-bg);
|
||||
height: 78px;
|
||||
|
||||
iframe {
|
||||
margin: -1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -60,20 +60,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.turnstile {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid var(--color-button-bg);
|
||||
height: 66px;
|
||||
|
||||
iframe {
|
||||
margin: -1px;
|
||||
min-width: calc(100% + 2px);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -22,12 +22,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NuxtTurnstile
|
||||
ref="turnstile"
|
||||
v-model="token"
|
||||
class="turnstile"
|
||||
:options="{ theme: $theme.active === 'light' ? 'light' : 'dark' }"
|
||||
/>
|
||||
<HCaptcha ref="captcha" v-model="token" />
|
||||
|
||||
<button class="btn btn-primary centered-btn" :disabled="!token" @click="recovery">
|
||||
<SendIcon /> {{ formatMessage(methodChoiceMessages.action) }}
|
||||
@@ -73,6 +68,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { SendIcon, MailIcon, KeyIcon } from "@modrinth/assets";
|
||||
import HCaptcha from "@/components/ui/HCaptcha.vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -165,7 +161,7 @@ if (route.query.flow) {
|
||||
step.value = "passed_challenge";
|
||||
}
|
||||
|
||||
const turnstile = ref();
|
||||
const captcha = ref();
|
||||
|
||||
const email = ref("");
|
||||
const token = ref("");
|
||||
@@ -194,7 +190,7 @@ async function recovery() {
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
turnstile.value?.reset();
|
||||
captcha.value?.reset();
|
||||
}
|
||||
stopLoading();
|
||||
}
|
||||
@@ -227,7 +223,7 @@ async function changePassword() {
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
turnstile.value?.reset();
|
||||
captcha.value?.reset();
|
||||
}
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
@@ -81,12 +81,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NuxtTurnstile
|
||||
ref="turnstile"
|
||||
v-model="token"
|
||||
class="turnstile"
|
||||
:options="{ theme: $theme.active === 'light' ? 'light' : 'dark' }"
|
||||
/>
|
||||
<HCaptcha ref="captcha" v-model="token" />
|
||||
|
||||
<button
|
||||
class="btn btn-primary continue-btn centered-btn"
|
||||
@@ -127,6 +122,7 @@ import {
|
||||
KeyIcon,
|
||||
MailIcon,
|
||||
} from "@modrinth/assets";
|
||||
import HCaptcha from "@/components/ui/HCaptcha.vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -189,7 +185,7 @@ if (auth.value.user) {
|
||||
await finishSignIn();
|
||||
}
|
||||
|
||||
const turnstile = ref();
|
||||
const captcha = ref();
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
@@ -225,7 +221,7 @@ async function beginPasswordSignIn() {
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
turnstile.value?.reset();
|
||||
captcha.value?.reset();
|
||||
}
|
||||
stopLoading();
|
||||
}
|
||||
@@ -250,7 +246,7 @@ async function begin2FASignIn() {
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
turnstile.value?.reset();
|
||||
captcha.value?.reset();
|
||||
}
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
@@ -106,12 +106,7 @@
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<NuxtTurnstile
|
||||
ref="turnstile"
|
||||
v-model="token"
|
||||
class="turnstile"
|
||||
:options="{ theme: $theme.active === 'light' ? 'light' : 'dark' }"
|
||||
/>
|
||||
<HCaptcha ref="captcha" v-model="token" />
|
||||
|
||||
<button
|
||||
class="btn btn-primary continue-btn centered-btn"
|
||||
@@ -145,6 +140,7 @@ import {
|
||||
SSOGitLabIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Checkbox } from "@modrinth/ui";
|
||||
import HCaptcha from "@/components/ui/HCaptcha.vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -209,7 +205,7 @@ if (auth.value.user) {
|
||||
await navigateTo("/dashboard");
|
||||
}
|
||||
|
||||
const turnstile = ref();
|
||||
const captcha = ref();
|
||||
|
||||
const email = ref("");
|
||||
const username = ref("");
|
||||
@@ -235,7 +231,7 @@ async function createAccount() {
|
||||
}),
|
||||
type: "error",
|
||||
});
|
||||
turnstile.value?.reset();
|
||||
captcha.value?.reset();
|
||||
}
|
||||
|
||||
const res = await useBaseFetch("auth/create", {
|
||||
@@ -264,7 +260,7 @@ async function createAccount() {
|
||||
text: err.data ? err.data.description : err,
|
||||
type: "error",
|
||||
});
|
||||
turnstile.value?.reset();
|
||||
captcha.value?.reset();
|
||||
}
|
||||
stopLoading();
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ TREMENDOUS_API_KEY=none
|
||||
TREMENDOUS_PRIVATE_KEY=none
|
||||
TREMENDOUS_CAMPAIGN_ID=none
|
||||
|
||||
TURNSTILE_SECRET=none
|
||||
HCAPTCHA_SECRET=none
|
||||
|
||||
SMTP_USERNAME=none
|
||||
SMTP_PASSWORD=none
|
||||
|
||||
@@ -450,7 +450,7 @@ pub fn check_env_vars() -> bool {
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
|
||||
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
|
||||
|
||||
failed |= check_var::<String>("TURNSTILE_SECRET");
|
||||
failed |= check_var::<String>("HCAPTCHA_SECRET");
|
||||
|
||||
failed |= check_var::<String>("SMTP_USERNAME");
|
||||
failed |= check_var::<String>("SMTP_PASSWORD");
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::queue::session::AuthQueue;
|
||||
use crate::queue::socket::ActiveSockets;
|
||||
use crate::routes::internal::session::issue_session;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::captcha::check_turnstile_captcha;
|
||||
use crate::util::captcha::check_hcaptcha;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
@@ -1468,8 +1468,6 @@ pub struct NewAccount {
|
||||
pub sign_up_newsletter: Option<bool>,
|
||||
}
|
||||
|
||||
const NEW_ACCOUNT_LIMITER_NAMESPACE: &str = "new_account_ips";
|
||||
|
||||
#[post("create")]
|
||||
pub async fn create_account_with_password(
|
||||
req: HttpRequest,
|
||||
@@ -1481,7 +1479,7 @@ pub async fn create_account_with_password(
|
||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
|
||||
if !check_turnstile_captcha(&req, &new_account.challenge).await? {
|
||||
if !check_hcaptcha(&req, &new_account.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
@@ -1566,36 +1564,6 @@ pub async fn create_account_with_password(
|
||||
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
|
||||
let res = crate::models::sessions::Session::from(session, true, None);
|
||||
|
||||
// We limit each ip to creating 5 accounts in a six hour period
|
||||
let ip = crate::util::ip::convert_to_ip_v6(&res.ip).map_err(|_| {
|
||||
ApiError::InvalidInput("unable to parse user ip!".to_string())
|
||||
})?;
|
||||
let stripped_ip = crate::util::ip::strip_ip(ip).to_string();
|
||||
|
||||
let mut conn = redis.connect().await?;
|
||||
let uses = if let Some(res) = conn
|
||||
.get(NEW_ACCOUNT_LIMITER_NAMESPACE, &stripped_ip)
|
||||
.await?
|
||||
{
|
||||
res.parse::<u64>().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if uses >= 5 {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"IP has been rate-limited.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
conn.set(
|
||||
NEW_ACCOUNT_LIMITER_NAMESPACE,
|
||||
&stripped_ip,
|
||||
&(uses + 1).to_string(),
|
||||
Some(60 * 60 * 6),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let flow = Flow::ConfirmEmail {
|
||||
user_id,
|
||||
confirm_email: new_account.email.clone(),
|
||||
@@ -1632,7 +1600,7 @@ pub async fn login_password(
|
||||
redis: Data<RedisPool>,
|
||||
login: web::Json<Login>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if !check_turnstile_captcha(&req, &login.challenge).await? {
|
||||
if !check_hcaptcha(&req, &login.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
@@ -2082,7 +2050,7 @@ pub async fn reset_password_begin(
|
||||
redis: Data<RedisPool>,
|
||||
reset_password: web::Json<ResetPassword>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if !check_turnstile_captcha(&req, &reset_password.challenge).await? {
|
||||
if !check_hcaptcha(&req, &reset_password.challenge).await? {
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ use crate::routes::ApiError;
|
||||
use crate::util::env::parse_var;
|
||||
use actix_web::HttpRequest;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub async fn check_turnstile_captcha(
|
||||
pub async fn check_hcaptcha(
|
||||
req: &HttpRequest,
|
||||
challenge: &str,
|
||||
) -> Result<bool, ApiError> {
|
||||
@@ -19,6 +19,8 @@ pub async fn check_turnstile_captcha(
|
||||
conn_info.peer_addr()
|
||||
};
|
||||
|
||||
let ip_addr = ip_addr.ok_or(ApiError::Turnstile)?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -26,13 +28,16 @@ pub async fn check_turnstile_captcha(
|
||||
success: bool,
|
||||
}
|
||||
|
||||
let mut form = HashMap::new();
|
||||
|
||||
let secret = dotenvy::var("HCAPTCHA_SECRET")?;
|
||||
form.insert("response", challenge);
|
||||
form.insert("secret", &*secret);
|
||||
form.insert("remoteip", ip_addr);
|
||||
|
||||
let val: Response = client
|
||||
.post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
|
||||
.json(&json!({
|
||||
"secret": dotenvy::var("TURNSTILE_SECRET")?,
|
||||
"response": challenge,
|
||||
"remoteip": ip_addr,
|
||||
}))
|
||||
.post("https://api.hcaptcha.com/siteverify")
|
||||
.form(&form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| ApiError::Turnstile)?
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -276,9 +276,6 @@ importers:
|
||||
'@nuxt/devtools':
|
||||
specifier: ^1.3.3
|
||||
version: 1.6.0(rollup@4.24.0)(vite@5.4.9(@types/node@20.16.11)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3))
|
||||
'@nuxtjs/turnstile':
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0(magicast@0.3.5)(rollup@4.24.0)
|
||||
'@types/dompurify':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
@@ -1785,9 +1782,6 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^8.23.0
|
||||
|
||||
'@nuxtjs/turnstile@0.8.0':
|
||||
resolution: {integrity: sha512-m4u/fVKVKF1fz1nH8LYYXY51jGzd1ROfujh0cfiGAYtQtn9D+V3nKdHFG9iHmRtfZxqzbL6z32V0k+6YYD6iig==}
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
@@ -9052,17 +9046,6 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
'@nuxtjs/turnstile@0.8.0(magicast@0.3.5)(rollup@4.24.0)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0)
|
||||
defu: 6.1.4
|
||||
pathe: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
- rollup
|
||||
- supports-color
|
||||
- webpack-sources
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@pagefind/darwin-arm64@1.1.1':
|
||||
|
||||
Reference in New Issue
Block a user