From 7c4b4d97dcc7ffa02973aab0e68661a7cd0bd802 Mon Sep 17 00:00:00 2001 From: tdgao Date: Wed, 25 Mar 2026 09:40:05 -0600 Subject: [PATCH 01/11] update auth with new designs --- apps/frontend/src/locales/en-US/index.json | 4 +- apps/frontend/src/pages/auth.vue | 24 +-- apps/frontend/src/pages/auth/sign-in.vue | 207 ++++++++++++--------- apps/frontend/src/pages/auth/sign-up.vue | 125 ++++++++----- 4 files changed, 206 insertions(+), 154 deletions(-) diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json index 3c66ea3398..cc8c142aaa 100644 --- a/apps/frontend/src/locales/en-US/index.json +++ b/apps/frontend/src/locales/en-US/index.json @@ -267,10 +267,10 @@ "message": "Enter code..." }, "auth.sign-in.additional-options": { - "message": "Forgot password?Create an account" + "message": "Forgot password • Don't have an account? Sign up" }, "auth.sign-in.sign-in-with": { - "message": "Sign in with" + "message": "Sign into Modrinth" }, "auth.sign-in.title": { "message": "Sign In" diff --git a/apps/frontend/src/pages/auth.vue b/apps/frontend/src/pages/auth.vue index 205866bcee..20f3587ed4 100644 --- a/apps/frontend/src/pages/auth.vue +++ b/apps/frontend/src/pages/auth.vue @@ -8,12 +8,12 @@ useSeoMeta({ }) diff --git a/apps/frontend/src/components/ui/HCaptcha.vue b/apps/frontend/src/components/ui/auth/HCaptcha.vue similarity index 100% rename from apps/frontend/src/components/ui/HCaptcha.vue rename to apps/frontend/src/components/ui/auth/HCaptcha.vue diff --git a/apps/frontend/src/components/ui/auth/SignIn.vue b/apps/frontend/src/components/ui/auth/SignIn.vue new file mode 100644 index 0000000000..09a969b89a --- /dev/null +++ b/apps/frontend/src/components/ui/auth/SignIn.vue @@ -0,0 +1,295 @@ + + + diff --git a/apps/frontend/src/components/ui/auth/SignUp.vue b/apps/frontend/src/components/ui/auth/SignUp.vue new file mode 100644 index 0000000000..9b46aca712 --- /dev/null +++ b/apps/frontend/src/components/ui/auth/SignUp.vue @@ -0,0 +1,268 @@ + + + diff --git a/apps/frontend/src/pages/auth/reset-password.vue b/apps/frontend/src/pages/auth/reset-password.vue index 365a9063f5..594b08de89 100644 --- a/apps/frontend/src/pages/auth/reset-password.vue +++ b/apps/frontend/src/pages/auth/reset-password.vue @@ -76,7 +76,7 @@ import { } from '@modrinth/ui' import { useQuery } from '@tanstack/vue-query' -import HCaptcha from '@/components/ui/HCaptcha.vue' +import HCaptcha from '@/components/ui/auth/HCaptcha.vue' const client = injectModrinthClient() const { addNotification } = injectNotificationManager() diff --git a/apps/frontend/src/pages/auth/sign-in.vue b/apps/frontend/src/pages/auth/sign-in.vue index dbf4fb8878..7697c66ae8 100644 --- a/apps/frontend/src/pages/auth/sign-in.vue +++ b/apps/frontend/src/pages/auth/sign-in.vue @@ -1,177 +1,32 @@ diff --git a/apps/frontend/src/pages/auth/sign-in.vue b/apps/frontend/src/pages/auth/sign-in.vue index 30ec6a0d13..da602e810b 100644 --- a/apps/frontend/src/pages/auth/sign-in.vue +++ b/apps/frontend/src/pages/auth/sign-in.vue @@ -49,6 +49,18 @@ useHead({ const auth = await useAuth() const route = useNativeRoute() +if (route.query.state !== undefined) { + await navigateTo( + { + path: '/auth/create/oauth', + query: route.query, + }, + { + replace: true, + }, + ) +} + const redirectTarget = route.query.redirect || '' const subtleLauncherRedirectUri = ref() diff --git a/packages/ui/src/components/base/Checkbox.vue b/packages/ui/src/components/base/Checkbox.vue index 02a935e3ab..5837be1a7a 100644 --- a/packages/ui/src/components/base/Checkbox.vue +++ b/packages/ui/src/components/base/Checkbox.vue @@ -13,7 +13,7 @@ @click="toggle" > -
- -
{{ formatMessage(messages.subscribeLabel) }}
+
+
From ddceea90461511e33e11d416727d6ee6639c4a46 Mon Sep 17 00:00:00 2001 From: tdgao Date: Fri, 17 Apr 2026 09:41:53 -0600 Subject: [PATCH 08/11] remove hard coded username --- apps/frontend/src/pages/auth/create/oauth.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/pages/auth/create/oauth.vue b/apps/frontend/src/pages/auth/create/oauth.vue index da0b1ef373..0053ece482 100644 --- a/apps/frontend/src/pages/auth/create/oauth.vue +++ b/apps/frontend/src/pages/auth/create/oauth.vue @@ -66,7 +66,7 @@ const oauthFlowState = computed(() => { const defaultUsername = computed(() => { const queryUsername = route.query.username const value = Array.isArray(queryUsername) ? queryUsername[0] : queryUsername - return typeof value === 'string' && value.length > 0 ? value : 'user' + return typeof value === 'string' && value.length > 0 ? value : '' }) const dateOfBirth = ref('') From a5ccfab01f7ac9375930230fb2584ce43d78221d Mon Sep 17 00:00:00 2001 From: tdgao Date: Fri, 17 Apr 2026 16:52:02 -0600 Subject: [PATCH 09/11] implement create user validation endpoint and add more specific error responses --- apps/labrinth/src/auth/mod.rs | 20 +- apps/labrinth/src/routes/internal/flows.rs | 360 ++++++++++++++------- 2 files changed, 262 insertions(+), 118 deletions(-) diff --git a/apps/labrinth/src/auth/mod.rs b/apps/labrinth/src/auth/mod.rs index d996afb1d5..24c31d03ed 100644 --- a/apps/labrinth/src/auth/mod.rs +++ b/apps/labrinth/src/auth/mod.rs @@ -43,7 +43,13 @@ pub enum AuthenticationError { #[error( "User email is already registered on Modrinth. Try 'Forgot password' to access your account." )] - DuplicateUser, + DuplicateEmail, + #[error("Username is already taken on Modrinth.")] + UsernameTaken, + #[error( + "This authentication provider is already linked to another Modrinth account." + )] + ProviderAlreadyLinked, #[error("Invalid state sent, you probably need to get a new websocket")] SocketError, #[error("Invalid callback URL specified")] @@ -73,7 +79,11 @@ impl actix_web::ResponseError for AuthenticationError { AuthenticationError::FileHosting(..) => { StatusCode::INTERNAL_SERVER_ERROR } - AuthenticationError::DuplicateUser => StatusCode::BAD_REQUEST, + AuthenticationError::DuplicateEmail => StatusCode::BAD_REQUEST, + AuthenticationError::UsernameTaken => StatusCode::BAD_REQUEST, + AuthenticationError::ProviderAlreadyLinked => { + StatusCode::BAD_REQUEST + } AuthenticationError::SocketError => StatusCode::BAD_REQUEST, } } @@ -102,7 +112,11 @@ impl AuthenticationError { AuthenticationError::InvalidClientId => "invalid_client_id", AuthenticationError::Url => "url_error", AuthenticationError::FileHosting(..) => "file_hosting", - AuthenticationError::DuplicateUser => "duplicate_user", + AuthenticationError::DuplicateEmail => "duplicate_email", + AuthenticationError::UsernameTaken => "username_taken", + AuthenticationError::ProviderAlreadyLinked => { + "provider_already_linked" + } AuthenticationError::SocketError => "socket", } } diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index e531e824eb..a933bc55eb 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -10,6 +10,7 @@ use crate::database::models::{DBUser, DBUserId}; use crate::database::redis::RedisPool; use crate::env::ENV; use crate::file_hosting::{FileHost, FileHostPublicity}; +use crate::models::error::ApiError as ApiErrorResponse; use crate::models::notifications::NotificationBody; use crate::models::pats::Scopes; use crate::models::users::{Badges, Role}; @@ -23,6 +24,7 @@ use crate::util::ext::get_image_ext; use crate::util::img::upload_image_optimized; use crate::util::validate::validation_errors_to_string; use actix_http::header::LOCATION; +use actix_web::http::StatusCode; use actix_web::web::{Data, Query, ServiceConfig, scope}; use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use argon2::password_hash::SaltString; @@ -40,6 +42,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; +use thiserror::Error; use tracing::{error, info}; use url::Url; use validator::Validate; @@ -52,6 +55,7 @@ pub fn config(cfg: &mut ServiceConfig) { .service(auth_callback) .service(delete_auth_provider) .service(create_oauth_account) + .service(validate_create_account_with_password) .service(create_account_with_password) .service(login_password) .service(login_2fa) @@ -96,7 +100,7 @@ impl TempUser { .await? .is_some() { - return Err(AuthenticationError::DuplicateUser); + return Err(AuthenticationError::DuplicateEmail); } let user_id = @@ -107,7 +111,7 @@ impl TempUser { .wrap_err("failed to fetch existing user by id")?; if existing_id.is_some() { - return Err(AuthenticationError::DuplicateUser); + return Err(AuthenticationError::UsernameTaken); } let (avatar_url, raw_avatar_url) = if let Some(avatar_url) = @@ -1266,7 +1270,7 @@ pub async fn auth_callback( if let Some(id) = user_id { if user_id_opt.is_some() { - return Err(AuthenticationError::DuplicateUser); + return Err(AuthenticationError::ProviderAlreadyLinked); } provider @@ -1339,6 +1343,7 @@ pub async fn auth_callback( // for this, we redirect them to a frontend page which lets them set a username. // then frontend will call `/create/oauth` with the same state parameter and // chosen settings (username, subscribe to newsletter), and handle navigation. + let suggested_username = oauth_user.username.clone(); flow_guard .replace_with(DBFlow::OAuthPending { @@ -1356,7 +1361,8 @@ pub async fn auth_callback( .append_pair( "requires_dob", &requires_dob(provider).to_string(), - ); + ) + .append_pair("username", &suggested_username); let redirect_url = redirect_url.to_string(); Ok(HttpResponse::TemporaryRedirect() @@ -1394,6 +1400,10 @@ async fn create_oauth_account( redis: Data, web::Json(new_account): web::Json, ) -> Result { + new_account.validate().map_err(|err| { + ApiError::InvalidInput(validation_errors_to_string(err, None)) + })?; + if !check_hcaptcha(&req, &new_account.challenge).await? { return Err(ApiError::Turnstile); } @@ -1525,144 +1535,264 @@ pub async fn check_sendy_subscription( Ok(response.trim() == "Subscribed") } -#[derive(Deserialize, Validate)] +#[derive(Deserialize)] pub struct NewAccount { // keep in sync with NewOAuthAccount - #[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))] pub username: String, - #[validate(length(min = 8, max = 256))] pub password: String, - #[validate(email)] pub email: String, - pub challenge: String, + pub challenge: Option, pub sign_up_newsletter: Option, } -#[post("create")] -pub async fn create_account_with_password( - req: HttpRequest, - pool: Data, - redis: Data, - new_account: web::Json, - email: web::Data, -) -> Result { - new_account.0.validate().map_err(|err| { - ApiError::InvalidInput(validation_errors_to_string(err, None)) - })?; +#[derive(Debug, Validate)] +struct AccountRegisterFlow { + #[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))] + username: String, + #[validate(length(min = 8, max = 256))] + password: String, + #[validate(email)] + email: String, + sign_up_newsletter: bool, +} - if !check_hcaptcha(&req, &new_account.challenge).await? { - return Err(ApiError::Turnstile); - } +#[derive(Debug)] +struct ReadyAccountRegisterFlow { + inner: AccountRegisterFlow, +} - if crate::database::models::DBUser::get( - &new_account.username, - &**pool, - &redis, - ) - .await? - .is_some() - { - return Err(ApiError::InvalidInput("Username is taken!".to_string())); +#[derive(Debug, Error)] +enum AccountRegisterValidateError { + #[error("Username is already taken on Modrinth.")] + UsernameTaken, + #[error( + "Email is already registered on Modrinth. Try 'Forgot password' to access your account." + )] + DuplicateEmail, + #[error("{}", match .0 { + Some(feedback) => format!("Password too weak: {feedback}"), + None => "Specified password is too weak! Please improve its strength.".to_string(), + })] + WeakPassword(Option), + #[error("{0}")] + InvalidInput(String), +} + +impl AccountRegisterValidateError { + fn error_code(&self) -> &'static str { + match self { + AccountRegisterValidateError::UsernameTaken => "username_taken", + AccountRegisterValidateError::DuplicateEmail => "duplicate_email", + AccountRegisterValidateError::WeakPassword(_) => "weak_password", + AccountRegisterValidateError::InvalidInput(_) => "invalid_input", + } } +} - let mut transaction = pool.begin().await?; - let user_id = - crate::database::models::generate_user_id(&mut transaction).await?; +impl actix_web::ResponseError for AccountRegisterValidateError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } - let new_account = new_account.0; + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(ApiErrorResponse { + error: self.error_code(), + description: self.to_string(), + details: None, + }) + } +} - let score = zxcvbn::zxcvbn( - &new_account.password, - &[&new_account.username, &new_account.email], - ); +impl From for ApiError { + fn from(value: AccountRegisterValidateError) -> Self { + match &value { + AccountRegisterValidateError::UsernameTaken => { + ApiError::Authentication(AuthenticationError::UsernameTaken) + } + AccountRegisterValidateError::DuplicateEmail => { + ApiError::Authentication(AuthenticationError::DuplicateEmail) + } + _ => ApiError::InvalidInput(value.to_string()), + } + } +} - if score.score() < Score::Three { - return Err(ApiError::InvalidInput( - if let Some(feedback) = score.feedback().and_then(|x| x.warning()) { - format!("Password too weak: {feedback}") - } else { - "Specified password is too weak! Please improve its strength." - .to_string() - }, - )); +impl From for AccountRegisterFlow { + fn from(account: NewAccount) -> Self { + Self { + username: account.username, + password: account.password, + email: account.email, + sign_up_newsletter: account.sign_up_newsletter.unwrap_or(false), + } } +} - let hasher = Argon2::default(); - let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy()); - let password_hash = hasher - .hash_password(new_account.password.as_bytes(), &salt)? - .to_string(); +impl AccountRegisterFlow { + async fn validate( + self, + pool: &PgPool, + redis: &RedisPool, + ) -> Result { + validator::Validate::validate(&self).map_err(|err| { + AccountRegisterValidateError::InvalidInput( + validation_errors_to_string(err, None), + ) + })?; - if !crate::database::models::DBUser::get_by_case_insensitive_email( - &new_account.email, - &**pool, - ) - .await? - .is_empty() - { - return Err(ApiError::InvalidInput( - "Email is already registered on Modrinth! Try 'Forgot password' to access your account.".to_string(), - )); - } + if crate::database::models::DBUser::get(&self.username, pool, redis) + .await + .map_err(|err| { + AccountRegisterValidateError::InvalidInput(err.to_string()) + })? + .is_some() + { + return Err(AccountRegisterValidateError::UsernameTaken); + } - crate::database::models::DBUser { - id: user_id, - github_id: None, - discord_id: None, - gitlab_id: None, - google_id: None, - steam_id: None, - microsoft_id: None, - password: Some(password_hash), - paypal_id: None, - paypal_country: None, - paypal_email: None, - venmo_handle: None, - stripe_customer_id: None, - totp_secret: None, - username: new_account.username.clone(), - email: Some(new_account.email.clone()), - email_verified: false, - avatar_url: None, - raw_avatar_url: None, - bio: None, - created: Utc::now(), - role: Role::Developer.to_string(), - badges: Badges::default(), - allow_friend_requests: true, - is_subscribed_to_newsletter: new_account - .sign_up_newsletter - .unwrap_or(false), - } - .insert(&mut transaction) - .await?; + let score = + zxcvbn::zxcvbn(&self.password, &[&self.username, &self.email]); - let session = - issue_session(req, user_id, &mut transaction, &redis, None).await?; - let res = crate::models::sessions::Session::from(session, true, None); + if score.score() < Score::Three { + let feedback = score + .feedback() + .and_then(|x| x.warning()) + .map(|w| w.to_string()); + return Err(AccountRegisterValidateError::WeakPassword(feedback)); + } - let mailbox: Mailbox = new_account.email.parse().map_err(|_| { - ApiError::InvalidInput("Invalid email address!".to_string()) - })?; + if !crate::database::models::DBUser::get_by_case_insensitive_email( + &self.email, + pool, + ) + .await + .map_err(|err| { + AccountRegisterValidateError::InvalidInput(err.to_string()) + })? + .is_empty() + { + return Err(AccountRegisterValidateError::DuplicateEmail); + } - let flow = DBFlow::ConfirmEmail { - user_id, - confirm_email: new_account.email.clone(), + Ok(ReadyAccountRegisterFlow { inner: self }) } - .insert(Duration::hours(24), &redis) - .await?; +} - email - .send_one( - &mut transaction, - NotificationBody::VerifyEmail { flow }, +impl ReadyAccountRegisterFlow { + async fn execute( + self, + req: HttpRequest, + pool: &PgPool, + redis: &RedisPool, + email_queue: &EmailQueue, + ) -> Result { + let register_flow = self.inner; + + let mut transaction = pool.begin().await?; + let user_id = + crate::database::models::generate_user_id(&mut transaction).await?; + + let hasher = Argon2::default(); + let salt = SaltString::generate(&mut ChaCha20Rng::from_entropy()); + let password_hash = hasher + .hash_password(register_flow.password.as_bytes(), &salt)? + .to_string(); + + crate::database::models::DBUser { + id: user_id, + github_id: None, + discord_id: None, + gitlab_id: None, + google_id: None, + steam_id: None, + microsoft_id: None, + password: Some(password_hash), + paypal_id: None, + paypal_country: None, + paypal_email: None, + venmo_handle: None, + stripe_customer_id: None, + totp_secret: None, + username: register_flow.username.clone(), + email: Some(register_flow.email.clone()), + email_verified: false, + avatar_url: None, + raw_avatar_url: None, + bio: None, + created: Utc::now(), + role: Role::Developer.to_string(), + badges: Badges::default(), + allow_friend_requests: true, + is_subscribed_to_newsletter: register_flow.sign_up_newsletter, + } + .insert(&mut transaction) + .await?; + + let session = + issue_session(req, user_id, &mut transaction, redis, None).await?; + let res = crate::models::sessions::Session::from(session, true, None); + + let mailbox: Mailbox = register_flow.email.parse().map_err(|_| { + ApiError::InvalidInput("Invalid email address!".to_string()) + })?; + + let flow = DBFlow::ConfirmEmail { user_id, - mailbox, - ) + confirm_email: register_flow.email.clone(), + } + .insert(Duration::hours(24), redis) + .await?; + + email_queue + .send_one( + &mut transaction, + NotificationBody::VerifyEmail { flow }, + user_id, + mailbox, + ) + .await? + .as_user_error()?; + + transaction.commit().await?; + + Ok(res) + } +} + +#[post("create/validate")] +pub async fn validate_create_account_with_password( + pool: Data, + redis: Data, + new_account: web::Json, +) -> Result<(), AccountRegisterValidateError> { + AccountRegisterFlow::from(new_account.into_inner()) + .validate(&pool, &redis) + .await?; + + Ok(()) +} + +#[post("create")] +pub async fn create_account_with_password( + req: HttpRequest, + pool: Data, + redis: Data, + new_account: web::Json, + email: web::Data, +) -> Result { + let new_account = new_account.into_inner(); + + if !check_hcaptcha(&req, new_account.challenge.as_deref().unwrap_or("")) .await? - .as_user_error()?; + { + return Err(ApiError::Turnstile); + } - transaction.commit().await?; + let ready_flow = AccountRegisterFlow::from(new_account) + .validate(&pool, &redis) + .await?; + + let res = ready_flow.execute(req, &pool, &redis, &email).await?; Ok(HttpResponse::Ok().json(res)) } From 2d8c66186c0c37228f11fbdf878961d2d9c1bdef Mon Sep 17 00:00:00 2001 From: tdgao Date: Fri, 17 Apr 2026 16:54:44 -0600 Subject: [PATCH 10/11] feat: implement under 13 DOB guard and email/password validation route --- .../src/components/ui/auth/CreateAccount.vue | 87 +++++++++++++++---- .../src/components/ui/auth/SignUp.vue | 53 +---------- apps/frontend/src/pages/auth/sign-up.vue | 57 ++++++++++-- .../src/modules/labrinth/auth/v2.ts | 16 ++++ .../api-client/src/modules/labrinth/types.ts | 6 ++ 5 files changed, 146 insertions(+), 73 deletions(-) diff --git a/apps/frontend/src/components/ui/auth/CreateAccount.vue b/apps/frontend/src/components/ui/auth/CreateAccount.vue index 288d1ab5c0..67e318f49e 100644 --- a/apps/frontend/src/components/ui/auth/CreateAccount.vue +++ b/apps/frontend/src/components/ui/auth/CreateAccount.vue @@ -15,25 +15,29 @@ -
You must be over 13 years old to use Modrinth.
- -
-
- {{ formatMessage(messages.infoPanelText) }} +
+ {{ formatMessage(messages.over13HelperText) }} +
+ + @@ -71,7 +75,7 @@ @@ -85,6 +89,7 @@ import { ButtonStyled, Checkbox, defineMessages, + injectNotificationManager, StyledInput, useVIntl, } from '@modrinth/ui' @@ -120,7 +125,7 @@ const props = defineProps({ sourceCodeUrl: { type: String, default: - 'https://github.com/modrinth/labrinth/blob/main/apps/labrinth/src/routes/internal/flows.rs', + 'https://github.com/modrinth/code/blob/main/apps/frontend/src/components/ui/auth/CreateAccount.vue', }, onCompleteSignUp: { type: Function, @@ -165,8 +170,40 @@ const maxBirthDate = computed(() => { return date.toISOString().slice(0, 10) }) +const isDateOfBirthMissing = computed(() => props.requiresDob && dateOfBirthModel.value === '') + +const isUnder13 = computed( + () => + props.requiresDob && + dateOfBirthModel.value !== '' && + dateOfBirthModel.value > maxBirthDate.value, +) + +const { addNotification } = injectNotificationManager() const { formatMessage } = useVIntl() +function onCompleteSignUpClick() { + if (isDateOfBirthMissing.value) { + addNotification({ + title: formatMessage(messages.dateOfBirthRequiredTitle), + text: formatMessage(messages.dateOfBirthRequiredText), + type: 'warning', + }) + return + } + + if (isUnder13.value) { + addNotification({ + title: formatMessage(messages.ageRequirementWarningTitle), + text: formatMessage(messages.under13HelperText), + type: 'error', + }) + return + } + + props.onCompleteSignUp() +} + const messages = defineMessages({ title: { id: 'auth.create-account.title', @@ -176,10 +213,26 @@ const messages = defineMessages({ id: 'auth.create-account.date-of-birth.label', defaultMessage: 'Date of birth', }, + dateOfBirthRequiredTitle: { + id: 'auth.create-account.date-of-birth.required.title', + defaultMessage: 'Date of birth required', + }, + dateOfBirthRequiredText: { + id: 'auth.create-account.date-of-birth.required.text', + defaultMessage: 'Please enter your date of birth before continuing.', + }, over13HelperText: { id: 'auth.create-account.date-of-birth.over13-helper', defaultMessage: 'You must be over 13 years old to use Modrinth.', }, + under13HelperText: { + id: 'auth.create-account.date-of-birth.under13-helper', + defaultMessage: 'You cannot create an account at Modrinth unless you are 13 years old.', + }, + ageRequirementWarningTitle: { + id: 'auth.create-account.age-requirement.warning-title', + defaultMessage: 'Age requirement', + }, infoPanelText: { id: 'auth.create-account.info-panel.text', defaultMessage: diff --git a/apps/frontend/src/components/ui/auth/SignUp.vue b/apps/frontend/src/components/ui/auth/SignUp.vue index 9b46aca712..e12ebd14b1 100644 --- a/apps/frontend/src/components/ui/auth/SignUp.vue +++ b/apps/frontend/src/components/ui/auth/SignUp.vue @@ -79,13 +79,6 @@ wrapper-class="w-full" /> -